Skip to content

Commit 1ad4597

Browse files
authored
[#602] 앱이 독에 내려갔을 때 Todo Fetch 요청이 되는 현상을 해결한다 (#657)
* chore: 커밋 메시지 지침 보강 * feat: 앱이 백그라운드로 넘어갈 시 날짜 변경에 대한 싱크 구현 * fix: Todo 변경 시 위젯 동기화 요청을 백그라운드로 지연 * fix: 설정된 위젯 기준으로 동기화 fetch 범위 제한 * refactor: 위젯 동기화 요청 상태 제거 * refactor: 위젯 설정 조회 기반 동기화 분기 제거 * docs: 불필요 이미지 제거 * refactor: 위젯 스냅샷 갱신 흐름 정리 * refactor: 파라미터명 수정 * refactor: Today 위젯 스냅샷 저장 항목 최대 3개 제한 * refactor: Today 위젯 스냅샷 항목 구조 정리 * fix: 위젯 스냅샷 원본 갱신 누락 수정
1 parent 6394850 commit 1ad4597

21 files changed

Lines changed: 577 additions & 175 deletions

AGENTS.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,16 @@ These may proceed after inspection when they do not change architecture meaning:
107107
- Do not claim architecture work is complete without checking the diff scope.
108108
- 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.
109109

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

112122
- DevLog-specific working rules belong in this repository, not in global agent memory.

Application/DevLogApp/Sources/App/DevLogApp.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ struct DevLogApp: App {
1818
@Environment(\.diContainer) var container: DIContainer
1919
@Environment(\.scenePhase) var scenePhase
2020
@State private var windowEvent = TodoEditorWindowEvent()
21+
@State private var syncDate = Date()
2122

2223
init() {
2324
AppAssembler().assemble(AppDIContainer.shared)
@@ -38,6 +39,17 @@ struct DevLogApp: App {
3839
.autocorrectionDisabled()
3940
.onChange(of: scenePhase) { _, phase in
4041
guard phase == .background else { return }
42+
let now = Date()
43+
44+
// 위젯 갱신은 앱 실행 시 로그인 세션 흐름에서 한 번 요청된다. (WidgetSessionSyncHandler.swift:47)
45+
// 따라서 이 백그라운드 트리거는 매번 최신 데이터를 다시 가져오기 위한 경로가 아니라,
46+
// 앱이 실행된 상태로 날짜가 넘어가서 Today widget의 분류 기준일이 바뀌었을 때만
47+
// 기존 위젯 갱신 흐름을 보조로 허용하기 위한 안전장치다.
48+
// 같은 날의 첫 백그라운드 진입을 막는 것은 의도된 동작이며,
49+
// 앱이 꺼져 있는 동안 날짜가 바뀐 경우는 다음 실행 시 세션 기반 갱신 요청이 담당한다.
50+
guard !Calendar.current.isDate(syncDate, inSameDayAs: now) else { return }
51+
52+
syncDate = now
4153
container.resolve(WidgetSyncEventBus.self).publish(.syncRequested)
4254
}
4355
}

Application/DevLogData/Sources/DataAssembler.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ public final class DataAssembler: Assembler {
4141
todoService: container.resolve(TodoService.self),
4242
todoCategoryService: container.resolve(TodoCategoryService.self),
4343
store: container.resolve(MemoryCacheStore.self),
44-
widgetSyncEventBus: container.resolve(WidgetSyncEventBus.self),
45-
todoMutationEventBus: container.resolve(TodoMutationEventBus.self)
44+
updater: container.resolve(WidgetSnapshotUpdater.self),
45+
eventBus: container.resolve(TodoMutationEventBus.self)
4646
)
4747
}
4848

Application/DevLogData/Sources/Mapper/TodoMapping.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,19 @@ public extension WidgetTodoSnapshot {
8989
dueDate: todo.dueDate
9090
)
9191
}
92+
93+
static func fromDomain(_ draft: TodoDraft) -> Self {
94+
WidgetTodoSnapshot(
95+
id: draft.id,
96+
number: nil,
97+
title: draft.title,
98+
isPinned: draft.isPinned,
99+
createdAt: draft.createdAt,
100+
completedAt: draft.completedAt,
101+
deletedAt: nil,
102+
dueDate: draft.dueDate
103+
)
104+
}
92105
}
93106

