Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,16 @@ These may proceed after inspection when they do not change architecture meaning:
- Do not claim architecture work is complete without checking the diff scope.
- Do not spend time on unrelated generated project or lockfile churn. Keep generated workspace/project and `Package.resolved` changes out of source control unless they are part of an explicitly approved dependency-lock policy.

## Git and commit rules

- Commit messages must start with a short prefix used by recent local commits, such as `feat`, `fix`, `refactor`, `chore`, `test`, `docs`, `ui`, or `rollback`.
- Write commit message prose in Korean.
- Keep implementation names such as `ToastPresenter`, `toastHost`, `MainView`, `DevLogPresentation`, file paths, commands, branch names, and commit hashes in their original form.
- Do not translate implementation names into Korean unless the user explicitly asks for a user-facing Korean label.
- Do not write a commit message body.
- When checking recent commit-message style, do not infer local commit style from GitHub merge or squash-merge subjects such as `[#123] ... (#456)`.
- For squash-merge commits, inspect the commit body and use the individual bullet commit messages as the style reference.

## Canonical project rules

- DevLog-specific working rules belong in this repository, not in global agent memory.
Expand Down
12 changes: 12 additions & 0 deletions Application/DevLogApp/Sources/App/DevLogApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ struct DevLogApp: App {
@Environment(\.diContainer) var container: DIContainer
@Environment(\.scenePhase) var scenePhase
@State private var windowEvent = TodoEditorWindowEvent()
@State private var syncDate = Date()

init() {
AppAssembler().assemble(AppDIContainer.shared)
Expand All @@ -38,6 +39,17 @@ struct DevLogApp: App {
.autocorrectionDisabled()
.onChange(of: scenePhase) { _, phase in
guard phase == .background else { return }
let now = Date()

// 위젯 갱신은 앱 실행 시 로그인 세션 흐름에서 한 번 요청된다. (WidgetSessionSyncHandler.swift:47)
// 따라서 이 백그라운드 트리거는 매번 최신 데이터를 다시 가져오기 위한 경로가 아니라,
// 앱이 실행된 상태로 날짜가 넘어가서 Today widget의 분류 기준일이 바뀌었을 때만
// 기존 위젯 갱신 흐름을 보조로 허용하기 위한 안전장치다.
// 같은 날의 첫 백그라운드 진입을 막는 것은 의도된 동작이며,
// 앱이 꺼져 있는 동안 날짜가 바뀐 경우는 다음 실행 시 세션 기반 갱신 요청이 담당한다.
guard !Calendar.current.isDate(syncDate, inSameDayAs: now) else { return }

syncDate = now
container.resolve(WidgetSyncEventBus.self).publish(.syncRequested)
}
}
Expand Down
4 changes: 2 additions & 2 deletions Application/DevLogData/Sources/DataAssembler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ public final class DataAssembler: Assembler {
todoService: container.resolve(TodoService.self),
todoCategoryService: container.resolve(TodoCategoryService.self),
store: container.resolve(MemoryCacheStore.self),
widgetSyncEventBus: container.resolve(WidgetSyncEventBus.self),
todoMutationEventBus: container.resolve(TodoMutationEventBus.self)
updater: container.resolve(WidgetSnapshotUpdater.self),
eventBus: container.resolve(TodoMutationEventBus.self)
)
}

Expand Down
13 changes: 13 additions & 0 deletions Application/DevLogData/Sources/Mapper/TodoMapping.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,19 @@ public extension WidgetTodoSnapshot {
dueDate: todo.dueDate
)
}

static func fromDomain(_ draft: TodoDraft) -> Self {
WidgetTodoSnapshot(
id: draft.id,
number: nil,
title: draft.title,
isPinned: draft.isPinned,
createdAt: draft.createdAt,
completedAt: draft.completedAt,
deletedAt: nil,
dueDate: draft.dueDate
)
}
}

