diff --git a/DevLog.xcodeproj/project.pbxproj b/DevLog.xcodeproj/project.pbxproj index 74e9bf8b..18261118 100644 --- a/DevLog.xcodeproj/project.pbxproj +++ b/DevLog.xcodeproj/project.pbxproj @@ -396,7 +396,7 @@ DEVELOPMENT_TEAM = 4CPC6N38WA; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 17; - MARKETING_VERSION = 1.0.1; + MARKETING_VERSION = 1.1.0; PRODUCT_BUNDLE_IDENTIFIER = opfic.DevLog_Unit; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = NO; @@ -421,7 +421,7 @@ DEVELOPMENT_TEAM = 4CPC6N38WA; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 17; - MARKETING_VERSION = 1.0.1; + MARKETING_VERSION = 1.1.0; PRODUCT_BUNDLE_IDENTIFIER = opfic.DevLog_Unit; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = NO; @@ -457,7 +457,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.1; + MARKETING_VERSION = 1.1.0; PRODUCT_BUNDLE_IDENTIFIER = opfic.DevLog.DevLogWidget; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -490,7 +490,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.1; + MARKETING_VERSION = 1.1.0; PRODUCT_BUNDLE_IDENTIFIER = opfic.DevLog.DevLogWidget; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -536,7 +536,7 @@ "@executable_path/Frameworks", ); LOCALIZED_STRING_SWIFTUI_SUPPORT = YES; - MARKETING_VERSION = 1.0.1; + MARKETING_VERSION = 1.1.0; PRODUCT_BUNDLE_IDENTIFIER = opfic.DevLog; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -583,7 +583,7 @@ "@executable_path/Frameworks", ); LOCALIZED_STRING_SWIFTUI_SUPPORT = YES; - MARKETING_VERSION = 1.0.1; + MARKETING_VERSION = 1.1.0; PRODUCT_BUNDLE_IDENTIFIER = opfic.DevLog; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/DevLog/App/Assembler/DataAssembler.swift b/DevLog/App/Assembler/DataAssembler.swift index 682b47f4..d62a519f 100644 --- a/DevLog/App/Assembler/DataAssembler.swift +++ b/DevLog/App/Assembler/DataAssembler.swift @@ -22,7 +22,8 @@ final class DataAssembler: Assembler { AuthenticationService.self, name: "GoogleAuthenticationService" ), - userService: container.resolve(UserService.self) + userService: container.resolve(UserService.self), + widgetSnapshotUpdater: container.resolve(WidgetSnapshotUpdater.self) ) } diff --git a/DevLog/Data/Repository/AuthenticationRepositoryImpl.swift b/DevLog/Data/Repository/AuthenticationRepositoryImpl.swift index a96b2862..ba7d7db8 100644 --- a/DevLog/Data/Repository/AuthenticationRepositoryImpl.swift +++ b/DevLog/Data/Repository/AuthenticationRepositoryImpl.swift @@ -11,19 +11,22 @@ final class AuthenticationRepositoryImpl: AuthenticationRepository { private let githubAuthService: AuthenticationService private let googleAuthService: AuthenticationService private let userService: UserService + private let widgetSnapshotUpdater: WidgetSnapshotUpdater init( authService: AuthService, appleAuthService: AuthenticationService, githubAuthService: AuthenticationService, googleAuthService: AuthenticationService, - userService: UserService + userService: UserService, + widgetSnapshotUpdater: WidgetSnapshotUpdater ) { self.authService = authService self.appleAuthService = appleAuthService self.githubAuthService = githubAuthService self.googleAuthService = googleAuthService self.userService = userService + self.widgetSnapshotUpdater = widgetSnapshotUpdater } func signIn(_ provider: AuthProvider) async throws { @@ -58,6 +61,7 @@ final class AuthenticationRepositoryImpl: AuthenticationRepository { let provider = AuthProvider(rawValue: providerID) else { try await authService.clearCurrentSession() + widgetSnapshotUpdater.clear() return } @@ -73,6 +77,8 @@ final class AuthenticationRepositoryImpl: AuthenticationRepository { } catch AuthError.notAuthenticated { try await authService.clearCurrentSession() } + + widgetSnapshotUpdater.clear() } func restore() -> Bool { @@ -100,5 +106,6 @@ final class AuthenticationRepositoryImpl: AuthenticationRepository { try await authService.deleteCurrentUser() try await authService.clearCurrentSession() + widgetSnapshotUpdater.clear() } } diff --git a/DevLog/Storage/Persistence/WidgetSnapshotPreferenceStore.swift b/DevLog/Storage/Persistence/WidgetSnapshotPreferenceStore.swift index 56094f79..1eebbe82 100644 --- a/DevLog/Storage/Persistence/WidgetSnapshotPreferenceStore.swift +++ b/DevLog/Storage/Persistence/WidgetSnapshotPreferenceStore.swift @@ -8,10 +8,10 @@ import Foundation final class WidgetSnapshotPreferenceStore { - private enum Key { - static let heatmapActivityTypes = "Profile.heatmap.activityTypes" - static let todayDueDateVisibility = "Today.dueDateVisibility" - static let todayFocusVisibility = "Today.focusVisibility" + private enum Key: String, CaseIterable { + case heatmapActivityTypes = "Profile.heatmap.activityTypes" + case todayDueDateVisibility = "Today.dueDateVisibility" + case todayFocusVisibility = "Today.focusVisibility" } private let userDefaults: UserDefaults @@ -21,11 +21,11 @@ final class WidgetSnapshotPreferenceStore { } func heatmapActivityTypes() -> [String] { - userDefaults.stringArray(forKey: Key.heatmapActivityTypes) ?? [] + userDefaults.stringArray(forKey: Key.heatmapActivityTypes.rawValue) ?? [] } func setHeatmapActivityTypes(_ activityTypes: [String]) { - userDefaults.set(activityTypes, forKey: Key.heatmapActivityTypes) + userDefaults.set(activityTypes, forKey: Key.heatmapActivityTypes.rawValue) } func selectedActivityKinds() -> Set { @@ -41,8 +41,8 @@ final class WidgetSnapshotPreferenceStore { } func todayDisplayOptions() -> TodayDisplayOptions { - let dueDateVisibilityRawValue = userDefaults.string(forKey: Key.todayDueDateVisibility) - let focusVisibilityRawValue = userDefaults.string(forKey: Key.todayFocusVisibility) + let dueDateVisibilityRawValue = userDefaults.string(forKey: Key.todayDueDateVisibility.rawValue) + let focusVisibilityRawValue = userDefaults.string(forKey: Key.todayFocusVisibility.rawValue) return TodayDisplayOptions( dueDateVisibility: TodayDisplayOptions.DueDateVisibility( @@ -55,7 +55,13 @@ final class WidgetSnapshotPreferenceStore { } func setTodayDisplayOptions(_ options: TodayDisplayOptions) { - userDefaults.set(options.dueDateVisibility.rawValue, forKey: Key.todayDueDateVisibility) - userDefaults.set(options.focusVisibility.rawValue, forKey: Key.todayFocusVisibility) + userDefaults.set(options.dueDateVisibility.rawValue, forKey: Key.todayDueDateVisibility.rawValue) + userDefaults.set(options.focusVisibility.rawValue, forKey: Key.todayFocusVisibility.rawValue) + } + + func clear() { + Key.allCases.forEach { + userDefaults.removeObject(forKey: $0.rawValue) + } } } diff --git a/DevLog/Storage/Persistence/WidgetSnapshotUpdater.swift b/DevLog/Storage/Persistence/WidgetSnapshotUpdater.swift index 38c54eb7..bfcdac41 100644 --- a/DevLog/Storage/Persistence/WidgetSnapshotUpdater.swift +++ b/DevLog/Storage/Persistence/WidgetSnapshotUpdater.swift @@ -104,4 +104,10 @@ final class WidgetSnapshotUpdater { ) } } + + func clear() { + snapshotStore.clearSnapshots() + preferenceStore.clear() + WidgetCenter.shared.reloadAllTimelines() + } } diff --git a/DevLog/Widget/Common/WidgetSharedDefaultsStore.swift b/DevLog/Widget/Common/WidgetSharedDefaultsStore.swift index eedc63ac..b30ad1b0 100644 --- a/DevLog/Widget/Common/WidgetSharedDefaultsStore.swift +++ b/DevLog/Widget/Common/WidgetSharedDefaultsStore.swift @@ -21,4 +21,8 @@ final class WidgetSharedDefaultsStore { func setData(_ value: Data?, forKey key: String) { userDefaults.set(value, forKey: key) } + + func removeObject(forKey key: String) { + userDefaults.removeObject(forKey: key) + } } diff --git a/DevLog/Widget/Common/WidgetSnapshotStore.swift b/DevLog/Widget/Common/WidgetSnapshotStore.swift index 71abd4ff..5a887efd 100644 --- a/DevLog/Widget/Common/WidgetSnapshotStore.swift +++ b/DevLog/Widget/Common/WidgetSnapshotStore.swift @@ -35,4 +35,10 @@ final class WidgetSnapshotStore { guard let data = store.data(forKey: WidgetSnapshotKey.heatmap) else { return nil } return try decoder.decode(HeatmapWidgetSnapshot.self, from: data) } + + func clearSnapshots() { + WidgetSnapshotKey.snapshots.forEach { + store.removeObject(forKey: $0) + } + } } diff --git a/DevLogWidget/Heatmap/HeatmapWidget.swift b/DevLogWidget/Heatmap/HeatmapWidget.swift index 9f30cce4..4d70e4f2 100644 --- a/DevLogWidget/Heatmap/HeatmapWidget.swift +++ b/DevLogWidget/Heatmap/HeatmapWidget.swift @@ -22,7 +22,7 @@ struct HeatmapWidget: Widget { .containerBackground(.fill.tertiary, for: .widget) .widgetURL(WidgetDeepLink.heatmapURL) } - .configurationDisplayName("Heatmap") + .configurationDisplayName(LocalizedStringResource("widget_heatmap_title")) .description("widget_heatmap_description") .supportedFamilies([.systemSmall, .systemMedium]) } diff --git a/DevLogWidget/Heatmap/HeatmapWidgetConfigurationIntent.swift b/DevLogWidget/Heatmap/HeatmapWidgetConfigurationIntent.swift index f14c7841..134d20d9 100644 --- a/DevLogWidget/Heatmap/HeatmapWidgetConfigurationIntent.swift +++ b/DevLogWidget/Heatmap/HeatmapWidgetConfigurationIntent.swift @@ -9,6 +9,6 @@ import AppIntents import WidgetKit struct HeatmapWidgetConfigurationIntent: WidgetConfigurationIntent { - static var title: LocalizedStringResource = "Heatmap" + static var title: LocalizedStringResource = "widget_heatmap_title" static var description = IntentDescription("widget_heatmap_description") } diff --git a/DevLogWidget/Resource/Localizable.xcstrings b/DevLogWidget/Resource/Localizable.xcstrings index 3a61a290..7e5ea55a 100644 --- a/DevLogWidget/Resource/Localizable.xcstrings +++ b/DevLogWidget/Resource/Localizable.xcstrings @@ -6,12 +6,6 @@ }, "%lld" : { - }, - "Heatmap" : { - - }, - "Today" : { - }, "widget_heatmap_current_month_title" : { "extractionState" : "manual", @@ -64,6 +58,23 @@ } } }, + "widget_heatmap_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Heatmap" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Heatmap" + } + } + } + }, "widget_today_description" : { "extractionState" : "manual", "localizations" : { @@ -97,6 +108,23 @@ } } } + }, + "widget_today_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Today" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Today" + } + } + } } }, "version" : "1.0" diff --git a/DevLogWidget/Today/TodayTodoWidget.swift b/DevLogWidget/Today/TodayTodoWidget.swift index e002c9b5..57e15e61 100644 --- a/DevLogWidget/Today/TodayTodoWidget.swift +++ b/DevLogWidget/Today/TodayTodoWidget.swift @@ -23,7 +23,7 @@ struct TodayTodoWidget: Widget { .widgetURL(WidgetDeepLink.todayTodoURL) } .description("widget_today_description") - .configurationDisplayName("Today") + .configurationDisplayName(LocalizedStringResource("widget_today_title")) .supportedFamilies([.systemSmall, .systemMedium]) } } diff --git a/DevLogWidget/Today/TodayTodoWidgetConfigurationIntent.swift b/DevLogWidget/Today/TodayTodoWidgetConfigurationIntent.swift index 111cf4a4..8078ee87 100644 --- a/DevLogWidget/Today/TodayTodoWidgetConfigurationIntent.swift +++ b/DevLogWidget/Today/TodayTodoWidgetConfigurationIntent.swift @@ -9,6 +9,6 @@ import AppIntents import WidgetKit struct TodayTodoWidgetConfigurationIntent: WidgetConfigurationIntent { - static var title: LocalizedStringResource = "Today" + static var title: LocalizedStringResource = "widget_today_title" static var description = IntentDescription("widget_today_description") } diff --git a/DevLogWidget/Today/TodayTodoWidgetEntryView.swift b/DevLogWidget/Today/TodayTodoWidgetEntryView.swift index 4b9f7177..6e748abe 100644 --- a/DevLogWidget/Today/TodayTodoWidgetEntryView.swift +++ b/DevLogWidget/Today/TodayTodoWidgetEntryView.swift @@ -14,7 +14,7 @@ struct TodayTodoWidgetEntryView: View { var body: some View { VStack(alignment: .leading) { - Text("Today") + Text("widget_today_title") .font(.headline) Spacer() diff --git a/DevLog_Unit/Widget/WidgetSnapshotUpdaterTests.swift b/DevLog_Unit/Widget/WidgetSnapshotUpdaterTests.swift index 404b72ae..4e5dd86d 100644 --- a/DevLog_Unit/Widget/WidgetSnapshotUpdaterTests.swift +++ b/DevLog_Unit/Widget/WidgetSnapshotUpdaterTests.swift @@ -66,9 +66,55 @@ struct WidgetSnapshotUpdaterTests { #expect(snapshot.maxCount == 1) } + @Test("WidgetSnapshotUpdater는 모든 위젯 스냅샷을 삭제한다") + func widgetSnapshotUpdater는_모든_위젯_스냅샷을_삭제한다() throws { + let calendar = Calendar(identifier: .gregorian) + let quarterStart = try #require(calendar.date(from: DateComponents(year: 2026, month: 4, day: 1))) + let now = try #require(calendar.date(from: DateComponents(year: 2026, month: 4, day: 30))) + let fixture = makeFixture(calendar: calendar) + let todo = try makeTodayTodoItem(now: now) + fixture.preferenceStore.setHeatmapActivityTypes(["created"]) + fixture.preferenceStore.setTodayDisplayOptions( + TodayDisplayOptions( + dueDateVisibility: .withDueDateOnly, + focusVisibility: .focusedOnly + ) + ) + + fixture.updater.updateTodaySnapshot( + todos: [todo], + displayOptions: .default, + now: now + ) + fixture.updater.updateHeatmapSnapshot( + createdTodos: [ + makeTodo( + id: "created", + createdAt: now + ) + ], + completedTodos: [], + deletedTodos: [], + selectedActivityKinds: [.created], + quarterStart: quarterStart, + now: now + ) + + fixture.updater.clear() + + #expect(try fixture.snapshotStore.loadTodaySnapshot() == nil) + #expect(try fixture.snapshotStore.loadHeatmapSnapshot() == nil) + #expect(fixture.preferenceStore.heatmapActivityTypes().isEmpty) + #expect(fixture.preferenceStore.todayDisplayOptions() == .default) + } + private func makeFixture( calendar: Calendar = .current - ) -> (updater: WidgetSnapshotUpdater, snapshotStore: WidgetSnapshotStore) { + ) -> ( + updater: WidgetSnapshotUpdater, + snapshotStore: WidgetSnapshotStore, + preferenceStore: WidgetSnapshotPreferenceStore + ) { let suiteName = "WidgetSnapshotUpdaterTests.\(UUID().uuidString)" let userDefaults = UserDefaults(suiteName: suiteName) ?? .standard userDefaults.removePersistentDomain(forName: suiteName) @@ -83,7 +129,7 @@ struct WidgetSnapshotUpdaterTests { preferenceStore: preferenceStore, heatmapFactory: HeatmapWidgetSnapshotFactory(calendar: calendar) ) - return (updater, snapshotStore) + return (updater, snapshotStore, preferenceStore) } private func makeTodayTodoItem(now: Date) throws -> TodayTodoItem { diff --git a/WidgetShared/WidgetSnapshotKey.swift b/WidgetShared/WidgetSnapshotKey.swift index 3cf46991..3f972d8b 100644 --- a/WidgetShared/WidgetSnapshotKey.swift +++ b/WidgetShared/WidgetSnapshotKey.swift @@ -10,4 +10,5 @@ import Foundation enum WidgetSnapshotKey { static let today = "Widget.today.snapshot" static let heatmap = "Widget.heatmap.snapshot" + static let snapshots = [today, heatmap] }