94107
public extension TodoCursorDTO {

Application/DevLogData/Sources/Protocol/WidgetSnapshotUpdater.swift

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,64 @@ import DevLogCore
1010

1111
public protocol WidgetSnapshotUpdater {
1212
func updateTodaySnapshot(
13-
todos: [WidgetTodoSnapshot],
13+
todos: [WidgetTodoSnapshot]?,
14+
displayOptions: TodayDisplayOptions?,
1415
now: Date
1516
)
16-
func updateTodaySnapshot(
17-
todos: [WidgetTodoSnapshot],
18-
displayOptions: TodayDisplayOptions,
17+
18+
func updateHeatmapSnapshot(
19+
createdTodos: [WidgetTodoSnapshot]?,
20+
completedTodos: [WidgetTodoSnapshot]?,
21+
deletedTodos: [WidgetTodoSnapshot]?,
22+
quarterStart: Date?,
1923
now: Date
2024
)
21-
func updateHeatmapSnapshot(
22-
createdTodos: [WidgetTodoSnapshot],
23-
completedTodos: [WidgetTodoSnapshot],
24-
deletedTodos: [WidgetTodoSnapshot],
25-
quarterStart: Date,
25+
26+
func upsertTodoSnapshot(
27+
_ todo: WidgetTodoSnapshot,
2628
now: Date
2729
)
30+
31+
func deleteTodoSnapshot(
32+
todoId: String,
33+
deletedAt: Date,
34+
now: Date
35+
)
36+
37+
func restoreTodoSnapshot(
38+
todoId: String,
39+
now: Date
40+
)
41+
2842
func clear()
2943
}
44+
45+
public extension WidgetSnapshotUpdater {
46+
func updateTodaySnapshot(
47+
todos: [WidgetTodoSnapshot]? = nil,
48+
displayOptions: TodayDisplayOptions? = nil,
49+
now: Date
50+
) {
51+
updateTodaySnapshot(
52+
todos: todos,
53+
displayOptions: displayOptions,
54+
now: now
55+
)
56+
}
57+
58+
func updateHeatmapSnapshot(
59+
createdTodos: [WidgetTodoSnapshot]? = nil,
60+
completedTodos: [WidgetTodoSnapshot]? = nil,
61+
deletedTodos: [WidgetTodoSnapshot]? = nil,
62+
quarterStart: Date? = nil,
63+
now: Date
64+
) {
65+
updateHeatmapSnapshot(
66+
createdTodos: createdTodos,
67+
completedTodos: completedTodos,
68+
deletedTodos: deletedTodos,
69+
quarterStart: quarterStart,
70+
now: now
71+
)
72+
}
73+
}

Application/DevLogData/Sources/Repository/TodoRepositoryImpl.swift

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,21 @@ final class TodoRepositoryImpl: TodoRepository {
1717
private let todoService: TodoService
1818
private let todoCategoryService: TodoCategoryService
1919
private let store: MemoryCacheStore
20-
private let widgetSyncEventBus: WidgetSyncEventBus
21-
private let todoMutationEventBus: TodoMutationEventBus
20+
private let updater: WidgetSnapshotUpdater
21+
private let eventBus: TodoMutationEventBus
2222

2323
init(
2424
todoService: TodoService,
2525
todoCategoryService: TodoCategoryService,
2626
store: MemoryCacheStore,
27-
widgetSyncEventBus: WidgetSyncEventBus,
28-
todoMutationEventBus: TodoMutationEventBus
27+
updater: WidgetSnapshotUpdater,
28+
eventBus: TodoMutationEventBus
2929
) {
3030
self.todoService = todoService
3131
self.todoCategoryService = todoCategoryService
3232
self.store = store
33-
self.widgetSyncEventBus = widgetSyncEventBus
34-
self.todoMutationEventBus = todoMutationEventBus
33+
self.updater = updater
34+
self.eventBus = eventBus
3535
}
3636

3737
func fetchTodos(_ query: TodoQuery, cursor: TodoCursor?) async throws -> TodoPage {
@@ -132,18 +132,23 @@ final class TodoRepositoryImpl: TodoRepository {
132132
func upsertTodo(_ todo: Todo) async throws {
133133
let todoRequest = TodoRequest.fromDomain(todo)
134134
try await upsertTodo(todoRequest)
135-
todoMutationEventBus.publish(.updated(todo.id))
135+
let now = Date()
136+
let snapshot = WidgetTodoSnapshot.fromDomain(todo)
137+
updater.upsertTodoSnapshot(snapshot, now: now)
138+
eventBus.publish(.updated(todo.id))
136139
}
137140

138141
func upsertTodo(_ todoDraft: TodoDraft) async throws {
139142
let todoRequest = TodoRequest.fromDomain(todoDraft)
140143
try await upsertTodo(todoRequest)
144+
let now = Date()
145+
let snapshot = WidgetTodoSnapshot.fromDomain(todoDraft)
146+
updater.upsertTodoSnapshot(snapshot, now: now)
141147
}
142148

143149
private func upsertTodo(_ todoRequest: TodoRequest) async throws {
144150
do {
145151
try await todoService.upsertTodo(request: todoRequest)
146-
widgetSyncEventBus.publish(.syncRequested)
147152
} catch {
148153
throw error.toDomain()
149154
}
@@ -152,8 +157,9 @@ final class TodoRepositoryImpl: TodoRepository {
152157
func deleteTodo(_ todoId: String) async throws {
153158
do {
154159
try await todoService.deleteTodo(todoId: todoId)
155-
widgetSyncEventBus.publish(.syncRequested)
156-
todoMutationEventBus.publish(.deleted(todoId))
160+
let now = Date()
161+
updater.deleteTodoSnapshot(todoId: todoId, deletedAt: now, now: now)
162+
eventBus.publish(.deleted(todoId))
157163
} catch {
158164
throw error.toDomain()
159165
}
@@ -162,8 +168,9 @@ final class TodoRepositoryImpl: TodoRepository {
162168
func undoDeleteTodo(_ todoId: String) async throws {
163169
do {
164170
try await todoService.undoDeleteTodo(todoId: todoId)
165-
widgetSyncEventBus.publish(.syncRequested)
166-
todoMutationEventBus.publish(.restored(todoId))
171+
let now = Date()
172+
updater.restoreTodoSnapshot(todoId: todoId, now: now)
173+
eventBus.publish(.restored(todoId))
167174
} catch {
168175
throw error.toDomain()
169176
}

Application/DevLogData/Sources/Repository/UserPreferencesRepositoryImpl.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ final class UserPreferencesRepositoryImpl: UserPreferencesRepository {
9595

9696
func setHeatmapActivityTypes(_ activityTypes: [String]) {
9797
widgetSnapshotPreferenceStore.setHeatmapActivityTypes(activityTypes)
98-
widgetSyncEventBus.publish(.syncRequested)
98+
widgetSyncEventBus.publish(.refreshRequested)
9999
}
100100

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

105105
func setTodayDisplayOptions(_ options: TodayDisplayOptions) {
106106
widgetSnapshotPreferenceStore.setTodayDisplayOptions(options)
107-
widgetSyncEventBus.publish(.syncRequested)
107+
widgetSyncEventBus.publish(.refreshRequested)
108108
}
109109
}

Application/DevLogData/Sources/Widget/WidgetSyncEvent.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@
77

88
public enum WidgetSyncEvent: Equatable {
99
case syncRequested
10+
case refreshRequested
1011
}

Application/DevLogData/Tests/Repository/TodoRepositoryImplTests.swift

Lines changed: 59 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,24 @@ import DevLogDomain
1313
@testable import DevLogData
1414

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

2121
try await fixture.repository.upsertTodo(todo)
2222
try await fixture.repository.deleteTodo(todo.id)
2323
try await fixture.repository.undoDeleteTodo(todo.id)
2424

25-
let events = fixture.widgetSyncEventBus.events
26-
#expect(events == [.syncRequested, .syncRequested, .syncRequested])
27-
2825
let mutationEvents = fixture.todoMutationEventBus.publishedEvents()
2926
#expect(mutationEvents == [.updated(todo.id), .deleted(todo.id), .restored(todo.id)])
27+
#expect(fixture.widgetSnapshotUpdater.upsertedTodoIds == [todo.id])
28+
#expect(fixture.widgetSnapshotUpdater.deletedTodoIds == [todo.id])
29+
#expect(fixture.widgetSnapshotUpdater.restoredTodoIds == [todo.id])
3030
}
3131

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

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

58-
let syncEvents = fixture.widgetSyncEventBus.events
59-
#expect(syncEvents.isEmpty)
60-
6158
let mutationEvents = fixture.todoMutationEventBus.publishedEvents()
6259
#expect(mutationEvents.isEmpty)
60+
#expect(fixture.widgetSnapshotUpdater.upsertedTodoIds.isEmpty)
61+
#expect(fixture.widgetSnapshotUpdater.deletedTodoIds.isEmpty)
62+
#expect(fixture.widgetSnapshotUpdater.restoredTodoIds.isEmpty)
6363
}
6464

6565
private func makeFixture() -> Fixture {
6666
let todoService = TodoServiceSpy()
6767
let todoCategoryService = TodoCategoryServiceSpy()
6868
let store = TodoRepositoryMemoryCacheStoreSpy()
69-
let widgetSyncEventBus = WidgetSyncEventBusSpy()
69+
let widgetSnapshotUpdater = WidgetSnapshotUpdaterSpy()
7070
let todoMutationEventBus = TodoMutationEventBusSpy()
7171
let repository = TodoRepositoryImpl(
7272
todoService: todoService,
7373
todoCategoryService: todoCategoryService,
7474
store: store,
75-
widgetSyncEventBus: widgetSyncEventBus,
76-
todoMutationEventBus: todoMutationEventBus
75+
updater: widgetSnapshotUpdater,
76+
eventBus: todoMutationEventBus
7777
)
7878

7979
return Fixture(
8080
repository: repository,
8181
todoService: todoService,
82-
widgetSyncEventBus: widgetSyncEventBus,
82+
widgetSnapshotUpdater: widgetSnapshotUpdater,
8383
todoMutationEventBus: todoMutationEventBus
8484
)
8585
}
@@ -107,7 +107,7 @@ struct TodoRepositoryImplTests {
107107
private struct Fixture {
108108
let repository: TodoRepositoryImpl
109109
let todoService: TodoServiceSpy
110-
let widgetSyncEventBus: WidgetSyncEventBusSpy
110+
let widgetSnapshotUpdater: WidgetSnapshotUpdaterSpy
111111
let todoMutationEventBus: TodoMutationEventBusSpy
112112
}
113113

@@ -176,18 +176,6 @@ private final class TodoRepositoryMemoryCacheStoreSpy: MemoryCacheStore {
176176
}
177177
}
178178

179-
private final class WidgetSyncEventBusSpy: WidgetSyncEventBus {
180-
private(set) var events = [WidgetSyncEvent]()
181-
182-
func observe() -> AnyPublisher<WidgetSyncEvent, Never> {
183-
Empty().eraseToAnyPublisher()
184-
}
185-
186-
func publish(_ event: WidgetSyncEvent) {
187-
events.append(event)
188-
}
189-
}
190-
191179
private final class TodoMutationEventBusSpy: TodoMutationEventBus {
192180
private var capturedEvents = [TodoMutationEvent]()
193181

@@ -204,6 +192,50 @@ private final class TodoMutationEventBusSpy: TodoMutationEventBus {
204192
}
205193
}
206194

195+
private final class WidgetSnapshotUpdaterSpy: WidgetSnapshotUpdater {
196+
private(set) var upsertedTodoIds = [String]()
197+
private(set) var deletedTodoIds = [String]()
198+
private(set) var restoredTodoIds = [String]()
199+
200+
func updateTodaySnapshot(
201+
todos: [WidgetTodoSnapshot]?,
202+
displayOptions: TodayDisplayOptions?,
203+
now: Date
204+
) { }
205+
206+
func updateHeatmapSnapshot(
207+
createdTodos: [WidgetTodoSnapshot]?,
208+
completedTodos: [WidgetTodoSnapshot]?,
209+
deletedTodos: [WidgetTodoSnapshot]?,
210+
quarterStart: Date?,
211+
now: Date
212+
) { }
213+
214+
func upsertTodoSnapshot(
215+
_ todo: WidgetTodoSnapshot,
216+
now: Date
217+
) {
218+
upsertedTodoIds.append(todo.id)
219+
}
220+
221+
func deleteTodoSnapshot(
222+
todoId: String,
223+
deletedAt: Date,
224+
now: Date
225+
) {
226+
deletedTodoIds.append(todoId)
227+
}
228+
229+
func restoreTodoSnapshot(
230+
todoId: String,
231+
now: Date
232+
) {
233+
restoredTodoIds.append(todoId)
234+
}
235+
236+
func clear() { }
237+
}
238+
207239
private enum TodoRepositoryImplTestsError: Error, Equatable {
208240
case serviceFailed
209241
case unexpectedCall

0 commit comments

Comments
 (0)