public extension TodoCursorDTO {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,64 @@ import DevLogCore

public protocol WidgetSnapshotUpdater {
func updateTodaySnapshot(
todos: [WidgetTodoSnapshot],
todos: [WidgetTodoSnapshot]?,
displayOptions: TodayDisplayOptions?,
now: Date
)
func updateTodaySnapshot(
todos: [WidgetTodoSnapshot],
displayOptions: TodayDisplayOptions,

func updateHeatmapSnapshot(
createdTodos: [WidgetTodoSnapshot]?,
completedTodos: [WidgetTodoSnapshot]?,
deletedTodos: [WidgetTodoSnapshot]?,
quarterStart: Date?,
now: Date
)
func updateHeatmapSnapshot(
createdTodos: [WidgetTodoSnapshot],
completedTodos: [WidgetTodoSnapshot],
deletedTodos: [WidgetTodoSnapshot],
quarterStart: Date,

func upsertTodoSnapshot(
_ todo: WidgetTodoSnapshot,
now: Date
)

func deleteTodoSnapshot(
todoId: String,
deletedAt: Date,
now: Date
)

func restoreTodoSnapshot(
todoId: String,
now: Date
)

func clear()
}

public extension WidgetSnapshotUpdater {
func updateTodaySnapshot(
todos: [WidgetTodoSnapshot]? = nil,
displayOptions: TodayDisplayOptions? = nil,
now: Date
) {
updateTodaySnapshot(
todos: todos,
displayOptions: displayOptions,
now: now
)
}

func updateHeatmapSnapshot(
createdTodos: [WidgetTodoSnapshot]? = nil,
completedTodos: [WidgetTodoSnapshot]? = nil,
deletedTodos: [WidgetTodoSnapshot]? = nil,
quarterStart: Date? = nil,
now: Date
) {
updateHeatmapSnapshot(
createdTodos: createdTodos,
completedTodos: completedTodos,
deletedTodos: deletedTodos,
quarterStart: quarterStart,
now: now
)
}
}
31 changes: 19 additions & 12 deletions Application/DevLogData/Sources/Repository/TodoRepositoryImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,21 @@ final class TodoRepositoryImpl: TodoRepository {
private let todoService: TodoService
private let todoCategoryService: TodoCategoryService
private let store: MemoryCacheStore
private let widgetSyncEventBus: WidgetSyncEventBus
private let todoMutationEventBus: TodoMutationEventBus
private let updater: WidgetSnapshotUpdater
private let eventBus: TodoMutationEventBus

init(
todoService: TodoService,
todoCategoryService: TodoCategoryService,
store: MemoryCacheStore,
widgetSyncEventBus: WidgetSyncEventBus,
todoMutationEventBus: TodoMutationEventBus
updater: WidgetSnapshotUpdater,
eventBus: TodoMutationEventBus
) {
self.todoService = todoService
self.todoCategoryService = todoCategoryService
self.store = store
self.widgetSyncEventBus = widgetSyncEventBus
self.todoMutationEventBus = todoMutationEventBus
self.updater = updater
self.eventBus = eventBus
}

func fetchTodos(_ query: TodoQuery, cursor: TodoCursor?) async throws -> TodoPage {
Expand Down Expand Up @@ -132,18 +132,23 @@ final class TodoRepositoryImpl: TodoRepository {
func upsertTodo(_ todo: Todo) async throws {
let todoRequest = TodoRequest.fromDomain(todo)
try await upsertTodo(todoRequest)
todoMutationEventBus.publish(.updated(todo.id))
let now = Date()
let snapshot = WidgetTodoSnapshot.fromDomain(todo)
updater.upsertTodoSnapshot(snapshot, now: now)
eventBus.publish(.updated(todo.id))
}

func upsertTodo(_ todoDraft: TodoDraft) async throws {
let todoRequest = TodoRequest.fromDomain(todoDraft)
try await upsertTodo(todoRequest)
let now = Date()
let snapshot = WidgetTodoSnapshot.fromDomain(todoDraft)
updater.upsertTodoSnapshot(snapshot, now: now)
}
Comment thread
opficdev marked this conversation as resolved.

private func upsertTodo(_ todoRequest: TodoRequest) async throws {
do {
try await todoService.upsertTodo(request: todoRequest)
widgetSyncEventBus.publish(.syncRequested)
} catch {
throw error.toDomain()
}
Expand All @@ -152,8 +157,9 @@ final class TodoRepositoryImpl: TodoRepository {
func deleteTodo(_ todoId: String) async throws {
do {
try await todoService.deleteTodo(todoId: todoId)
widgetSyncEventBus.publish(.syncRequested)
todoMutationEventBus.publish(.deleted(todoId))
let now = Date()
updater.deleteTodoSnapshot(todoId: todoId, deletedAt: now, now: now)
eventBus.publish(.deleted(todoId))
} catch {
throw error.toDomain()
}
Expand All @@ -162,8 +168,9 @@ final class TodoRepositoryImpl: TodoRepository {
func undoDeleteTodo(_ todoId: String) async throws {
do {
try await todoService.undoDeleteTodo(todoId: todoId)
widgetSyncEventBus.publish(.syncRequested)
todoMutationEventBus.publish(.restored(todoId))
let now = Date()
updater.restoreTodoSnapshot(todoId: todoId, now: now)
eventBus.publish(.restored(todoId))
} catch {
throw error.toDomain()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ final class UserPreferencesRepositoryImpl: UserPreferencesRepository {

func setHeatmapActivityTypes(_ activityTypes: [String]) {
widgetSnapshotPreferenceStore.setHeatmapActivityTypes(activityTypes)
widgetSyncEventBus.publish(.syncRequested)
widgetSyncEventBus.publish(.refreshRequested)
}

func todayDisplayOptions() -> TodayDisplayOptions {
Expand All @@ -104,6 +104,6 @@ final class UserPreferencesRepositoryImpl: UserPreferencesRepository {

func setTodayDisplayOptions(_ options: TodayDisplayOptions) {
widgetSnapshotPreferenceStore.setTodayDisplayOptions(options)
widgetSyncEventBus.publish(.syncRequested)
widgetSyncEventBus.publish(.refreshRequested)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@

public enum WidgetSyncEvent: Equatable {
case syncRequested
case refreshRequested
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,24 @@ import DevLogDomain
@testable import DevLogData

struct TodoRepositoryImplTests {
@Test("Todo 변경 성공 시 위젯 동기화와 mutation 이벤트를 발행한다")
func todo_변경_성공_시_위젯_동기화와_mutation_이벤트를_발행한다() async throws {
@Test("Todo 변경 성공 시 mutation 이벤트를 발행한다")
func todo_변경_성공_시_mutation_이벤트를_발행한다() async throws {
let fixture = makeFixture()
let todo = makeTodo()

try await fixture.repository.upsertTodo(todo)
try await fixture.repository.deleteTodo(todo.id)
try await fixture.repository.undoDeleteTodo(todo.id)

let events = fixture.widgetSyncEventBus.events
#expect(events == [.syncRequested, .syncRequested, .syncRequested])

let mutationEvents = fixture.todoMutationEventBus.publishedEvents()
#expect(mutationEvents == [.updated(todo.id), .deleted(todo.id), .restored(todo.id)])
#expect(fixture.widgetSnapshotUpdater.upsertedTodoIds == [todo.id])
#expect(fixture.widgetSnapshotUpdater.deletedTodoIds == [todo.id])
#expect(fixture.widgetSnapshotUpdater.restoredTodoIds == [todo.id])
}

@Test("Todo 변경 실패 시 위젯 동기화와 mutation 이벤트를 발행하지 않는다")
func todo_변경_실패_시_위젯_동기화와_mutation_이벤트를_발행하지_않는다() async throws {
@Test("Todo 변경 실패 시 mutation 이벤트를 발행하지 않는다")
func todo_변경_실패_시_mutation_이벤트를_발행하지_않는다() async throws {
let fixture = makeFixture()
let todo = makeTodo()

Expand All @@ -55,31 +55,31 @@ struct TodoRepositoryImplTests {
#expect(error as? TodoRepositoryImplTestsError == .serviceFailed)
}

let syncEvents = fixture.widgetSyncEventBus.events
#expect(syncEvents.isEmpty)

let mutationEvents = fixture.todoMutationEventBus.publishedEvents()
#expect(mutationEvents.isEmpty)
#expect(fixture.widgetSnapshotUpdater.upsertedTodoIds.isEmpty)
#expect(fixture.widgetSnapshotUpdater.deletedTodoIds.isEmpty)
#expect(fixture.widgetSnapshotUpdater.restoredTodoIds.isEmpty)
}

private func makeFixture() -> Fixture {
let todoService = TodoServiceSpy()
let todoCategoryService = TodoCategoryServiceSpy()
let store = TodoRepositoryMemoryCacheStoreSpy()
let widgetSyncEventBus = WidgetSyncEventBusSpy()
let widgetSnapshotUpdater = WidgetSnapshotUpdaterSpy()
let todoMutationEventBus = TodoMutationEventBusSpy()
let repository = TodoRepositoryImpl(
todoService: todoService,
todoCategoryService: todoCategoryService,
store: store,
widgetSyncEventBus: widgetSyncEventBus,
todoMutationEventBus: todoMutationEventBus
updater: widgetSnapshotUpdater,
eventBus: todoMutationEventBus
)

return Fixture(
repository: repository,
todoService: todoService,
widgetSyncEventBus: widgetSyncEventBus,
widgetSnapshotUpdater: widgetSnapshotUpdater,
todoMutationEventBus: todoMutationEventBus
)
}
Expand Down Expand Up @@ -107,7 +107,7 @@ struct TodoRepositoryImplTests {
private struct Fixture {
let repository: TodoRepositoryImpl
let todoService: TodoServiceSpy
let widgetSyncEventBus: WidgetSyncEventBusSpy
let widgetSnapshotUpdater: WidgetSnapshotUpdaterSpy
let todoMutationEventBus: TodoMutationEventBusSpy
}

Expand Down Expand Up @@ -176,18 +176,6 @@ private final class TodoRepositoryMemoryCacheStoreSpy: MemoryCacheStore {
}
}

private final class WidgetSyncEventBusSpy: WidgetSyncEventBus {
private(set) var events = [WidgetSyncEvent]()

func observe() -> AnyPublisher<WidgetSyncEvent, Never> {
Empty().eraseToAnyPublisher()
}

func publish(_ event: WidgetSyncEvent) {
events.append(event)
}
}

private final class TodoMutationEventBusSpy: TodoMutationEventBus {
private var capturedEvents = [TodoMutationEvent]()

Expand All @@ -204,6 +192,50 @@ private final class TodoMutationEventBusSpy: TodoMutationEventBus {
}
}

private final class WidgetSnapshotUpdaterSpy: WidgetSnapshotUpdater {
private(set) var upsertedTodoIds = [String]()
private(set) var deletedTodoIds = [String]()
private(set) var restoredTodoIds = [String]()

func updateTodaySnapshot(
todos: [WidgetTodoSnapshot]?,
displayOptions: TodayDisplayOptions?,
now: Date
) { }

func updateHeatmapSnapshot(
createdTodos: [WidgetTodoSnapshot]?,
completedTodos: [WidgetTodoSnapshot]?,
deletedTodos: [WidgetTodoSnapshot]?,
quarterStart: Date?,
now: Date
) { }

func upsertTodoSnapshot(
_ todo: WidgetTodoSnapshot,
now: Date
) {
upsertedTodoIds.append(todo.id)
}

func deleteTodoSnapshot(
todoId: String,
deletedAt: Date,
now: Date
) {
deletedTodoIds.append(todoId)
}

func restoreTodoSnapshot(
todoId: String,
now: Date
) {
restoredTodoIds.append(todoId)
}

func clear() { }
}

private enum TodoRepositoryImplTestsError: Error, Equatable {
case serviceFailed
case unexpectedCall
Expand Down
Loading
Loading