From e28a8b61a72400ec315c267f026206a70749f807 Mon Sep 17 00:00:00 2001 From: opficdev Date: Fri, 17 Apr 2026 17:10:52 +0900 Subject: [PATCH 01/17] =?UTF-8?q?chore:=20=EA=B8=B0=EB=B3=B8=20=EC=9C=84?= =?UTF-8?q?=EC=A0=AF=20=EC=9D=B5=EC=8A=A4=ED=85=90=EC=85=98=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog.xcodeproj/project.pbxproj | 177 +++++++++++++++++- .../xcschemes/DevLogWidgetExtension.xcscheme | 115 ++++++++++++ .../xcschemes/xcschememanagement.plist | 10 + .../AccentColor.colorset/Contents.json | 11 ++ .../AppIcon.appiconset/Contents.json | 35 ++++ DevLogWidget/Assets.xcassets/Contents.json | 6 + .../WidgetBackground.colorset/Contents.json | 11 ++ DevLogWidget/DevLogWidget.swift | 84 +++++++++ DevLogWidget/DevLogWidgetBundle.swift | 16 ++ DevLogWidget/Info.plist | 11 ++ 10 files changed, 475 insertions(+), 1 deletion(-) create mode 100644 DevLog.xcodeproj/xcshareddata/xcschemes/DevLogWidgetExtension.xcscheme create mode 100644 DevLogWidget/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 DevLogWidget/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 DevLogWidget/Assets.xcassets/Contents.json create mode 100644 DevLogWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json create mode 100644 DevLogWidget/DevLogWidget.swift create mode 100644 DevLogWidget/DevLogWidgetBundle.swift create mode 100644 DevLogWidget/Info.plist diff --git a/DevLog.xcodeproj/project.pbxproj b/DevLog.xcodeproj/project.pbxproj index e94976e3..b8a9ff31 100644 --- a/DevLog.xcodeproj/project.pbxproj +++ b/DevLog.xcodeproj/project.pbxproj @@ -15,6 +15,9 @@ DFABA3B02E23526500FEFBDB /* FirebaseFirestore in Frameworks */ = {isa = PBXBuildFile; productRef = DFABA3AF2E23526500FEFBDB /* FirebaseFirestore */; }; DFABA3B22E23526500FEFBDB /* FirebaseFunctions in Frameworks */ = {isa = PBXBuildFile; productRef = DFABA3B12E23526500FEFBDB /* FirebaseFunctions */; }; DFABA3B42E23526500FEFBDB /* FirebaseMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = DFABA3B32E23526500FEFBDB /* FirebaseMessaging */; }; + DFD3A9722F8E89DD001DA7CD /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DFD3A9712F8E89DD001DA7CD /* WidgetKit.framework */; }; + DFD3A9742F8E89DD001DA7CD /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DFD3A9732F8E89DD001DA7CD /* SwiftUI.framework */; }; + DFD3A97F2F8E89DF001DA7CD /* DevLogWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = DFD3A9702F8E89DD001DA7CD /* DevLogWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; DFD3B68D2F8F1FB8001DA7CD /* Nexa in Frameworks */ = {isa = PBXBuildFile; productRef = DFD3B68C2F8F1FB8001DA7CD /* Nexa */; }; DFD645402EC827A10073E133 /* .gitignore in Resources */ = {isa = PBXBuildFile; fileRef = DFD6453F2EC827A10073E133 /* .gitignore */; }; DFD74E2F2E423EA700613803 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = DFD74E2E2E423EA700613803 /* README.md */; }; @@ -29,10 +32,34 @@ remoteGlobalIDString = DFD48AFF2DC4D6E2005905C5; remoteInfo = DevLog; }; + DFD3A97D2F8E89DF001DA7CD /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DFD48AF82DC4D6E2005905C5 /* Project object */; + proxyType = 1; + remoteGlobalIDString = DFD3A96F2F8E89DD001DA7CD; + remoteInfo = DevLogWidgetExtension; + }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + DFD3A9802F8E89DF001DA7CD /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + DFD3A97F2F8E89DF001DA7CD /* DevLogWidgetExtension.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ DF3416492E45F67C00F9312B /* DevLog_Unit.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DevLog_Unit.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + DFD3A9702F8E89DD001DA7CD /* DevLogWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = DevLogWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + DFD3A9712F8E89DD001DA7CD /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; + DFD3A9732F8E89DD001DA7CD /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; DFD48B002DC4D6E2005905C5 /* DevLog.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DevLog.app; sourceTree = BUILT_PRODUCTS_DIR; }; DFD6453F2EC827A10073E133 /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitignore; sourceTree = ""; }; DFD74E2E2E423EA700613803 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; @@ -47,6 +74,13 @@ ); target = DFD48AFF2DC4D6E2005905C5 /* DevLog */; }; + DFD3A9842F8E89DF001DA7CD /* Exceptions for "DevLogWidget" folder in "DevLogWidgetExtension" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = DFD3A96F2F8E89DD001DA7CD /* DevLogWidgetExtension */; + }; /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ @@ -63,6 +97,14 @@ path = DevLog; sourceTree = ""; }; + DFD3A9752F8E89DD001DA7CD /* DevLogWidget */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + DFD3A9842F8E89DF001DA7CD /* Exceptions for "DevLogWidget" folder in "DevLogWidgetExtension" target */, + ); + path = DevLogWidget; + sourceTree = ""; + }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -73,6 +115,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + DFD3A96D2F8E89DD001DA7CD /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DFD3A9742F8E89DD001DA7CD /* SwiftUI.framework in Frameworks */, + DFD3A9722F8E89DD001DA7CD /* WidgetKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; DFD48AFD2DC4D6E2005905C5 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -100,6 +151,7 @@ DF8AB7982E938B0B00E50BBF /* DevLog */, DF34164A2E45F67C00F9312B /* DevLog_Unit */, DFD74E2E2E423EA700613803 /* README.md */, + DFD3A9752F8E89DD001DA7CD /* DevLogWidget */, DFE28EB62DCCF26300B28FE5 /* Frameworks */, DFD48B012DC4D6E2005905C5 /* Products */, ); @@ -110,6 +162,7 @@ children = ( DF3416492E45F67C00F9312B /* DevLog_Unit.xctest */, DFD48B002DC4D6E2005905C5 /* DevLog.app */, + DFD3A9702F8E89DD001DA7CD /* DevLogWidgetExtension.appex */, ); name = Products; sourceTree = ""; @@ -117,6 +170,8 @@ DFE28EB62DCCF26300B28FE5 /* Frameworks */ = { isa = PBXGroup; children = ( + DFD3A9712F8E89DD001DA7CD /* WidgetKit.framework */, + DFD3A9732F8E89DD001DA7CD /* SwiftUI.framework */, ); name = Frameworks; sourceTree = ""; @@ -147,6 +202,28 @@ productReference = DF3416492E45F67C00F9312B /* DevLog_Unit.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; + DFD3A96F2F8E89DD001DA7CD /* DevLogWidgetExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = DFD3A9832F8E89DF001DA7CD /* Build configuration list for PBXNativeTarget "DevLogWidgetExtension" */; + buildPhases = ( + DFD3A96C2F8E89DD001DA7CD /* Sources */, + DFD3A96D2F8E89DD001DA7CD /* Frameworks */, + DFD3A96E2F8E89DD001DA7CD /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + DFD3A9752F8E89DD001DA7CD /* DevLogWidget */, + ); + name = DevLogWidgetExtension; + packageProductDependencies = ( + ); + productName = DevLogWidgetExtension; + productReference = DFD3A9702F8E89DD001DA7CD /* DevLogWidgetExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; DFD48AFF2DC4D6E2005905C5 /* DevLog */ = { isa = PBXNativeTarget; buildConfigurationList = DFD48B112DC4D6E4005905C5 /* Build configuration list for PBXNativeTarget "DevLog" */; @@ -154,11 +231,13 @@ DFD48AFC2DC4D6E2005905C5 /* Sources */, DFD48AFD2DC4D6E2005905C5 /* Frameworks */, DFD48AFE2DC4D6E2005905C5 /* Resources */, + DFD3A9802F8E89DF001DA7CD /* Embed Foundation Extensions */, ); buildRules = ( ); dependencies = ( DF66A07D2EA52E9F0098E643 /* PBXTargetDependency */, + DFD3A97E2F8E89DF001DA7CD /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( DF8AB7982E938B0B00E50BBF /* DevLog */, @@ -187,13 +266,16 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1640; + LastSwiftUpdateCheck = 2640; LastUpgradeCheck = 2600; TargetAttributes = { DF3416442E45F67C00F9312B = { CreatedOnToolsVersion = 16.3; TestTargetID = DFD48AFF2DC4D6E2005905C5; }; + DFD3A96F2F8E89DD001DA7CD = { + CreatedOnToolsVersion = 26.4; + }; DFD48AFF2DC4D6E2005905C5 = { CreatedOnToolsVersion = 16.3; }; @@ -223,6 +305,7 @@ targets = ( DF3416442E45F67C00F9312B /* DevLog_Unit */, DFD48AFF2DC4D6E2005905C5 /* DevLog */, + DFD3A96F2F8E89DD001DA7CD /* DevLogWidgetExtension */, ); }; /* End PBXProject section */ @@ -235,6 +318,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + DFD3A96E2F8E89DD001DA7CD /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; DFD48AFE2DC4D6E2005905C5 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -254,6 +344,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + DFD3A96C2F8E89DD001DA7CD /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; DFD48AFC2DC4D6E2005905C5 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -273,6 +370,11 @@ isa = PBXTargetDependency; productRef = DF66A07C2EA52E9F0098E643 /* SwiftLintBuildToolPlugin */; }; + DFD3A97E2F8E89DF001DA7CD /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DFD3A96F2F8E89DD001DA7CD /* DevLogWidgetExtension */; + targetProxy = DFD3A97D2F8E89DF001DA7CD /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -326,6 +428,70 @@ }; name = Release; }; + DFD3A9812F8E89DF001DA7CD /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 4CPC6N38WA; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = DevLogWidget/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = DevLogWidget; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 26.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = opfic.DevLog.DevLogWidget; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + DFD3A9822F8E89DF001DA7CD /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 4CPC6N38WA; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = DevLogWidget/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = DevLogWidget; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 26.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = opfic.DevLog.DevLogWidget; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; DFD48B122DC4D6E4005905C5 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReferenceAnchor = DF8AB7982E938B0B00E50BBF /* DevLog */; @@ -561,6 +727,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + DFD3A9832F8E89DF001DA7CD /* Build configuration list for PBXNativeTarget "DevLogWidgetExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DFD3A9812F8E89DF001DA7CD /* Debug */, + DFD3A9822F8E89DF001DA7CD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; DFD48AFB2DC4D6E2005905C5 /* Build configuration list for PBXProject "DevLog" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/DevLog.xcodeproj/xcshareddata/xcschemes/DevLogWidgetExtension.xcscheme b/DevLog.xcodeproj/xcshareddata/xcschemes/DevLogWidgetExtension.xcscheme new file mode 100644 index 00000000..ce2c141f --- /dev/null +++ b/DevLog.xcodeproj/xcshareddata/xcschemes/DevLogWidgetExtension.xcscheme @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DevLog.xcodeproj/xcuserdata/opfic.xcuserdatad/xcschemes/xcschememanagement.plist b/DevLog.xcodeproj/xcuserdata/opfic.xcuserdatad/xcschemes/xcschememanagement.plist index 8d3cbfd3..d9ba791d 100644 --- a/DevLog.xcodeproj/xcuserdata/opfic.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/DevLog.xcodeproj/xcuserdata/opfic.xcuserdatad/xcschemes/xcschememanagement.plist @@ -9,6 +9,11 @@ orderHint 0 + DevLogWidgetExtension.xcscheme_^#shared#^_ + + orderHint + 3 + DevLog_Unit.xcscheme_^#shared#^_ orderHint @@ -27,6 +32,11 @@ primary + DFD3A96F2F8E89DD001DA7CD + + primary + + DFD48AFF2DC4D6E2005905C5 primary diff --git a/DevLogWidget/Assets.xcassets/AccentColor.colorset/Contents.json b/DevLogWidget/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/DevLogWidget/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DevLogWidget/Assets.xcassets/AppIcon.appiconset/Contents.json b/DevLogWidget/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..23058801 --- /dev/null +++ b/DevLogWidget/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DevLogWidget/Assets.xcassets/Contents.json b/DevLogWidget/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/DevLogWidget/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DevLogWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json b/DevLogWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/DevLogWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DevLogWidget/DevLogWidget.swift b/DevLogWidget/DevLogWidget.swift new file mode 100644 index 00000000..6bac91e6 --- /dev/null +++ b/DevLogWidget/DevLogWidget.swift @@ -0,0 +1,84 @@ +// +// DevLogWidget.swift +// DevLogWidget +// +// Created by ์ตœ์œค์ง„ on 4/14/26. +// + +import WidgetKit +import SwiftUI + +struct Provider: TimelineProvider { + func placeholder(in context: Context) -> SimpleEntry { + SimpleEntry(date: Date(), emoji: "๐Ÿ˜€") + } + + func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) { + let entry = SimpleEntry(date: Date(), emoji: "๐Ÿ˜€") + completion(entry) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> ()) { + var entries: [SimpleEntry] = [] + + // Generate a timeline consisting of five entries an hour apart, starting from the current date. + let currentDate = Date() + for hourOffset in 0 ..< 5 { + let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)! + let entry = SimpleEntry(date: entryDate, emoji: "๐Ÿ˜€") + entries.append(entry) + } + + let timeline = Timeline(entries: entries, policy: .atEnd) + completion(timeline) + } + +// func relevances() async -> WidgetRelevances { +// // Generate a list containing the contexts this widget is relevant in. +// } +} + +struct SimpleEntry: TimelineEntry { + let date: Date + let emoji: String +} + +struct DevLogWidgetEntryView : View { + var entry: Provider.Entry + + var body: some View { + VStack { + Text("Time:") + Text(entry.date, style: .time) + + Text("Emoji:") + Text(entry.emoji) + } + } +} + +struct DevLogWidget: Widget { + let kind: String = "DevLogWidget" + + var body: some WidgetConfiguration { + StaticConfiguration(kind: kind, provider: Provider()) { entry in + if #available(iOS 17.0, *) { + DevLogWidgetEntryView(entry: entry) + .containerBackground(.fill.tertiary, for: .widget) + } else { + DevLogWidgetEntryView(entry: entry) + .padding() + .background() + } + } + .configurationDisplayName("My Widget") + .description("This is an example widget.") + } +} + +#Preview(as: .systemSmall) { + DevLogWidget() +} timeline: { + SimpleEntry(date: .now, emoji: "๐Ÿ˜€") + SimpleEntry(date: .now, emoji: "๐Ÿคฉ") +} diff --git a/DevLogWidget/DevLogWidgetBundle.swift b/DevLogWidget/DevLogWidgetBundle.swift new file mode 100644 index 00000000..55035a17 --- /dev/null +++ b/DevLogWidget/DevLogWidgetBundle.swift @@ -0,0 +1,16 @@ +// +// DevLogWidgetBundle.swift +// DevLogWidget +// +// Created by ์ตœ์œค์ง„ on 4/14/26. +// + +import WidgetKit +import SwiftUI + +@main +struct DevLogWidgetBundle: WidgetBundle { + var body: some Widget { + DevLogWidget() + } +} diff --git a/DevLogWidget/Info.plist b/DevLogWidget/Info.plist new file mode 100644 index 00000000..0f118fb7 --- /dev/null +++ b/DevLogWidget/Info.plist @@ -0,0 +1,11 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.widgetkit-extension + + + From be7c5af1565ecbc57e0670650221115ec94d363e Mon Sep 17 00:00:00 2001 From: opficdev Date: Wed, 15 Apr 2026 00:05:31 +0900 Subject: [PATCH 02/17] =?UTF-8?q?chore:=20Resource=20=EB=94=94=EB=A0=89?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=EB=A1=9C=20=ED=8C=8C=EC=9D=BC=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog.xcodeproj/project.pbxproj | 8 +++++--- .../Assets.xcassets/AccentColor.colorset/Contents.json | 0 .../Assets.xcassets/AppIcon.appiconset/Contents.json | 0 .../{ => Resource}/Assets.xcassets/Contents.json | 0 .../WidgetBackground.colorset/Contents.json | 0 DevLogWidget/Resource/DevLogWidget.entitlements | 10 ++++++++++ DevLogWidget/{ => Resource}/Info.plist | 0 7 files changed, 15 insertions(+), 3 deletions(-) rename DevLogWidget/{ => Resource}/Assets.xcassets/AccentColor.colorset/Contents.json (100%) rename DevLogWidget/{ => Resource}/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename DevLogWidget/{ => Resource}/Assets.xcassets/Contents.json (100%) rename DevLogWidget/{ => Resource}/Assets.xcassets/WidgetBackground.colorset/Contents.json (100%) create mode 100644 DevLogWidget/Resource/DevLogWidget.entitlements rename DevLogWidget/{ => Resource}/Info.plist (100%) diff --git a/DevLog.xcodeproj/project.pbxproj b/DevLog.xcodeproj/project.pbxproj index b8a9ff31..33d9d80d 100644 --- a/DevLog.xcodeproj/project.pbxproj +++ b/DevLog.xcodeproj/project.pbxproj @@ -77,7 +77,7 @@ DFD3A9842F8E89DF001DA7CD /* Exceptions for "DevLogWidget" folder in "DevLogWidgetExtension" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( - Info.plist, + Resource/Info.plist, ); target = DFD3A96F2F8E89DD001DA7CD /* DevLogWidgetExtension */; }; @@ -433,12 +433,13 @@ buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CODE_SIGN_ENTITLEMENTS = DevLogWidget/Resource/DevLogWidget.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 4CPC6N38WA; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = DevLogWidget/Info.plist; + INFOPLIST_FILE = DevLogWidget/Resource/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = DevLogWidget; INFOPLIST_KEY_NSHumanReadableCopyright = ""; IPHONEOS_DEPLOYMENT_TARGET = 26.4; @@ -465,12 +466,13 @@ buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CODE_SIGN_ENTITLEMENTS = DevLogWidget/Resource/DevLogWidget.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 4CPC6N38WA; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = DevLogWidget/Info.plist; + INFOPLIST_FILE = DevLogWidget/Resource/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = DevLogWidget; INFOPLIST_KEY_NSHumanReadableCopyright = ""; IPHONEOS_DEPLOYMENT_TARGET = 26.4; diff --git a/DevLogWidget/Assets.xcassets/AccentColor.colorset/Contents.json b/DevLogWidget/Resource/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from DevLogWidget/Assets.xcassets/AccentColor.colorset/Contents.json rename to DevLogWidget/Resource/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/DevLogWidget/Assets.xcassets/AppIcon.appiconset/Contents.json b/DevLogWidget/Resource/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from DevLogWidget/Assets.xcassets/AppIcon.appiconset/Contents.json rename to DevLogWidget/Resource/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/DevLogWidget/Assets.xcassets/Contents.json b/DevLogWidget/Resource/Assets.xcassets/Contents.json similarity index 100% rename from DevLogWidget/Assets.xcassets/Contents.json rename to DevLogWidget/Resource/Assets.xcassets/Contents.json diff --git a/DevLogWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json b/DevLogWidget/Resource/Assets.xcassets/WidgetBackground.colorset/Contents.json similarity index 100% rename from DevLogWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json rename to DevLogWidget/Resource/Assets.xcassets/WidgetBackground.colorset/Contents.json diff --git a/DevLogWidget/Resource/DevLogWidget.entitlements b/DevLogWidget/Resource/DevLogWidget.entitlements new file mode 100644 index 00000000..2cc6f897 --- /dev/null +++ b/DevLogWidget/Resource/DevLogWidget.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.opfic.DevLog + + + diff --git a/DevLogWidget/Info.plist b/DevLogWidget/Resource/Info.plist similarity index 100% rename from DevLogWidget/Info.plist rename to DevLogWidget/Resource/Info.plist From 1b4441fe64886242c77226d4e53afe1ea50764ac Mon Sep 17 00:00:00 2001 From: opficdev Date: Fri, 17 Apr 2026 10:32:43 +0900 Subject: [PATCH 03/17] =?UTF-8?q?chore:=20=EC=9C=84=EC=A0=AF=20=EB=B2=84?= =?UTF-8?q?=EC=A0=80=EB=8B=9D=20=EC=84=A4=EC=A0=95=EC=9D=84=20iOS=20?= =?UTF-8?q?=EB=B2=84=EC=A0=80=EB=8B=9D=EA=B3=BC=20=EB=8F=99=EC=9D=BC?= =?UTF-8?q?=ED=99=94=20=EB=B0=8F=20=EA=B4=80=EB=A0=A8=20=EB=A6=AC=EC=86=8C?= =?UTF-8?q?=EC=8A=A4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog.xcodeproj/project.pbxproj | 12 ++++++------ DevLog/Resource/DevLog.entitlements | 4 ++++ DevLog/Resource/Info.plist | 16 ++++++++-------- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/DevLog.xcodeproj/project.pbxproj b/DevLog.xcodeproj/project.pbxproj index 33d9d80d..8b9b7299 100644 --- a/DevLog.xcodeproj/project.pbxproj +++ b/DevLog.xcodeproj/project.pbxproj @@ -387,7 +387,7 @@ DEVELOPMENT_TEAM = 4CPC6N38WA; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 17; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.0.1; PRODUCT_BUNDLE_IDENTIFIER = opfic.DevLog_Unit; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = NO; @@ -412,7 +412,7 @@ DEVELOPMENT_TEAM = 4CPC6N38WA; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 17; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.0.1; PRODUCT_BUNDLE_IDENTIFIER = opfic.DevLog_Unit; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = NO; @@ -442,13 +442,13 @@ INFOPLIST_FILE = DevLogWidget/Resource/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = DevLogWidget; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 26.4; + IPHONEOS_DEPLOYMENT_TARGET = 17; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.0.1; PRODUCT_BUNDLE_IDENTIFIER = opfic.DevLog.DevLogWidget; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -475,13 +475,13 @@ INFOPLIST_FILE = DevLogWidget/Resource/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = DevLogWidget; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 26.4; + IPHONEOS_DEPLOYMENT_TARGET = 17; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.0.1; PRODUCT_BUNDLE_IDENTIFIER = opfic.DevLog.DevLogWidget; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; diff --git a/DevLog/Resource/DevLog.entitlements b/DevLog/Resource/DevLog.entitlements index 36fd0193..7049d86a 100644 --- a/DevLog/Resource/DevLog.entitlements +++ b/DevLog/Resource/DevLog.entitlements @@ -8,5 +8,9 @@ Default + com.apple.security.application-groups + + group.opfic.DevLog + diff --git a/DevLog/Resource/Info.plist b/DevLog/Resource/Info.plist index 8bdf699c..13e3de1b 100644 --- a/DevLog/Resource/Info.plist +++ b/DevLog/Resource/Info.plist @@ -2,6 +2,10 @@ + APPSTORE_URL + $(APPSTORE_URL) + APP_REDIRECT_URL + $(APP_REDIRECT_URL) CFBundleURLTypes @@ -14,18 +18,14 @@ - GIDClientID - $(CLIENT_ID) - PRIVACY_POLICY_URL - $(PRIVACY_POLICY_URL) FirebaseAppDelegateProxyEnabled + GIDClientID + $(CLIENT_ID) GITHUB_CLIENT_ID $(GITHUB_CLIENT_ID) - APPSTORE_URL - $(APPSTORE_URL) - APP_REDIRECT_URL - $(APP_REDIRECT_URL) + PRIVACY_POLICY_URL + $(PRIVACY_POLICY_URL) UIBackgroundModes remote-notification From ca284471c9a0b39a552351c18d7cc3be621cb23e Mon Sep 17 00:00:00 2001 From: opficdev Date: Fri, 17 Apr 2026 10:39:50 +0900 Subject: [PATCH 04/17] =?UTF-8?q?refactor:=20AppIntent=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20=EC=9C=84=EC=A0=AF=20=EA=B5=AC=EC=A1=B0=EB=A1=9C=20?= =?UTF-8?q?=EC=9E=AC=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/App/Widget/WidgetAppGroup.swift | 12 +++ DevLogWidget/DevLogWidget.swift | 84 ------------------- DevLogWidget/DevLogWidgetBundle.swift | 3 +- ...fileHeatmapWidgetConfigurationIntent.swift | 14 ++++ .../TodayTodoWidgetConfigurationIntent.swift | 14 ++++ .../Model/ProfileHeatmapWidgetEntry.swift | 12 +++ DevLogWidget/Model/TodayTodoWidgetEntry.swift | 12 +++ .../ProfileHeatmapWidgetProvider.swift | 34 ++++++++ .../Provider/TodayTodoWidgetProvider.swift | 34 ++++++++ DevLogWidget/Store/WidgetAppGroup.swift | 16 ++++ .../View/ProfileHeatmapWidgetEntryView.swift | 35 ++++++++ .../View/TodayTodoWidgetEntryView.swift | 38 +++++++++ DevLogWidget/View/WidgetPlaceholderCard.swift | 28 +++++++ .../Widget/ProfileHeatmapWidget.swift | 28 +++++++ DevLogWidget/Widget/TodayTodoWidget.swift | 28 +++++++ 15 files changed, 307 insertions(+), 85 deletions(-) create mode 100644 DevLog/App/Widget/WidgetAppGroup.swift delete mode 100644 DevLogWidget/DevLogWidget.swift create mode 100644 DevLogWidget/Intent/ProfileHeatmapWidgetConfigurationIntent.swift create mode 100644 DevLogWidget/Intent/TodayTodoWidgetConfigurationIntent.swift create mode 100644 DevLogWidget/Model/ProfileHeatmapWidgetEntry.swift create mode 100644 DevLogWidget/Model/TodayTodoWidgetEntry.swift create mode 100644 DevLogWidget/Provider/ProfileHeatmapWidgetProvider.swift create mode 100644 DevLogWidget/Provider/TodayTodoWidgetProvider.swift create mode 100644 DevLogWidget/Store/WidgetAppGroup.swift create mode 100644 DevLogWidget/View/ProfileHeatmapWidgetEntryView.swift create mode 100644 DevLogWidget/View/TodayTodoWidgetEntryView.swift create mode 100644 DevLogWidget/View/WidgetPlaceholderCard.swift create mode 100644 DevLogWidget/Widget/ProfileHeatmapWidget.swift create mode 100644 DevLogWidget/Widget/TodayTodoWidget.swift diff --git a/DevLog/App/Widget/WidgetAppGroup.swift b/DevLog/App/Widget/WidgetAppGroup.swift new file mode 100644 index 00000000..6625334c --- /dev/null +++ b/DevLog/App/Widget/WidgetAppGroup.swift @@ -0,0 +1,12 @@ +// +// WidgetAppGroup.swift +// DevLog +// +// Created by opfic on 4/15/26. +// + +import Foundation + +enum WidgetAppGroup { + static let identifier = "group.opfic.DevLog" +} diff --git a/DevLogWidget/DevLogWidget.swift b/DevLogWidget/DevLogWidget.swift deleted file mode 100644 index 6bac91e6..00000000 --- a/DevLogWidget/DevLogWidget.swift +++ /dev/null @@ -1,84 +0,0 @@ -// -// DevLogWidget.swift -// DevLogWidget -// -// Created by ์ตœ์œค์ง„ on 4/14/26. -// - -import WidgetKit -import SwiftUI - -struct Provider: TimelineProvider { - func placeholder(in context: Context) -> SimpleEntry { - SimpleEntry(date: Date(), emoji: "๐Ÿ˜€") - } - - func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) { - let entry = SimpleEntry(date: Date(), emoji: "๐Ÿ˜€") - completion(entry) - } - - func getTimeline(in context: Context, completion: @escaping (Timeline) -> ()) { - var entries: [SimpleEntry] = [] - - // Generate a timeline consisting of five entries an hour apart, starting from the current date. - let currentDate = Date() - for hourOffset in 0 ..< 5 { - let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)! - let entry = SimpleEntry(date: entryDate, emoji: "๐Ÿ˜€") - entries.append(entry) - } - - let timeline = Timeline(entries: entries, policy: .atEnd) - completion(timeline) - } - -// func relevances() async -> WidgetRelevances { -// // Generate a list containing the contexts this widget is relevant in. -// } -} - -struct SimpleEntry: TimelineEntry { - let date: Date - let emoji: String -} - -struct DevLogWidgetEntryView : View { - var entry: Provider.Entry - - var body: some View { - VStack { - Text("Time:") - Text(entry.date, style: .time) - - Text("Emoji:") - Text(entry.emoji) - } - } -} - -struct DevLogWidget: Widget { - let kind: String = "DevLogWidget" - - var body: some WidgetConfiguration { - StaticConfiguration(kind: kind, provider: Provider()) { entry in - if #available(iOS 17.0, *) { - DevLogWidgetEntryView(entry: entry) - .containerBackground(.fill.tertiary, for: .widget) - } else { - DevLogWidgetEntryView(entry: entry) - .padding() - .background() - } - } - .configurationDisplayName("My Widget") - .description("This is an example widget.") - } -} - -#Preview(as: .systemSmall) { - DevLogWidget() -} timeline: { - SimpleEntry(date: .now, emoji: "๐Ÿ˜€") - SimpleEntry(date: .now, emoji: "๐Ÿคฉ") -} diff --git a/DevLogWidget/DevLogWidgetBundle.swift b/DevLogWidget/DevLogWidgetBundle.swift index 55035a17..ee057801 100644 --- a/DevLogWidget/DevLogWidgetBundle.swift +++ b/DevLogWidget/DevLogWidgetBundle.swift @@ -11,6 +11,7 @@ import SwiftUI @main struct DevLogWidgetBundle: WidgetBundle { var body: some Widget { - DevLogWidget() + TodayTodoWidget() + ProfileHeatmapWidget() } } diff --git a/DevLogWidget/Intent/ProfileHeatmapWidgetConfigurationIntent.swift b/DevLogWidget/Intent/ProfileHeatmapWidgetConfigurationIntent.swift new file mode 100644 index 00000000..5b3f313b --- /dev/null +++ b/DevLogWidget/Intent/ProfileHeatmapWidgetConfigurationIntent.swift @@ -0,0 +1,14 @@ +// +// ProfileHeatmapWidgetConfigurationIntent.swift +// DevLogWidget +// +// Created by opfic on 4/15/26. +// + +import AppIntents +import WidgetKit + +struct ProfileHeatmapWidgetConfigurationIntent: WidgetConfigurationIntent { + static var title: LocalizedStringResource = "Profile Heatmap" + static var description = IntentDescription("์ด๋ฒˆ ๋‹ฌ ํ™œ๋™ ํžˆํŠธ๋งต์„ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.") +} diff --git a/DevLogWidget/Intent/TodayTodoWidgetConfigurationIntent.swift b/DevLogWidget/Intent/TodayTodoWidgetConfigurationIntent.swift new file mode 100644 index 00000000..3320cfba --- /dev/null +++ b/DevLogWidget/Intent/TodayTodoWidgetConfigurationIntent.swift @@ -0,0 +1,14 @@ +// +// TodayTodoWidgetConfigurationIntent.swift +// DevLogWidget +// +// Created by opfic on 4/15/26. +// + +import AppIntents +import WidgetKit + +struct TodayTodoWidgetConfigurationIntent: WidgetConfigurationIntent { + static var title: LocalizedStringResource = "Today Todo" + static var description = IntentDescription("์˜ค๋Š˜ ๊ธฐ์ค€ Todo ๋ชฉ๋ก์„ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.") +} diff --git a/DevLogWidget/Model/ProfileHeatmapWidgetEntry.swift b/DevLogWidget/Model/ProfileHeatmapWidgetEntry.swift new file mode 100644 index 00000000..e415c76e --- /dev/null +++ b/DevLogWidget/Model/ProfileHeatmapWidgetEntry.swift @@ -0,0 +1,12 @@ +// +// ProfileHeatmapWidgetEntry.swift +// DevLogWidget +// +// Created by opfic on 4/15/26. +// + +import WidgetKit + +struct ProfileHeatmapWidgetEntry: TimelineEntry { + let date: Date +} diff --git a/DevLogWidget/Model/TodayTodoWidgetEntry.swift b/DevLogWidget/Model/TodayTodoWidgetEntry.swift new file mode 100644 index 00000000..238e6ff1 --- /dev/null +++ b/DevLogWidget/Model/TodayTodoWidgetEntry.swift @@ -0,0 +1,12 @@ +// +// TodayTodoWidgetEntry.swift +// DevLogWidget +// +// Created by opfic on 4/15/26. +// + +import WidgetKit + +struct TodayTodoWidgetEntry: TimelineEntry { + let date: Date +} diff --git a/DevLogWidget/Provider/ProfileHeatmapWidgetProvider.swift b/DevLogWidget/Provider/ProfileHeatmapWidgetProvider.swift new file mode 100644 index 00000000..87175f93 --- /dev/null +++ b/DevLogWidget/Provider/ProfileHeatmapWidgetProvider.swift @@ -0,0 +1,34 @@ +// +// ProfileHeatmapWidgetProvider.swift +// DevLogWidget +// +// Created by opfic on 4/15/26. +// + +import AppIntents +import WidgetKit + +struct ProfileHeatmapWidgetProvider: AppIntentTimelineProvider { + typealias Intent = ProfileHeatmapWidgetConfigurationIntent + + func placeholder(in context: Context) -> ProfileHeatmapWidgetEntry { + .init(date: .now) + } + + func snapshot( + for configuration: ProfileHeatmapWidgetConfigurationIntent, + in context: Context + ) async -> ProfileHeatmapWidgetEntry { + .init(date: .now) + } + + func timeline( + for configuration: ProfileHeatmapWidgetConfigurationIntent, + in context: Context + ) async -> Timeline { + Timeline( + entries: [.init(date: .now)], + policy: .never + ) + } +} diff --git a/DevLogWidget/Provider/TodayTodoWidgetProvider.swift b/DevLogWidget/Provider/TodayTodoWidgetProvider.swift new file mode 100644 index 00000000..9ca3c5c8 --- /dev/null +++ b/DevLogWidget/Provider/TodayTodoWidgetProvider.swift @@ -0,0 +1,34 @@ +// +// TodayTodoWidgetProvider.swift +// DevLogWidget +// +// Created by opfic on 4/15/26. +// + +import AppIntents +import WidgetKit + +struct TodayTodoWidgetProvider: AppIntentTimelineProvider { + typealias Intent = TodayTodoWidgetConfigurationIntent + + func placeholder(in context: Context) -> TodayTodoWidgetEntry { + .init(date: .now) + } + + func snapshot( + for configuration: TodayTodoWidgetConfigurationIntent, + in context: Context + ) async -> TodayTodoWidgetEntry { + .init(date: .now) + } + + func timeline( + for configuration: TodayTodoWidgetConfigurationIntent, + in context: Context + ) async -> Timeline { + Timeline( + entries: [.init(date: .now)], + policy: .never + ) + } +} diff --git a/DevLogWidget/Store/WidgetAppGroup.swift b/DevLogWidget/Store/WidgetAppGroup.swift new file mode 100644 index 00000000..1ecc0859 --- /dev/null +++ b/DevLogWidget/Store/WidgetAppGroup.swift @@ -0,0 +1,16 @@ +// +// WidgetAppGroup.swift +// DevLogWidget +// +// Created by opfic on 4/15/26. +// + +import Foundation + +enum WidgetAppGroup { + static let identifier = "group.opfic.DevLog" +} + +enum WidgetSharedUserDefaults { + static let shared = UserDefaults(suiteName: WidgetAppGroup.identifier) +} diff --git a/DevLogWidget/View/ProfileHeatmapWidgetEntryView.swift b/DevLogWidget/View/ProfileHeatmapWidgetEntryView.swift new file mode 100644 index 00000000..72dafb45 --- /dev/null +++ b/DevLogWidget/View/ProfileHeatmapWidgetEntryView.swift @@ -0,0 +1,35 @@ +// +// ProfileHeatmapWidgetEntryView.swift +// DevLogWidget +// +// Created by opfic on 4/15/26. +// + +import SwiftUI +import WidgetKit + +struct ProfileHeatmapWidgetEntryView: View { + @Environment(\.widgetFamily) private var widgetFamily + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text("์ด๋ฒˆ ๋‹ฌ ํžˆํŠธ๋งต") + .font(.headline) + + Spacer() + + if widgetFamily == .systemSmall { + Text("์•ฑ์„ ์—ด์–ด\nํžˆํŠธ๋งต์„ ์ค€๋น„ํ•˜์„ธ์š”") + .font(.caption) + .foregroundStyle(.secondary) + } else { + WidgetPlaceholderCard( + title: "์ด๋ฒˆ ๋‹ฌ ํžˆํŠธ๋งต", + message: "๋ฐ์ดํ„ฐ ์—ฐ๊ฒฐ ์ „" + ) + .frame(maxWidth: .infinity) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + } +} diff --git a/DevLogWidget/View/TodayTodoWidgetEntryView.swift b/DevLogWidget/View/TodayTodoWidgetEntryView.swift new file mode 100644 index 00000000..62ee15ad --- /dev/null +++ b/DevLogWidget/View/TodayTodoWidgetEntryView.swift @@ -0,0 +1,38 @@ +// +// TodayTodoWidgetEntryView.swift +// DevLogWidget +// +// Created by opfic on 4/15/26. +// + +import SwiftUI +import WidgetKit + +struct TodayTodoWidgetEntryView: View { + @Environment(\.widgetFamily) private var widgetFamily + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text("Today Todo") + .font(.headline) + + Spacer() + + switch widgetFamily { + case .systemSmall: + Text("์•ฑ์„ ์—ด์–ด\nToday ์œ„์ ฏ์„ ์ค€๋น„ํ•˜์„ธ์š”") + .font(.caption) + .foregroundStyle(.secondary) + case .systemMedium, .systemLarge: + WidgetPlaceholderCard( + title: "Today Todo", + message: "๋ฐ์ดํ„ฐ ์—ฐ๊ฒฐ ์ „" + ) + .frame(maxWidth: .infinity) + default: + EmptyView() + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + } +} diff --git a/DevLogWidget/View/WidgetPlaceholderCard.swift b/DevLogWidget/View/WidgetPlaceholderCard.swift new file mode 100644 index 00000000..18ffa723 --- /dev/null +++ b/DevLogWidget/View/WidgetPlaceholderCard.swift @@ -0,0 +1,28 @@ +// +// WidgetPlaceholderCard.swift +// DevLogWidget +// +// Created by opfic on 4/15/26. +// + +import SwiftUI + +struct WidgetPlaceholderCard: View { + let title: String + let message: String + + var body: some View { + RoundedRectangle(cornerRadius: 8) + .fill(Color.secondary.opacity(0.12)) + .overlay { + Text(message) + .font(.caption) + .foregroundStyle(.secondary) + } + .overlay(alignment: .topLeading) { + Text(title) + .font(.headline) + .padding(12) + } + } +} diff --git a/DevLogWidget/Widget/ProfileHeatmapWidget.swift b/DevLogWidget/Widget/ProfileHeatmapWidget.swift new file mode 100644 index 00000000..a413669f --- /dev/null +++ b/DevLogWidget/Widget/ProfileHeatmapWidget.swift @@ -0,0 +1,28 @@ +// +// ProfileHeatmapWidget.swift +// DevLogWidget +// +// Created by opfic on 4/15/26. +// + +import SwiftUI +import AppIntents +import WidgetKit + +struct ProfileHeatmapWidget: Widget { + let kind = "ProfileHeatmapWidget" + + var body: some WidgetConfiguration { + AppIntentConfiguration( + kind: kind, + intent: ProfileHeatmapWidgetConfigurationIntent.self, + provider: ProfileHeatmapWidgetProvider() + ) { _ in + ProfileHeatmapWidgetEntryView() + .containerBackground(.fill.tertiary, for: .widget) + } + .configurationDisplayName("Profile Heatmap") + .description("์ด๋ฒˆ ๋‹ฌ ํ™œ๋™ ํžˆํŠธ๋งต์„ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.") + .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) + } +} diff --git a/DevLogWidget/Widget/TodayTodoWidget.swift b/DevLogWidget/Widget/TodayTodoWidget.swift new file mode 100644 index 00000000..d74bcb92 --- /dev/null +++ b/DevLogWidget/Widget/TodayTodoWidget.swift @@ -0,0 +1,28 @@ +// +// TodayTodoWidget.swift +// DevLogWidget +// +// Created by opfic on 4/15/26. +// + +import SwiftUI +import AppIntents +import WidgetKit + +struct TodayTodoWidget: Widget { + let kind = "TodayTodoWidget" + + var body: some WidgetConfiguration { + AppIntentConfiguration( + kind: kind, + intent: TodayTodoWidgetConfigurationIntent.self, + provider: TodayTodoWidgetProvider() + ) { _ in + TodayTodoWidgetEntryView() + .containerBackground(.fill.tertiary, for: .widget) + } + .configurationDisplayName("Today Todo") + .description("์˜ค๋Š˜ ๊ธฐ์ค€ Todo ๋ชฉ๋ก์„ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.") + .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) + } +} From 33126bbf7b60c2942ac93442af63bd1c6921f248 Mon Sep 17 00:00:00 2001 From: opficdev Date: Fri, 17 Apr 2026 15:06:57 +0900 Subject: [PATCH 05/17] =?UTF-8?q?chore:=20=EC=9C=84=EC=A0=AF=20=EC=A7=80?= =?UTF-8?q?=EC=9B=90=20=ED=8C=8C=EC=9D=BC=20=EC=9C=84=EC=B9=98=20=EC=9E=AC?= =?UTF-8?q?=EB=B0=B0=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Widget/ProfileHeatmapWidgetSnapshot.swift | 29 +++++++++++++ DevLog/Widget/TodayWidgetSnapshot.swift | 30 +++++++++++++ DevLog/{App => }/Widget/WidgetAppGroup.swift | 0 DevLog/Widget/WidgetSharedDefaultsStore.swift | 24 +++++++++++ DevLog/Widget/WidgetSnapshotStore.swift | 43 +++++++++++++++++++ 5 files changed, 126 insertions(+) create mode 100644 DevLog/Widget/ProfileHeatmapWidgetSnapshot.swift create mode 100644 DevLog/Widget/TodayWidgetSnapshot.swift rename DevLog/{App => }/Widget/WidgetAppGroup.swift (100%) create mode 100644 DevLog/Widget/WidgetSharedDefaultsStore.swift create mode 100644 DevLog/Widget/WidgetSnapshotStore.swift diff --git a/DevLog/Widget/ProfileHeatmapWidgetSnapshot.swift b/DevLog/Widget/ProfileHeatmapWidgetSnapshot.swift new file mode 100644 index 00000000..ad4d4385 --- /dev/null +++ b/DevLog/Widget/ProfileHeatmapWidgetSnapshot.swift @@ -0,0 +1,29 @@ +// +// ProfileHeatmapWidgetSnapshot.swift +// DevLog +// +// Created by opfic on 4/17/26. +// + +import Foundation + +struct ProfileHeatmapWidgetSnapshot: Codable, Equatable { + let generatedAt: Date + let monthStart: Date + let selectedActivityKindRawValues: [String] + let maxCount: Int + let weeks: [WidgetHeatmapWeekSnapshot] +} + +struct WidgetHeatmapWeekSnapshot: Codable, Equatable { + let id: Int + let days: [WidgetHeatmapDaySnapshot] +} + +struct WidgetHeatmapDaySnapshot: Codable, Equatable { + let date: Date + let createdCount: Int + let completedCount: Int + let deletedCount: Int + let isVisible: Bool +} diff --git a/DevLog/Widget/TodayWidgetSnapshot.swift b/DevLog/Widget/TodayWidgetSnapshot.swift new file mode 100644 index 00000000..65f3504b --- /dev/null +++ b/DevLog/Widget/TodayWidgetSnapshot.swift @@ -0,0 +1,30 @@ +// +// TodayWidgetSnapshot.swift +// DevLog +// +// Created by opfic on 4/17/26. +// + +import Foundation + +struct TodayWidgetSnapshot: Codable, Equatable { + let generatedAt: Date + let totalCount: Int + let focusedCount: Int + let overdueCount: Int + let dueSoonCount: Int + let sections: [TodayWidgetSectionSnapshot] +} + +struct TodayWidgetSectionSnapshot: Codable, Equatable { + let category: String + let items: [WidgetTodoSnapshotItem] +} + +struct WidgetTodoSnapshotItem: Codable, Equatable { + let id: String + let number: Int + let title: String + let isPinned: Bool + let dueDate: Date? +} diff --git a/DevLog/App/Widget/WidgetAppGroup.swift b/DevLog/Widget/WidgetAppGroup.swift similarity index 100% rename from DevLog/App/Widget/WidgetAppGroup.swift rename to DevLog/Widget/WidgetAppGroup.swift diff --git a/DevLog/Widget/WidgetSharedDefaultsStore.swift b/DevLog/Widget/WidgetSharedDefaultsStore.swift new file mode 100644 index 00000000..eedc63ac --- /dev/null +++ b/DevLog/Widget/WidgetSharedDefaultsStore.swift @@ -0,0 +1,24 @@ +// +// WidgetSharedDefaultsStore.swift +// DevLog +// +// Created by opfic on 4/17/26. +// + +import Foundation + +final class WidgetSharedDefaultsStore { + private let userDefaults: UserDefaults + + init(userDefaults: UserDefaults = UserDefaults(suiteName: WidgetAppGroup.identifier) ?? .standard) { + self.userDefaults = userDefaults + } + + func data(forKey key: String) -> Data? { + userDefaults.data(forKey: key) + } + + func setData(_ value: Data?, forKey key: String) { + userDefaults.set(value, forKey: key) + } +} diff --git a/DevLog/Widget/WidgetSnapshotStore.swift b/DevLog/Widget/WidgetSnapshotStore.swift new file mode 100644 index 00000000..13fa6098 --- /dev/null +++ b/DevLog/Widget/WidgetSnapshotStore.swift @@ -0,0 +1,43 @@ +// +// WidgetSnapshotStore.swift +// DevLog +// +// Created by opfic on 4/17/26. +// + +import Foundation + +final class WidgetSnapshotStore { + private enum Key { + static let todaySnapshot = "Widget.today.snapshot" + static let profileHeatmapSnapshot = "Widget.profileHeatmap.snapshot" + } + + private let store: WidgetSharedDefaultsStore + private let encoder = JSONEncoder() + private let decoder = JSONDecoder() + + init(store: WidgetSharedDefaultsStore = .init()) { + self.store = store + } + + func saveTodaySnapshot(_ snapshot: TodayWidgetSnapshot) throws { + let data = try encoder.encode(snapshot) + store.setData(data, forKey: Key.todaySnapshot) + } + + func loadTodaySnapshot() throws -> TodayWidgetSnapshot? { + guard let data = store.data(forKey: Key.todaySnapshot) else { return nil } + return try decoder.decode(TodayWidgetSnapshot.self, from: data) + } + + func saveProfileHeatmapSnapshot(_ snapshot: ProfileHeatmapWidgetSnapshot) throws { + let data = try encoder.encode(snapshot) + store.setData(data, forKey: Key.profileHeatmapSnapshot) + } + + func loadProfileHeatmapSnapshot() throws -> ProfileHeatmapWidgetSnapshot? { + guard let data = store.data(forKey: Key.profileHeatmapSnapshot) else { return nil } + return try decoder.decode(ProfileHeatmapWidgetSnapshot.self, from: data) + } +} From 2f65b917906997f8f5508e51de3b1602152a1255 Mon Sep 17 00:00:00 2001 From: opficdev Date: Fri, 17 Apr 2026 15:24:13 +0900 Subject: [PATCH 06/17] =?UTF-8?q?chore:=20=EC=9C=84=EC=A0=AF=20=ED=9E=88?= =?UTF-8?q?=ED=8A=B8=EB=A7=B5=20=ED=83=80=EC=9E=85=20Profile=20=EC=A0=91?= =?UTF-8?q?=EB=91=90=EC=96=B4=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLogWidget/DevLogWidgetBundle.swift | 2 +- ...=> HeatmapWidgetConfigurationIntent.swift} | 6 +- DevLogWidget/Model/HeatmapWidgetEntry.swift | 13 ++++ .../Model/HeatmapWidgetSnapshot.swift | 29 +++++++++ .../Model/ProfileHeatmapWidgetEntry.swift | 12 ---- DevLogWidget/Model/TodayWidgetSnapshot.swift | 30 +++++++++ .../Provider/HeatmapWidgetProvider.swift | 52 +++++++++++++++ .../ProfileHeatmapWidgetProvider.swift | 34 ---------- .../Store/WidgetSharedDefaultsStore.swift | 20 ++++++ DevLogWidget/Store/WidgetSnapshotStore.swift | 32 +++++++++ .../View/HeatmapWidgetEntryView.swift | 65 +++++++++++++++++++ .../View/ProfileHeatmapWidgetEntryView.swift | 35 ---------- ...eatmapWidget.swift => HeatmapWidget.swift} | 16 ++--- 13 files changed, 253 insertions(+), 93 deletions(-) rename DevLogWidget/Intent/{ProfileHeatmapWidgetConfigurationIntent.swift => HeatmapWidgetConfigurationIntent.swift} (50%) create mode 100644 DevLogWidget/Model/HeatmapWidgetEntry.swift create mode 100644 DevLogWidget/Model/HeatmapWidgetSnapshot.swift delete mode 100644 DevLogWidget/Model/ProfileHeatmapWidgetEntry.swift create mode 100644 DevLogWidget/Model/TodayWidgetSnapshot.swift create mode 100644 DevLogWidget/Provider/HeatmapWidgetProvider.swift delete mode 100644 DevLogWidget/Provider/ProfileHeatmapWidgetProvider.swift create mode 100644 DevLogWidget/Store/WidgetSharedDefaultsStore.swift create mode 100644 DevLogWidget/Store/WidgetSnapshotStore.swift create mode 100644 DevLogWidget/View/HeatmapWidgetEntryView.swift delete mode 100644 DevLogWidget/View/ProfileHeatmapWidgetEntryView.swift rename DevLogWidget/Widget/{ProfileHeatmapWidget.swift => HeatmapWidget.swift} (56%) diff --git a/DevLogWidget/DevLogWidgetBundle.swift b/DevLogWidget/DevLogWidgetBundle.swift index ee057801..9af82ac2 100644 --- a/DevLogWidget/DevLogWidgetBundle.swift +++ b/DevLogWidget/DevLogWidgetBundle.swift @@ -12,6 +12,6 @@ import SwiftUI struct DevLogWidgetBundle: WidgetBundle { var body: some Widget { TodayTodoWidget() - ProfileHeatmapWidget() + HeatmapWidget() } } diff --git a/DevLogWidget/Intent/ProfileHeatmapWidgetConfigurationIntent.swift b/DevLogWidget/Intent/HeatmapWidgetConfigurationIntent.swift similarity index 50% rename from DevLogWidget/Intent/ProfileHeatmapWidgetConfigurationIntent.swift rename to DevLogWidget/Intent/HeatmapWidgetConfigurationIntent.swift index 5b3f313b..d6f3761f 100644 --- a/DevLogWidget/Intent/ProfileHeatmapWidgetConfigurationIntent.swift +++ b/DevLogWidget/Intent/HeatmapWidgetConfigurationIntent.swift @@ -1,5 +1,5 @@ // -// ProfileHeatmapWidgetConfigurationIntent.swift +// HeatmapWidgetConfigurationIntent.swift // DevLogWidget // // Created by opfic on 4/15/26. @@ -8,7 +8,7 @@ import AppIntents import WidgetKit -struct ProfileHeatmapWidgetConfigurationIntent: WidgetConfigurationIntent { - static var title: LocalizedStringResource = "Profile Heatmap" +struct HeatmapWidgetConfigurationIntent: WidgetConfigurationIntent { + static var title: LocalizedStringResource = "Heatmap" static var description = IntentDescription("์ด๋ฒˆ ๋‹ฌ ํ™œ๋™ ํžˆํŠธ๋งต์„ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.") } diff --git a/DevLogWidget/Model/HeatmapWidgetEntry.swift b/DevLogWidget/Model/HeatmapWidgetEntry.swift new file mode 100644 index 00000000..035f62f7 --- /dev/null +++ b/DevLogWidget/Model/HeatmapWidgetEntry.swift @@ -0,0 +1,13 @@ +// +// HeatmapWidgetEntry.swift +// DevLogWidget +// +// Created by opfic on 4/15/26. +// + +import WidgetKit + +struct HeatmapWidgetEntry: TimelineEntry { + let date: Date + let snapshot: HeatmapWidgetSnapshot? +} diff --git a/DevLogWidget/Model/HeatmapWidgetSnapshot.swift b/DevLogWidget/Model/HeatmapWidgetSnapshot.swift new file mode 100644 index 00000000..19429451 --- /dev/null +++ b/DevLogWidget/Model/HeatmapWidgetSnapshot.swift @@ -0,0 +1,29 @@ +// +// HeatmapWidgetSnapshot.swift +// DevLogWidget +// +// Created by opfic on 4/17/26. +// + +import Foundation + +struct HeatmapWidgetSnapshot: Decodable, Equatable { + let generatedAt: Date + let monthStart: Date + let selectedActivityKindRawValues: [String] + let maxCount: Int + let weeks: [WidgetHeatmapWeekSnapshot] +} + +struct WidgetHeatmapWeekSnapshot: Decodable, Equatable { + let id: Int + let days: [WidgetHeatmapDaySnapshot] +} + +struct WidgetHeatmapDaySnapshot: Decodable, Equatable { + let date: Date + let createdCount: Int + let completedCount: Int + let deletedCount: Int + let isVisible: Bool +} diff --git a/DevLogWidget/Model/ProfileHeatmapWidgetEntry.swift b/DevLogWidget/Model/ProfileHeatmapWidgetEntry.swift deleted file mode 100644 index e415c76e..00000000 --- a/DevLogWidget/Model/ProfileHeatmapWidgetEntry.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// ProfileHeatmapWidgetEntry.swift -// DevLogWidget -// -// Created by opfic on 4/15/26. -// - -import WidgetKit - -struct ProfileHeatmapWidgetEntry: TimelineEntry { - let date: Date -} diff --git a/DevLogWidget/Model/TodayWidgetSnapshot.swift b/DevLogWidget/Model/TodayWidgetSnapshot.swift new file mode 100644 index 00000000..cf1eb55b --- /dev/null +++ b/DevLogWidget/Model/TodayWidgetSnapshot.swift @@ -0,0 +1,30 @@ +// +// TodayWidgetSnapshot.swift +// DevLogWidget +// +// Created by opfic on 4/17/26. +// + +import Foundation + +struct TodayWidgetSnapshot: Decodable, Equatable { + let generatedAt: Date + let totalCount: Int + let focusedCount: Int + let overdueCount: Int + let dueSoonCount: Int + let sections: [TodayWidgetSectionSnapshot] +} + +struct TodayWidgetSectionSnapshot: Decodable, Equatable { + let category: String + let items: [WidgetTodoSnapshotItem] +} + +struct WidgetTodoSnapshotItem: Decodable, Equatable { + let id: String + let number: Int + let title: String + let isPinned: Bool + let dueDate: Date? +} diff --git a/DevLogWidget/Provider/HeatmapWidgetProvider.swift b/DevLogWidget/Provider/HeatmapWidgetProvider.swift new file mode 100644 index 00000000..5c4ab2b2 --- /dev/null +++ b/DevLogWidget/Provider/HeatmapWidgetProvider.swift @@ -0,0 +1,52 @@ +// +// HeatmapWidgetProvider.swift +// DevLogWidget +// +// Created by opfic on 4/15/26. +// + +import AppIntents +import WidgetKit + +struct HeatmapWidgetProvider: AppIntentTimelineProvider { + typealias Intent = HeatmapWidgetConfigurationIntent + typealias Entry = HeatmapWidgetEntry + private let store = WidgetSnapshotStore() + + // ์œ„์ ฏ ๊ฐค๋Ÿฌ๋ฆฌ๋‚˜ ๋กœ๋”ฉ ์ „ ์ƒํƒœ์—์„œ ์ฆ‰์‹œ ํ‘œ์‹œํ•  ๊ธฐ๋ณธ ์—”ํŠธ๋ฆฌ. + func placeholder(in context: Context) -> HeatmapWidgetEntry { + .init(date: .now, snapshot: nil) + } + + // ํ˜„์žฌ ์‹œ์ ์˜ ๋‹จ์ผ ์Šค๋ƒ…์ƒท์„ ๋งŒ๋“ค์–ด ๋ฏธ๋ฆฌ๋ณด๊ธฐ์™€ ์ผ์‹œ์ ์ธ ๋ Œ๋”๋ง์— ์‚ฌ์šฉํ•œ๋‹ค. + func snapshot( + for configuration: HeatmapWidgetConfigurationIntent, + in context: Context + ) async -> HeatmapWidgetEntry { + let snapshot = try? store.loadHeatmapSnapshot() + return .init( + date: snapshot?.generatedAt ?? .now, + snapshot: snapshot + ) + } + + // ์‹ค์ œ ์œ„์ ฏ์ด ์‚ฌ์šฉํ•  ํƒ€์ž„๋ผ์ธ ์—”ํŠธ๋ฆฌ๋ฅผ ๊ตฌ์„ฑํ•œ๋‹ค. + // ํ˜„์žฌ ๋‹จ๊ณ„์—์„œ๋Š” ์ €์žฅ๋œ ์Šค๋ƒ…์ƒท ํ•˜๋‚˜๋งŒ ๋‚ด๋ ค์ฃผ๊ณ , ๊ฐฑ์‹ ์€ ์•ฑ์ด ๋ณ„๋„๋กœ ํŠธ๋ฆฌ๊ฑฐํ•œ๋‹ค. + func timeline( + for configuration: HeatmapWidgetConfigurationIntent, + in context: Context + ) async -> Timeline { + let snapshot = try? store.loadHeatmapSnapshot() + let entries: [HeatmapWidgetEntry] = [ + .init( + date: snapshot?.generatedAt ?? .now, + snapshot: snapshot + ) + ] + + return Timeline( + entries: entries, + policy: .never + ) + } +} diff --git a/DevLogWidget/Provider/ProfileHeatmapWidgetProvider.swift b/DevLogWidget/Provider/ProfileHeatmapWidgetProvider.swift deleted file mode 100644 index 87175f93..00000000 --- a/DevLogWidget/Provider/ProfileHeatmapWidgetProvider.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// ProfileHeatmapWidgetProvider.swift -// DevLogWidget -// -// Created by opfic on 4/15/26. -// - -import AppIntents -import WidgetKit - -struct ProfileHeatmapWidgetProvider: AppIntentTimelineProvider { - typealias Intent = ProfileHeatmapWidgetConfigurationIntent - - func placeholder(in context: Context) -> ProfileHeatmapWidgetEntry { - .init(date: .now) - } - - func snapshot( - for configuration: ProfileHeatmapWidgetConfigurationIntent, - in context: Context - ) async -> ProfileHeatmapWidgetEntry { - .init(date: .now) - } - - func timeline( - for configuration: ProfileHeatmapWidgetConfigurationIntent, - in context: Context - ) async -> Timeline { - Timeline( - entries: [.init(date: .now)], - policy: .never - ) - } -} diff --git a/DevLogWidget/Store/WidgetSharedDefaultsStore.swift b/DevLogWidget/Store/WidgetSharedDefaultsStore.swift new file mode 100644 index 00000000..1ee41a04 --- /dev/null +++ b/DevLogWidget/Store/WidgetSharedDefaultsStore.swift @@ -0,0 +1,20 @@ +// +// WidgetSharedDefaultsStore.swift +// DevLogWidget +// +// Created by opfic on 4/17/26. +// + +import Foundation + +final class WidgetSharedDefaultsStore { + private let userDefaults: UserDefaults + + init(userDefaults: UserDefaults = UserDefaults(suiteName: WidgetAppGroup.identifier) ?? .standard) { + self.userDefaults = userDefaults + } + + func data(forKey key: String) -> Data? { + userDefaults.data(forKey: key) + } +} diff --git a/DevLogWidget/Store/WidgetSnapshotStore.swift b/DevLogWidget/Store/WidgetSnapshotStore.swift new file mode 100644 index 00000000..f116e1be --- /dev/null +++ b/DevLogWidget/Store/WidgetSnapshotStore.swift @@ -0,0 +1,32 @@ +// +// WidgetSnapshotStore.swift +// DevLogWidget +// +// Created by opfic on 4/17/26. +// + +import Foundation + +final class WidgetSnapshotStore { + private enum Key { + static let todaySnapshot = "Widget.today.snapshot" + static let heatmapSnapshot = "Widget.profileHeatmap.snapshot" + } + + private let store: WidgetSharedDefaultsStore + private let decoder = JSONDecoder() + + init(store: WidgetSharedDefaultsStore = .init()) { + self.store = store + } + + func loadTodaySnapshot() throws -> TodayWidgetSnapshot? { + guard let data = store.data(forKey: Key.todaySnapshot) else { return nil } + return try decoder.decode(TodayWidgetSnapshot.self, from: data) + } + + func loadHeatmapSnapshot() throws -> HeatmapWidgetSnapshot? { + guard let data = store.data(forKey: Key.heatmapSnapshot) else { return nil } + return try decoder.decode(HeatmapWidgetSnapshot.self, from: data) + } +} diff --git a/DevLogWidget/View/HeatmapWidgetEntryView.swift b/DevLogWidget/View/HeatmapWidgetEntryView.swift new file mode 100644 index 00000000..502230c6 --- /dev/null +++ b/DevLogWidget/View/HeatmapWidgetEntryView.swift @@ -0,0 +1,65 @@ +// +// HeatmapWidgetEntryView.swift +// DevLogWidget +// +// Created by opfic on 4/15/26. +// + +import SwiftUI +import WidgetKit + +struct HeatmapWidgetEntryView: View { + let entry: HeatmapWidgetEntry + @Environment(\.widgetFamily) private var widgetFamily + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text("์ด๋ฒˆ ๋‹ฌ ํžˆํŠธ๋งต") + .font(.headline) + + Spacer() + + if let snapshot = entry.snapshot { + content(snapshot) + } else { + emptyState + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + } + + @ViewBuilder + private func content(_ snapshot: HeatmapWidgetSnapshot) -> some View { + if widgetFamily == .systemSmall { + VStack(alignment: .leading, spacing: 4) { + Text("\(snapshot.maxCount)") + .font(.title) + .bold() + Text("์ด๋ฒˆ ๋‹ฌ ์ตœ๋Œ€ ํ™œ๋™ ์ˆ˜") + .font(.caption) + .foregroundStyle(.secondary) + } + } else { + WidgetPlaceholderCard( + title: "์ด๋ฒˆ ๋‹ฌ ํžˆํŠธ๋งต", + message: "์ €์žฅ๋œ ์ฃผ์ฐจ \(snapshot.weeks.count)๊ฐœ" + ) + .frame(maxWidth: .infinity) + } + } + + @ViewBuilder + private var emptyState: some View { + if widgetFamily == .systemSmall { + Text("์•ฑ์„ ์—ด์–ด\nํžˆํŠธ๋งต์„ ์ค€๋น„ํ•˜์„ธ์š”") + .font(.caption) + .foregroundStyle(.secondary) + } else { + WidgetPlaceholderCard( + title: "์ด๋ฒˆ ๋‹ฌ ํžˆํŠธ๋งต", + message: "๋ฐ์ดํ„ฐ ์—ฐ๊ฒฐ ์ „" + ) + .frame(maxWidth: .infinity) + } + } +} diff --git a/DevLogWidget/View/ProfileHeatmapWidgetEntryView.swift b/DevLogWidget/View/ProfileHeatmapWidgetEntryView.swift deleted file mode 100644 index 72dafb45..00000000 --- a/DevLogWidget/View/ProfileHeatmapWidgetEntryView.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// ProfileHeatmapWidgetEntryView.swift -// DevLogWidget -// -// Created by opfic on 4/15/26. -// - -import SwiftUI -import WidgetKit - -struct ProfileHeatmapWidgetEntryView: View { - @Environment(\.widgetFamily) private var widgetFamily - - var body: some View { - VStack(alignment: .leading, spacing: 8) { - Text("์ด๋ฒˆ ๋‹ฌ ํžˆํŠธ๋งต") - .font(.headline) - - Spacer() - - if widgetFamily == .systemSmall { - Text("์•ฑ์„ ์—ด์–ด\nํžˆํŠธ๋งต์„ ์ค€๋น„ํ•˜์„ธ์š”") - .font(.caption) - .foregroundStyle(.secondary) - } else { - WidgetPlaceholderCard( - title: "์ด๋ฒˆ ๋‹ฌ ํžˆํŠธ๋งต", - message: "๋ฐ์ดํ„ฐ ์—ฐ๊ฒฐ ์ „" - ) - .frame(maxWidth: .infinity) - } - } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) - } -} diff --git a/DevLogWidget/Widget/ProfileHeatmapWidget.swift b/DevLogWidget/Widget/HeatmapWidget.swift similarity index 56% rename from DevLogWidget/Widget/ProfileHeatmapWidget.swift rename to DevLogWidget/Widget/HeatmapWidget.swift index a413669f..9da8f917 100644 --- a/DevLogWidget/Widget/ProfileHeatmapWidget.swift +++ b/DevLogWidget/Widget/HeatmapWidget.swift @@ -1,5 +1,5 @@ // -// ProfileHeatmapWidget.swift +// HeatmapWidget.swift // DevLogWidget // // Created by opfic on 4/15/26. @@ -9,19 +9,19 @@ import SwiftUI import AppIntents import WidgetKit -struct ProfileHeatmapWidget: Widget { - let kind = "ProfileHeatmapWidget" +struct HeatmapWidget: Widget { + let kind = "HeatmapWidget" var body: some WidgetConfiguration { AppIntentConfiguration( kind: kind, - intent: ProfileHeatmapWidgetConfigurationIntent.self, - provider: ProfileHeatmapWidgetProvider() - ) { _ in - ProfileHeatmapWidgetEntryView() + intent: HeatmapWidgetConfigurationIntent.self, + provider: HeatmapWidgetProvider() + ) { entry in + HeatmapWidgetEntryView(entry: entry) .containerBackground(.fill.tertiary, for: .widget) } - .configurationDisplayName("Profile Heatmap") + .configurationDisplayName("Heatmap") .description("์ด๋ฒˆ ๋‹ฌ ํ™œ๋™ ํžˆํŠธ๋งต์„ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.") .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) } From d7690419fd2e0a31f773353b0ef416d2f098e7b5 Mon Sep 17 00:00:00 2001 From: opficdev Date: Fri, 17 Apr 2026 15:25:29 +0900 Subject: [PATCH 07/17] =?UTF-8?q?feat:=20=ED=88=AC=EB=8D=B0=EC=9D=B4=20?= =?UTF-8?q?=EC=9C=84=EC=A0=AF=20=EC=8A=A4=EB=83=85=EC=83=B7=20=EB=A0=8C?= =?UTF-8?q?=EB=8D=94=EB=A7=81=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLogWidget/Model/TodayTodoWidgetEntry.swift | 1 + .../Provider/TodayTodoWidgetProvider.swift | 26 ++++++-- DevLogWidget/Store/WidgetAppGroup.swift | 4 -- .../View/TodayTodoWidgetEntryView.swift | 64 +++++++++++++++---- DevLogWidget/Widget/TodayTodoWidget.swift | 4 +- 5 files changed, 77 insertions(+), 22 deletions(-) diff --git a/DevLogWidget/Model/TodayTodoWidgetEntry.swift b/DevLogWidget/Model/TodayTodoWidgetEntry.swift index 238e6ff1..177571db 100644 --- a/DevLogWidget/Model/TodayTodoWidgetEntry.swift +++ b/DevLogWidget/Model/TodayTodoWidgetEntry.swift @@ -9,4 +9,5 @@ import WidgetKit struct TodayTodoWidgetEntry: TimelineEntry { let date: Date + let snapshot: TodayWidgetSnapshot? } diff --git a/DevLogWidget/Provider/TodayTodoWidgetProvider.swift b/DevLogWidget/Provider/TodayTodoWidgetProvider.swift index 9ca3c5c8..cc9325f0 100644 --- a/DevLogWidget/Provider/TodayTodoWidgetProvider.swift +++ b/DevLogWidget/Provider/TodayTodoWidgetProvider.swift @@ -10,24 +10,42 @@ import WidgetKit struct TodayTodoWidgetProvider: AppIntentTimelineProvider { typealias Intent = TodayTodoWidgetConfigurationIntent + typealias Entry = TodayTodoWidgetEntry + private let store = WidgetSnapshotStore() + // ์œ„์ ฏ ๊ฐค๋Ÿฌ๋ฆฌ๋‚˜ ๋กœ๋”ฉ ์ „ ์ƒํƒœ์—์„œ ์ฆ‰์‹œ ํ‘œ์‹œํ•  ๊ธฐ๋ณธ ์—”ํŠธ๋ฆฌ. func placeholder(in context: Context) -> TodayTodoWidgetEntry { - .init(date: .now) + .init(date: .now, snapshot: nil) } + // ํ˜„์žฌ ์‹œ์ ์˜ ๋‹จ์ผ ์Šค๋ƒ…์ƒท์„ ๋งŒ๋“ค์–ด ๋ฏธ๋ฆฌ๋ณด๊ธฐ์™€ ์ผ์‹œ์ ์ธ ๋ Œ๋”๋ง์— ์‚ฌ์šฉํ•œ๋‹ค. func snapshot( for configuration: TodayTodoWidgetConfigurationIntent, in context: Context ) async -> TodayTodoWidgetEntry { - .init(date: .now) + let snapshot = try? store.loadTodaySnapshot() + return .init( + date: snapshot?.generatedAt ?? .now, + snapshot: snapshot + ) } + // ์‹ค์ œ ์œ„์ ฏ์ด ์‚ฌ์šฉํ•  ํƒ€์ž„๋ผ์ธ ์—”ํŠธ๋ฆฌ๋ฅผ ๊ตฌ์„ฑํ•œ๋‹ค. + // ํ˜„์žฌ ๋‹จ๊ณ„์—์„œ๋Š” ์ €์žฅ๋œ ์Šค๋ƒ…์ƒท ํ•˜๋‚˜๋งŒ ๋‚ด๋ ค์ฃผ๊ณ , ๊ฐฑ์‹ ์€ ์•ฑ์ด ๋ณ„๋„๋กœ ํŠธ๋ฆฌ๊ฑฐํ•œ๋‹ค. func timeline( for configuration: TodayTodoWidgetConfigurationIntent, in context: Context ) async -> Timeline { - Timeline( - entries: [.init(date: .now)], + let snapshot = try? store.loadTodaySnapshot() + let entries: [TodayTodoWidgetEntry] = [ + .init( + date: snapshot?.generatedAt ?? .now, + snapshot: snapshot + ) + ] + + return Timeline( + entries: entries, policy: .never ) } diff --git a/DevLogWidget/Store/WidgetAppGroup.swift b/DevLogWidget/Store/WidgetAppGroup.swift index 1ecc0859..aff00e78 100644 --- a/DevLogWidget/Store/WidgetAppGroup.swift +++ b/DevLogWidget/Store/WidgetAppGroup.swift @@ -10,7 +10,3 @@ import Foundation enum WidgetAppGroup { static let identifier = "group.opfic.DevLog" } - -enum WidgetSharedUserDefaults { - static let shared = UserDefaults(suiteName: WidgetAppGroup.identifier) -} diff --git a/DevLogWidget/View/TodayTodoWidgetEntryView.swift b/DevLogWidget/View/TodayTodoWidgetEntryView.swift index 62ee15ad..386cae5e 100644 --- a/DevLogWidget/View/TodayTodoWidgetEntryView.swift +++ b/DevLogWidget/View/TodayTodoWidgetEntryView.swift @@ -9,6 +9,7 @@ import SwiftUI import WidgetKit struct TodayTodoWidgetEntryView: View { + let entry: TodayTodoWidgetEntry @Environment(\.widgetFamily) private var widgetFamily var body: some View { @@ -18,21 +19,60 @@ struct TodayTodoWidgetEntryView: View { Spacer() - switch widgetFamily { - case .systemSmall: - Text("์•ฑ์„ ์—ด์–ด\nToday ์œ„์ ฏ์„ ์ค€๋น„ํ•˜์„ธ์š”") + if let snapshot = entry.snapshot { + content(snapshot) + } else { + emptyState + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + } + + @ViewBuilder + private func content(_ snapshot: TodayWidgetSnapshot) -> some View { + switch widgetFamily { + case .systemSmall: + VStack(alignment: .leading, spacing: 4) { + Text("\(snapshot.totalCount)") + .font(.system(size: 28, weight: .bold)) + Text(topItemTitle(from: snapshot)) .font(.caption) .foregroundStyle(.secondary) - case .systemMedium, .systemLarge: - WidgetPlaceholderCard( - title: "Today Todo", - message: "๋ฐ์ดํ„ฐ ์—ฐ๊ฒฐ ์ „" - ) - .frame(maxWidth: .infinity) - default: - EmptyView() + .lineLimit(2) } + case .systemMedium, .systemLarge: + WidgetPlaceholderCard( + title: "Today Todo", + message: "์ €์žฅ๋œ ํ•  ์ผ \(snapshot.totalCount)๊ฐœ" + ) + .frame(maxWidth: .infinity) + default: + EmptyView() } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + } + + @ViewBuilder + private var emptyState: some View { + switch widgetFamily { + case .systemSmall: + Text("์•ฑ์„ ์—ด์–ด\nToday ์œ„์ ฏ์„ ์ค€๋น„ํ•˜์„ธ์š”") + .font(.caption) + .foregroundStyle(.secondary) + case .systemMedium, .systemLarge: + WidgetPlaceholderCard( + title: "Today Todo", + message: "๋ฐ์ดํ„ฐ ์—ฐ๊ฒฐ ์ „" + ) + .frame(maxWidth: .infinity) + default: + EmptyView() + } + } + + private func topItemTitle(from snapshot: TodayWidgetSnapshot) -> String { + snapshot.sections + .flatMap(\.items) + .first? + .title ?? "ํ•  ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค" } } diff --git a/DevLogWidget/Widget/TodayTodoWidget.swift b/DevLogWidget/Widget/TodayTodoWidget.swift index d74bcb92..4de1f71e 100644 --- a/DevLogWidget/Widget/TodayTodoWidget.swift +++ b/DevLogWidget/Widget/TodayTodoWidget.swift @@ -17,8 +17,8 @@ struct TodayTodoWidget: Widget { kind: kind, intent: TodayTodoWidgetConfigurationIntent.self, provider: TodayTodoWidgetProvider() - ) { _ in - TodayTodoWidgetEntryView() + ) { entry in + TodayTodoWidgetEntryView(entry: entry) .containerBackground(.fill.tertiary, for: .widget) } .configurationDisplayName("Today Todo") From 79e89dae955bc679c572f11ce82c621fcb774211 Mon Sep 17 00:00:00 2001 From: opficdev Date: Fri, 17 Apr 2026 15:36:15 +0900 Subject: [PATCH 08/17] =?UTF-8?q?chore:=20WidgetKit=EC=9D=98=20Provider-En?= =?UTF-8?q?try-View=20=ED=8C=A8=ED=84=B4=EC=9C=BC=EB=A1=9C=20=ED=8F=B4?= =?UTF-8?q?=EB=8D=94=EB=A7=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/Widget/{ => Common}/WidgetAppGroup.swift | 0 DevLog/Widget/{ => Common}/WidgetSharedDefaultsStore.swift | 0 DevLog/Widget/{ => Common}/WidgetSnapshotStore.swift | 0 DevLog/Widget/{ => Heatmap}/ProfileHeatmapWidgetSnapshot.swift | 0 DevLog/Widget/{ => Today}/TodayWidgetSnapshot.swift | 0 DevLogWidget/{Store => Common}/WidgetAppGroup.swift | 0 DevLogWidget/{View => Common}/WidgetPlaceholderCard.swift | 0 DevLogWidget/{Store => Common}/WidgetSharedDefaultsStore.swift | 0 DevLogWidget/{Store => Common}/WidgetSnapshotStore.swift | 0 DevLogWidget/{Widget => Heatmap}/HeatmapWidget.swift | 0 .../{Intent => Heatmap}/HeatmapWidgetConfigurationIntent.swift | 0 DevLogWidget/{Model => Heatmap}/HeatmapWidgetEntry.swift | 0 DevLogWidget/{View => Heatmap}/HeatmapWidgetEntryView.swift | 0 DevLogWidget/{Provider => Heatmap}/HeatmapWidgetProvider.swift | 0 DevLogWidget/{Model => Heatmap}/HeatmapWidgetSnapshot.swift | 0 DevLogWidget/{Widget => Today}/TodayTodoWidget.swift | 0 .../{Intent => Today}/TodayTodoWidgetConfigurationIntent.swift | 0 DevLogWidget/{Model => Today}/TodayTodoWidgetEntry.swift | 0 DevLogWidget/{View => Today}/TodayTodoWidgetEntryView.swift | 0 DevLogWidget/{Provider => Today}/TodayTodoWidgetProvider.swift | 0 DevLogWidget/{Model => Today}/TodayWidgetSnapshot.swift | 0 21 files changed, 0 insertions(+), 0 deletions(-) rename DevLog/Widget/{ => Common}/WidgetAppGroup.swift (100%) rename DevLog/Widget/{ => Common}/WidgetSharedDefaultsStore.swift (100%) rename DevLog/Widget/{ => Common}/WidgetSnapshotStore.swift (100%) rename DevLog/Widget/{ => Heatmap}/ProfileHeatmapWidgetSnapshot.swift (100%) rename DevLog/Widget/{ => Today}/TodayWidgetSnapshot.swift (100%) rename DevLogWidget/{Store => Common}/WidgetAppGroup.swift (100%) rename DevLogWidget/{View => Common}/WidgetPlaceholderCard.swift (100%) rename DevLogWidget/{Store => Common}/WidgetSharedDefaultsStore.swift (100%) rename DevLogWidget/{Store => Common}/WidgetSnapshotStore.swift (100%) rename DevLogWidget/{Widget => Heatmap}/HeatmapWidget.swift (100%) rename DevLogWidget/{Intent => Heatmap}/HeatmapWidgetConfigurationIntent.swift (100%) rename DevLogWidget/{Model => Heatmap}/HeatmapWidgetEntry.swift (100%) rename DevLogWidget/{View => Heatmap}/HeatmapWidgetEntryView.swift (100%) rename DevLogWidget/{Provider => Heatmap}/HeatmapWidgetProvider.swift (100%) rename DevLogWidget/{Model => Heatmap}/HeatmapWidgetSnapshot.swift (100%) rename DevLogWidget/{Widget => Today}/TodayTodoWidget.swift (100%) rename DevLogWidget/{Intent => Today}/TodayTodoWidgetConfigurationIntent.swift (100%) rename DevLogWidget/{Model => Today}/TodayTodoWidgetEntry.swift (100%) rename DevLogWidget/{View => Today}/TodayTodoWidgetEntryView.swift (100%) rename DevLogWidget/{Provider => Today}/TodayTodoWidgetProvider.swift (100%) rename DevLogWidget/{Model => Today}/TodayWidgetSnapshot.swift (100%) diff --git a/DevLog/Widget/WidgetAppGroup.swift b/DevLog/Widget/Common/WidgetAppGroup.swift similarity index 100% rename from DevLog/Widget/WidgetAppGroup.swift rename to DevLog/Widget/Common/WidgetAppGroup.swift diff --git a/DevLog/Widget/WidgetSharedDefaultsStore.swift b/DevLog/Widget/Common/WidgetSharedDefaultsStore.swift similarity index 100% rename from DevLog/Widget/WidgetSharedDefaultsStore.swift rename to DevLog/Widget/Common/WidgetSharedDefaultsStore.swift diff --git a/DevLog/Widget/WidgetSnapshotStore.swift b/DevLog/Widget/Common/WidgetSnapshotStore.swift similarity index 100% rename from DevLog/Widget/WidgetSnapshotStore.swift rename to DevLog/Widget/Common/WidgetSnapshotStore.swift diff --git a/DevLog/Widget/ProfileHeatmapWidgetSnapshot.swift b/DevLog/Widget/Heatmap/ProfileHeatmapWidgetSnapshot.swift similarity index 100% rename from DevLog/Widget/ProfileHeatmapWidgetSnapshot.swift rename to DevLog/Widget/Heatmap/ProfileHeatmapWidgetSnapshot.swift diff --git a/DevLog/Widget/TodayWidgetSnapshot.swift b/DevLog/Widget/Today/TodayWidgetSnapshot.swift similarity index 100% rename from DevLog/Widget/TodayWidgetSnapshot.swift rename to DevLog/Widget/Today/TodayWidgetSnapshot.swift diff --git a/DevLogWidget/Store/WidgetAppGroup.swift b/DevLogWidget/Common/WidgetAppGroup.swift similarity index 100% rename from DevLogWidget/Store/WidgetAppGroup.swift rename to DevLogWidget/Common/WidgetAppGroup.swift diff --git a/DevLogWidget/View/WidgetPlaceholderCard.swift b/DevLogWidget/Common/WidgetPlaceholderCard.swift similarity index 100% rename from DevLogWidget/View/WidgetPlaceholderCard.swift rename to DevLogWidget/Common/WidgetPlaceholderCard.swift diff --git a/DevLogWidget/Store/WidgetSharedDefaultsStore.swift b/DevLogWidget/Common/WidgetSharedDefaultsStore.swift similarity index 100% rename from DevLogWidget/Store/WidgetSharedDefaultsStore.swift rename to DevLogWidget/Common/WidgetSharedDefaultsStore.swift diff --git a/DevLogWidget/Store/WidgetSnapshotStore.swift b/DevLogWidget/Common/WidgetSnapshotStore.swift similarity index 100% rename from DevLogWidget/Store/WidgetSnapshotStore.swift rename to DevLogWidget/Common/WidgetSnapshotStore.swift diff --git a/DevLogWidget/Widget/HeatmapWidget.swift b/DevLogWidget/Heatmap/HeatmapWidget.swift similarity index 100% rename from DevLogWidget/Widget/HeatmapWidget.swift rename to DevLogWidget/Heatmap/HeatmapWidget.swift diff --git a/DevLogWidget/Intent/HeatmapWidgetConfigurationIntent.swift b/DevLogWidget/Heatmap/HeatmapWidgetConfigurationIntent.swift similarity index 100% rename from DevLogWidget/Intent/HeatmapWidgetConfigurationIntent.swift rename to DevLogWidget/Heatmap/HeatmapWidgetConfigurationIntent.swift diff --git a/DevLogWidget/Model/HeatmapWidgetEntry.swift b/DevLogWidget/Heatmap/HeatmapWidgetEntry.swift similarity index 100% rename from DevLogWidget/Model/HeatmapWidgetEntry.swift rename to DevLogWidget/Heatmap/HeatmapWidgetEntry.swift diff --git a/DevLogWidget/View/HeatmapWidgetEntryView.swift b/DevLogWidget/Heatmap/HeatmapWidgetEntryView.swift similarity index 100% rename from DevLogWidget/View/HeatmapWidgetEntryView.swift rename to DevLogWidget/Heatmap/HeatmapWidgetEntryView.swift diff --git a/DevLogWidget/Provider/HeatmapWidgetProvider.swift b/DevLogWidget/Heatmap/HeatmapWidgetProvider.swift similarity index 100% rename from DevLogWidget/Provider/HeatmapWidgetProvider.swift rename to DevLogWidget/Heatmap/HeatmapWidgetProvider.swift diff --git a/DevLogWidget/Model/HeatmapWidgetSnapshot.swift b/DevLogWidget/Heatmap/HeatmapWidgetSnapshot.swift similarity index 100% rename from DevLogWidget/Model/HeatmapWidgetSnapshot.swift rename to DevLogWidget/Heatmap/HeatmapWidgetSnapshot.swift diff --git a/DevLogWidget/Widget/TodayTodoWidget.swift b/DevLogWidget/Today/TodayTodoWidget.swift similarity index 100% rename from DevLogWidget/Widget/TodayTodoWidget.swift rename to DevLogWidget/Today/TodayTodoWidget.swift diff --git a/DevLogWidget/Intent/TodayTodoWidgetConfigurationIntent.swift b/DevLogWidget/Today/TodayTodoWidgetConfigurationIntent.swift similarity index 100% rename from DevLogWidget/Intent/TodayTodoWidgetConfigurationIntent.swift rename to DevLogWidget/Today/TodayTodoWidgetConfigurationIntent.swift diff --git a/DevLogWidget/Model/TodayTodoWidgetEntry.swift b/DevLogWidget/Today/TodayTodoWidgetEntry.swift similarity index 100% rename from DevLogWidget/Model/TodayTodoWidgetEntry.swift rename to DevLogWidget/Today/TodayTodoWidgetEntry.swift diff --git a/DevLogWidget/View/TodayTodoWidgetEntryView.swift b/DevLogWidget/Today/TodayTodoWidgetEntryView.swift similarity index 100% rename from DevLogWidget/View/TodayTodoWidgetEntryView.swift rename to DevLogWidget/Today/TodayTodoWidgetEntryView.swift diff --git a/DevLogWidget/Provider/TodayTodoWidgetProvider.swift b/DevLogWidget/Today/TodayTodoWidgetProvider.swift similarity index 100% rename from DevLogWidget/Provider/TodayTodoWidgetProvider.swift rename to DevLogWidget/Today/TodayTodoWidgetProvider.swift diff --git a/DevLogWidget/Model/TodayWidgetSnapshot.swift b/DevLogWidget/Today/TodayWidgetSnapshot.swift similarity index 100% rename from DevLogWidget/Model/TodayWidgetSnapshot.swift rename to DevLogWidget/Today/TodayWidgetSnapshot.swift From 0a6ec595ed791669a8c10d50c43840cf60dc0e32 Mon Sep 17 00:00:00 2001 From: opficdev Date: Fri, 17 Apr 2026 15:59:28 +0900 Subject: [PATCH 09/17] =?UTF-8?q?feat:=20TodayViewModel=EC=9D=98=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=EC=9D=84=20=EC=B0=B8=EA=B3=A0=ED=95=98?= =?UTF-8?q?=EC=97=AC=20=EC=9C=84=EC=A0=AF=EC=97=90=EC=84=9C=20=EB=B3=B4?= =?UTF-8?q?=EC=97=AC=EC=A4=84=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Today/TodayWidgetSnapshotFactory.swift | 182 ++++++++++++++++++ .../TodayWidgetSnapshotFactoryTests.swift | 133 +++++++++++++ 2 files changed, 315 insertions(+) create mode 100644 DevLog/Widget/Today/TodayWidgetSnapshotFactory.swift create mode 100644 DevLog_Unit/Widget/TodayWidgetSnapshotFactoryTests.swift diff --git a/DevLog/Widget/Today/TodayWidgetSnapshotFactory.swift b/DevLog/Widget/Today/TodayWidgetSnapshotFactory.swift new file mode 100644 index 00000000..f6e72f78 --- /dev/null +++ b/DevLog/Widget/Today/TodayWidgetSnapshotFactory.swift @@ -0,0 +1,182 @@ +// +// TodayWidgetSnapshotFactory.swift +// DevLog +// +// Created by opfic on 4/17/26. +// + +import Foundation + +struct TodayWidgetSnapshotFactory { + private enum SectionCategory: String, CaseIterable { + case focused + case overdue + case dueSoon + case later + case unscheduled + } + + private struct SectionCollection { + var focused = [TodayTodoItem]() + var overdue = [TodayTodoItem]() + var dueSoon = [TodayTodoItem]() + var later = [TodayTodoItem]() + var unscheduled = [TodayTodoItem]() + + func items(for category: SectionCategory) -> [TodayTodoItem] { + switch category { + case .focused: + focused + case .overdue: + overdue + case .dueSoon: + dueSoon + case .later: + later + case .unscheduled: + unscheduled + } + } + } + + private let calendar: Calendar + private let upcomingWindowDays: Int + + init( + calendar: Calendar = .current, + upcomingWindowDays: Int = 7 + ) { + self.calendar = calendar + self.upcomingWindowDays = upcomingWindowDays + } + + func makeSnapshot( + todos: [TodayTodoItem], + displayOptions: TodayDisplayOptions, + now: Date = Date() + ) -> TodayWidgetSnapshot { + let displayedTodos = displayedTodos( + from: todos, + displayOptions: displayOptions + ) + let sections = groupedSectionItems( + from: displayedTodos, + now: now + ) + + return TodayWidgetSnapshot( + generatedAt: now, + totalCount: displayedTodos.count, + focusedCount: displayedTodos.filter(\.isPinned).count, + overdueCount: displayedTodos.filter { isOverdue($0, now: now) }.count, + dueSoonCount: displayedTodos.filter { isDueSoon($0, now: now) }.count, + sections: SectionCategory.allCases.compactMap { category in + let items = sections.items(for: category) + guard !items.isEmpty else { return nil } + + return TodayWidgetSectionSnapshot( + category: category.rawValue, + items: items.map { + WidgetTodoSnapshotItem( + id: $0.id, + number: $0.number, + title: $0.title, + isPinned: $0.isPinned, + dueDate: $0.dueDate + ) + } + ) + } + ) + } + + private func displayedTodos( + from todos: [TodayTodoItem], + displayOptions: TodayDisplayOptions + ) -> [TodayTodoItem] { + let dueDateFilteredTodos: [TodayTodoItem] + switch displayOptions.dueDateVisibility { + case .all: + dueDateFilteredTodos = todos + case .withDueDateOnly: + dueDateFilteredTodos = todos.filter { $0.dueDate != nil } + case .withoutDueDateOnly: + dueDateFilteredTodos = todos.filter { $0.dueDate == nil } + } + + switch displayOptions.focusVisibility { + case .all: + return dueDateFilteredTodos + case .focusedOnly: + return dueDateFilteredTodos.filter(\.isPinned) + } + } + + private func groupedSectionItems( + from items: [TodayTodoItem], + now: Date + ) -> SectionCollection { + let startOfToday = calendar.startOfDay(for: now) + guard let windowEnd = calendar.date( + byAdding: .day, + value: upcomingWindowDays, + to: startOfToday + ) else { + return SectionCollection( + focused: items.filter(\.isPinned), + unscheduled: items.filter { !$0.isPinned && $0.dueDate == nil } + ) + } + + var collection = SectionCollection() + + for item in items { + if item.isPinned { + collection.focused.append(item) + continue + } + + guard let dueDate = item.dueDate else { + collection.unscheduled.append(item) + continue + } + + let dueDay = calendar.startOfDay(for: dueDate) + if dueDay < startOfToday { + collection.overdue.append(item) + } else if dueDay <= windowEnd { + collection.dueSoon.append(item) + } else { + collection.later.append(item) + } + } + + return collection + } + + private func isOverdue( + _ item: TodayTodoItem, + now: Date + ) -> Bool { + guard let dueDate = item.dueDate else { return false } + return calendar.startOfDay(for: dueDate) < calendar.startOfDay(for: now) + } + + private func isDueSoon( + _ item: TodayTodoItem, + now: Date + ) -> Bool { + guard let dueDate = item.dueDate else { return false } + let startOfToday = calendar.startOfDay(for: now) + guard let windowEnd = calendar.date( + byAdding: .day, + value: upcomingWindowDays, + to: startOfToday + ) else { + return false + } + + let dueDay = calendar.startOfDay(for: dueDate) + return startOfToday <= dueDay && dueDay <= windowEnd + } +} diff --git a/DevLog_Unit/Widget/TodayWidgetSnapshotFactoryTests.swift b/DevLog_Unit/Widget/TodayWidgetSnapshotFactoryTests.swift new file mode 100644 index 00000000..c8452219 --- /dev/null +++ b/DevLog_Unit/Widget/TodayWidgetSnapshotFactoryTests.swift @@ -0,0 +1,133 @@ +// +// TodayWidgetSnapshotFactoryTests.swift +// DevLog_Unit +// +// Created by opfic on 4/17/26. +// + +import Foundation +import Testing +@testable import DevLog + +struct TodayWidgetSnapshotFactoryTests { + @Test("Today ์œ„์ ฏ ์Šค๋ƒ…์ƒท์€ ํ™”๋ฉด ๊ทœ์น™๊ณผ ๊ฐ™์€ ์ˆœ์„œ๋กœ ์„น์…˜๊ณผ ์š”์•ฝ ์ˆ˜์น˜๋ฅผ ๋งŒ๋“ ๋‹ค") + func today_์œ„์ ฏ_์Šค๋ƒ…์ƒท์€_ํ™”๋ฉด_๊ทœ์น™๊ณผ_๊ฐ™์€_์ˆœ์„œ๋กœ_์„น์…˜๊ณผ_์š”์•ฝ_์ˆ˜์น˜๋ฅผ_๋งŒ๋“ ๋‹ค() throws { + let calendar = Calendar(identifier: .gregorian) + let now = try #require(calendar.date(from: DateComponents(year: 2026, month: 4, day: 17))) + let factory = TodayWidgetSnapshotFactory(calendar: calendar) + + let snapshot = factory.makeSnapshot( + todos: try makeTodayTodos(now: now, calendar: calendar), + displayOptions: .default, + now: now + ) + + #expect(snapshot.totalCount == 5) + #expect(snapshot.focusedCount == 1) + #expect(snapshot.overdueCount == 1) + #expect(snapshot.dueSoonCount == 1) + #expect(snapshot.sections.map(\.category) == ["focused", "overdue", "dueSoon", "later", "unscheduled"]) + #expect(snapshot.sections[0].items.map(\.title) == ["๊ณ ์ •๋œ ํ•  ์ผ"]) + #expect(snapshot.sections[1].items.map(\.title) == ["์ง€๋‚œ ์ผ์ •"]) + #expect(snapshot.sections[2].items.map(\.title) == ["์ž„๋ฐ• ์ผ์ •"]) + #expect(snapshot.sections[3].items.map(\.title) == ["๋‚˜์ค‘ ์ผ์ •"]) + #expect(snapshot.sections[4].items.map(\.title) == ["๋ฏธ์ • ์ผ์ •"]) + } + + @Test("Today ์œ„์ ฏ ์Šค๋ƒ…์ƒท์€ ํ™”๋ฉด๊ณผ ๊ฐ™์€ display option ํ•„ํ„ฐ๋ฅผ ์ ์šฉํ•œ๋‹ค") + func today_์œ„์ ฏ_์Šค๋ƒ…์ƒท์€_ํ™”๋ฉด๊ณผ_๊ฐ™์€_display_option_ํ•„ํ„ฐ๋ฅผ_์ ์šฉํ•œ๋‹ค() throws { + let calendar = Calendar(identifier: .gregorian) + let now = try #require(calendar.date(from: DateComponents(year: 2026, month: 4, day: 17))) + let factory = TodayWidgetSnapshotFactory(calendar: calendar) + + let snapshot = factory.makeSnapshot( + todos: try makeTodayTodos(now: now, calendar: calendar), + displayOptions: TodayDisplayOptions( + dueDateVisibility: .withDueDateOnly, + focusVisibility: .focusedOnly + ), + now: now + ) + + #expect(snapshot.totalCount == 1) + #expect(snapshot.focusedCount == 1) + #expect(snapshot.overdueCount == 0) + #expect(snapshot.dueSoonCount == 0) + #expect(snapshot.sections.map(\.category) == ["focused"]) + #expect(snapshot.sections[0].items.map(\.title) == ["๊ณ ์ •๋œ ํ•  ์ผ"]) + } + + private func makeTodayTodos( + now: Date, + calendar: Calendar + ) throws -> [TodayTodoItem] { + let overdueDate = try #require(calendar.date(byAdding: .day, value: -1, to: now)) + let dueSoonDate = try #require(calendar.date(byAdding: .day, value: 3, to: now)) + let laterDate = try #require(calendar.date(byAdding: .day, value: 9, to: now)) + + return try [ + makeTodayTodoItem( + id: "todo-1", + number: 1, + title: "๊ณ ์ •๋œ ํ•  ์ผ", + isPinned: true, + dueDate: dueSoonDate + ), + makeTodayTodoItem( + id: "todo-2", + number: 2, + title: "์ง€๋‚œ ์ผ์ •", + isPinned: false, + dueDate: overdueDate + ), + makeTodayTodoItem( + id: "todo-3", + number: 3, + title: "์ž„๋ฐ• ์ผ์ •", + isPinned: false, + dueDate: dueSoonDate + ), + makeTodayTodoItem( + id: "todo-4", + number: 4, + title: "๋‚˜์ค‘ ์ผ์ •", + isPinned: false, + dueDate: laterDate + ), + makeTodayTodoItem( + id: "todo-5", + number: 5, + title: "๋ฏธ์ • ์ผ์ •", + isPinned: false, + dueDate: nil + ) + ] + } + + private func makeTodayTodoItem( + id: String, + number: Int, + title: String, + isPinned: Bool, + dueDate: Date? + ) throws -> TodayTodoItem { + let todo = Todo( + id: id, + isPinned: isPinned, + isCompleted: false, + isChecked: false, + number: number, + title: title, + content: "", + createdAt: .now, + updatedAt: .now, + completedAt: nil, + deletedAt: nil, + dueDate: dueDate, + tags: [], + category: .system(.feature) + ) + + return try #require(TodayTodoItem(from: todo)) + } +} From 816374abde21556f1afbde4aa0917c4d30f6ac39 Mon Sep 17 00:00:00 2001 From: opficdev Date: Fri, 17 Apr 2026 16:12:29 +0900 Subject: [PATCH 10/17] =?UTF-8?q?refactor:=20Today=20=EC=9C=84=EC=A0=AF=20?= =?UTF-8?q?=EB=8F=99=EA=B8=B0=ED=99=94=20=EC=BD=94=EB=94=94=EB=84=A4?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewModel/TodayViewModel.swift | 19 +++++++++ .../Today/TodayWidgetSyncCoordinator.swift | 41 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 DevLog/Widget/Today/TodayWidgetSyncCoordinator.swift diff --git a/DevLog/Presentation/ViewModel/TodayViewModel.swift b/DevLog/Presentation/ViewModel/TodayViewModel.swift index a4e7b113..d5ef9823 100644 --- a/DevLog/Presentation/ViewModel/TodayViewModel.swift +++ b/DevLog/Presentation/ViewModel/TodayViewModel.swift @@ -82,6 +82,7 @@ final class TodayViewModel: Store { private let upsertTodoUseCase: UpsertTodoUseCase private let updateTodayDisplayOptionsUseCase: UpdateTodayDisplayOptionsUseCase private let loadingState = LoadingState() + private let widgetCoordinator = TodayWidgetSyncCoordinator() init( fetchTodosUseCase: FetchTodosUseCase, @@ -183,6 +184,7 @@ final class TodayViewModel: Store { } if self.state != state { self.state = state } + widgetSyncIfNeeded(for: action) return effects } @@ -422,4 +424,21 @@ private extension TodayViewModel { let dueDay = calendar.startOfDay(for: dueDate) return startOfToday <= dueDay && dueDay <= windowEnd } + + func widgetSyncIfNeeded(for action: Action) { + switch action { + case .setDueDateVisibility, + .setFocusVisibility, + .resetDisplayOptions, + .fetchTodos, + .updateTodo, + .removeTodo: + widgetCoordinator.sync( + todos: state.todos, + displayOptions: state.displayOptions + ) + default: + break + } + } } diff --git a/DevLog/Widget/Today/TodayWidgetSyncCoordinator.swift b/DevLog/Widget/Today/TodayWidgetSyncCoordinator.swift new file mode 100644 index 00000000..41904d58 --- /dev/null +++ b/DevLog/Widget/Today/TodayWidgetSyncCoordinator.swift @@ -0,0 +1,41 @@ +// +// TodayWidgetSyncCoordinator.swift +// DevLog +// +// Created by opfic on 4/17/26. +// + +import Foundation +import WidgetKit + +final class TodayWidgetSyncCoordinator { + private let factory: TodayWidgetSnapshotFactory + private let store: WidgetSnapshotStore + + init( + factory: TodayWidgetSnapshotFactory = .init(), + store: WidgetSnapshotStore = .init() + ) { + self.factory = factory + self.store = store + } + + func sync( + todos: [TodayTodoItem], + displayOptions: TodayDisplayOptions, + now: Date = Date() + ) { + let todayWidgetSnapshot = factory.makeSnapshot( + todos: todos, + displayOptions: displayOptions, + now: now + ) + + do { + try store.saveTodaySnapshot(todayWidgetSnapshot) + WidgetCenter.shared.reloadTimelines(ofKind: "TodayTodoWidget") + } catch { + return + } + } +} From aeb783dd9d8b7433bc4bcb3c2959a2d116bf9f97 Mon Sep 17 00:00:00 2001 From: opficdev Date: Fri, 17 Apr 2026 16:25:16 +0900 Subject: [PATCH 11/17] =?UTF-8?q?chore:=20Profile=20=EC=A0=91=EB=91=90?= =?UTF-8?q?=EC=82=AC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/Widget/Common/WidgetSnapshotStore.swift | 12 ++++++------ ...getSnapshot.swift => HeatmapWidgetSnapshot.swift} | 4 ++-- DevLogWidget/Common/WidgetSnapshotStore.swift | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) rename DevLog/Widget/Heatmap/{ProfileHeatmapWidgetSnapshot.swift => HeatmapWidgetSnapshot.swift} (84%) diff --git a/DevLog/Widget/Common/WidgetSnapshotStore.swift b/DevLog/Widget/Common/WidgetSnapshotStore.swift index 13fa6098..d419a1db 100644 --- a/DevLog/Widget/Common/WidgetSnapshotStore.swift +++ b/DevLog/Widget/Common/WidgetSnapshotStore.swift @@ -10,7 +10,7 @@ import Foundation final class WidgetSnapshotStore { private enum Key { static let todaySnapshot = "Widget.today.snapshot" - static let profileHeatmapSnapshot = "Widget.profileHeatmap.snapshot" + static let heatmapSnapshot = "Widget.heatmap.snapshot" } private let store: WidgetSharedDefaultsStore @@ -31,13 +31,13 @@ final class WidgetSnapshotStore { return try decoder.decode(TodayWidgetSnapshot.self, from: data) } - func saveProfileHeatmapSnapshot(_ snapshot: ProfileHeatmapWidgetSnapshot) throws { + func saveHeatmapSnapshot(_ snapshot: HeatmapWidgetSnapshot) throws { let data = try encoder.encode(snapshot) - store.setData(data, forKey: Key.profileHeatmapSnapshot) + store.setData(data, forKey: Key.heatmapSnapshot) } - func loadProfileHeatmapSnapshot() throws -> ProfileHeatmapWidgetSnapshot? { - guard let data = store.data(forKey: Key.profileHeatmapSnapshot) else { return nil } - return try decoder.decode(ProfileHeatmapWidgetSnapshot.self, from: data) + func loadHeatmapSnapshot() throws -> HeatmapWidgetSnapshot? { + guard let data = store.data(forKey: Key.heatmapSnapshot) else { return nil } + return try decoder.decode(HeatmapWidgetSnapshot.self, from: data) } } diff --git a/DevLog/Widget/Heatmap/ProfileHeatmapWidgetSnapshot.swift b/DevLog/Widget/Heatmap/HeatmapWidgetSnapshot.swift similarity index 84% rename from DevLog/Widget/Heatmap/ProfileHeatmapWidgetSnapshot.swift rename to DevLog/Widget/Heatmap/HeatmapWidgetSnapshot.swift index ad4d4385..530b5c56 100644 --- a/DevLog/Widget/Heatmap/ProfileHeatmapWidgetSnapshot.swift +++ b/DevLog/Widget/Heatmap/HeatmapWidgetSnapshot.swift @@ -1,5 +1,5 @@ // -// ProfileHeatmapWidgetSnapshot.swift +// HeatmapWidgetSnapshot.swift // DevLog // // Created by opfic on 4/17/26. @@ -7,7 +7,7 @@ import Foundation -struct ProfileHeatmapWidgetSnapshot: Codable, Equatable { +struct HeatmapWidgetSnapshot: Codable, Equatable { let generatedAt: Date let monthStart: Date let selectedActivityKindRawValues: [String] diff --git a/DevLogWidget/Common/WidgetSnapshotStore.swift b/DevLogWidget/Common/WidgetSnapshotStore.swift index f116e1be..c2aed0d9 100644 --- a/DevLogWidget/Common/WidgetSnapshotStore.swift +++ b/DevLogWidget/Common/WidgetSnapshotStore.swift @@ -10,7 +10,7 @@ import Foundation final class WidgetSnapshotStore { private enum Key { static let todaySnapshot = "Widget.today.snapshot" - static let heatmapSnapshot = "Widget.profileHeatmap.snapshot" + static let heatmapSnapshot = "Widget.heatmap.snapshot" } private let store: WidgetSharedDefaultsStore From ded3ea3ac8888512a81fd87971714bb7445c8472 Mon Sep 17 00:00:00 2001 From: opficdev Date: Fri, 17 Apr 2026 16:27:37 +0900 Subject: [PATCH 12/17] =?UTF-8?q?feat:=20ProfileHeatMapView=EC=99=80=20Pro?= =?UTF-8?q?fileViewModel=EC=9D=98=20=EB=A1=9C=EC=A7=81=EC=9D=84=20?= =?UTF-8?q?=EC=B0=B8=EA=B3=A0=ED=95=B4=EC=84=9C=20=EC=9C=84=EC=A0=AF?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=B3=B4=EC=97=AC=EC=A4=84=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=83=9D=EC=84=B1=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HeatmapWidgetSnapshotFactory.swift | 235 ++++++++++++++++++ .../HeatmapWidgetSnapshotFactoryTests.swift | 156 ++++++++++++ 2 files changed, 391 insertions(+) create mode 100644 DevLog/Widget/Heatmap/HeatmapWidgetSnapshotFactory.swift create mode 100644 DevLog_Unit/Widget/HeatmapWidgetSnapshotFactoryTests.swift diff --git a/DevLog/Widget/Heatmap/HeatmapWidgetSnapshotFactory.swift b/DevLog/Widget/Heatmap/HeatmapWidgetSnapshotFactory.swift new file mode 100644 index 00000000..3172c440 --- /dev/null +++ b/DevLog/Widget/Heatmap/HeatmapWidgetSnapshotFactory.swift @@ -0,0 +1,235 @@ +// +// HeatmapWidgetSnapshotFactory.swift +// DevLog +// +// Created by opfic on 4/17/26. +// + +import Foundation + +struct HeatmapWidgetSnapshotFactory { + fileprivate struct DailyCounts { + var createdCount = 0 + var completedCount = 0 + var deletedCount = 0 + + mutating func increment(_ activityKind: ActivityKind) { + switch activityKind { + case .created: + createdCount += 1 + case .completed: + completedCount += 1 + case .deleted: + deletedCount += 1 + } + } + } + + private let calendar: Calendar + + init(calendar: Calendar = .current) { + self.calendar = calendar + } + + func makeSnapshot( + createdTodos: [Todo], + completedTodos: [Todo], + deletedTodos: [Todo], + selectedActivityKinds: Set, + monthStart: Date, + now: Date = Date() + ) -> HeatmapWidgetSnapshot { + let normalizedMonthStart = startOfMonth(for: monthStart) + let dailyCountsByDate = makeDailyCountsByDate( + createdTodos: createdTodos, + completedTodos: completedTodos, + deletedTodos: deletedTodos, + monthStart: normalizedMonthStart + ) + let weeks = makeWeeks( + monthStart: normalizedMonthStart, + dailyCountsByDate: dailyCountsByDate + ) + + return HeatmapWidgetSnapshot( + generatedAt: now, + monthStart: normalizedMonthStart, + selectedActivityKindRawValues: orderedActivityKinds(from: selectedActivityKinds).map(\.rawValue), + maxCount: maxCount( + from: weeks, + selectedActivityKinds: selectedActivityKinds + ), + weeks: weeks + ) + } +} + +private extension HeatmapWidgetSnapshotFactory { + func makeDailyCountsByDate( + createdTodos: [Todo], + completedTodos: [Todo], + deletedTodos: [Todo], + monthStart: Date + ) -> [Date: DailyCounts] { + var dailyCountsByDate = [Date: DailyCounts]() + + for todo in createdTodos { + appendCount( + activityKind: .created, + occurredAt: todo.createdAt, + monthStart: monthStart, + dailyCountsByDate: &dailyCountsByDate + ) + } + + for todo in completedTodos { + guard let completedAt = todo.completedAt else { continue } + appendCount( + activityKind: .completed, + occurredAt: completedAt, + monthStart: monthStart, + dailyCountsByDate: &dailyCountsByDate + ) + } + + for todo in deletedTodos { + guard let deletedAt = todo.deletedAt else { continue } + appendCount( + activityKind: .deleted, + occurredAt: deletedAt, + monthStart: monthStart, + dailyCountsByDate: &dailyCountsByDate + ) + } + + return dailyCountsByDate + } + + func appendCount( + activityKind: ActivityKind, + occurredAt: Date, + monthStart: Date, + dailyCountsByDate: inout [Date: DailyCounts] + ) { + guard isDateInMonth(occurredAt, monthStart: monthStart) else { return } + + let dayStart = calendar.startOfDay(for: occurredAt) + var dailyCounts = dailyCountsByDate[dayStart] ?? DailyCounts() + dailyCounts.increment(activityKind) + dailyCountsByDate[dayStart] = dailyCounts + } + + func makeWeeks( + monthStart: Date, + dailyCountsByDate: [Date: DailyCounts] + ) -> [WidgetHeatmapWeekSnapshot] { + guard let monthInterval = calendar.dateInterval(of: .month, for: monthStart), + let monthLastDay = calendar.date(byAdding: .day, value: -1, to: monthInterval.end), + let firstWeekInterval = calendar.dateInterval(of: .weekOfYear, for: monthInterval.start), + let lastWeekInterval = calendar.dateInterval(of: .weekOfYear, for: monthLastDay) else { + return [] + } + + var days = [WidgetHeatmapDaySnapshot]() + var cursor = firstWeekInterval.start + + while cursor < lastWeekInterval.end { + let normalizedDate = calendar.startOfDay(for: cursor) + let isVisible = calendar.isDate( + normalizedDate, + equalTo: monthStart, + toGranularity: .month + ) + let dailyCounts = dailyCountsByDate[normalizedDate] ?? DailyCounts() + + days.append( + WidgetHeatmapDaySnapshot( + date: normalizedDate, + createdCount: isVisible ? dailyCounts.createdCount : 0, + completedCount: isVisible ? dailyCounts.completedCount : 0, + deletedCount: isVisible ? dailyCounts.deletedCount : 0, + isVisible: isVisible + ) + ) + + guard let nextDay = calendar.date(byAdding: .day, value: 1, to: cursor) else { break } + cursor = nextDay + } + + var weeks = [WidgetHeatmapWeekSnapshot]() + var index = 0 + + while index < days.count { + let endIndex = min(index + 7, days.count) + weeks.append( + WidgetHeatmapWeekSnapshot( + id: weeks.count, + days: Array(days[index.. Date { + guard let monthInterval = calendar.dateInterval(of: .month, for: date) else { + return calendar.startOfDay(for: date) + } + return monthInterval.start + } + + func isDateInMonth( + _ date: Date, + monthStart: Date + ) -> Bool { + calendar.isDate( + calendar.startOfDay(for: date), + equalTo: monthStart, + toGranularity: .month + ) + } + + func maxCount( + from weeks: [WidgetHeatmapWeekSnapshot], + selectedActivityKinds: Set + ) -> Int { + weeks + .flatMap(\.days) + .filter(\.isVisible) + .map { day in + dayCount( + for: day, + selectedActivityKinds: selectedActivityKinds + ) + } + .max() ?? 0 + } + + func dayCount( + for day: WidgetHeatmapDaySnapshot, + selectedActivityKinds: Set + ) -> Int { + var value = 0 + + if selectedActivityKinds.contains(.created) { + value += day.createdCount + } + + if selectedActivityKinds.contains(.completed) { + value += day.completedCount + } + + if selectedActivityKinds.contains(.deleted) { + value += day.deletedCount + } + + return value + } + + func orderedActivityKinds(from activityKinds: Set) -> [ActivityKind] { + let orderedActivityKinds: [ActivityKind] = [.created, .completed, .deleted] + return orderedActivityKinds.filter { activityKinds.contains($0) } + } +} diff --git a/DevLog_Unit/Widget/HeatmapWidgetSnapshotFactoryTests.swift b/DevLog_Unit/Widget/HeatmapWidgetSnapshotFactoryTests.swift new file mode 100644 index 00000000..544c0298 --- /dev/null +++ b/DevLog_Unit/Widget/HeatmapWidgetSnapshotFactoryTests.swift @@ -0,0 +1,156 @@ +// +// HeatmapWidgetSnapshotFactoryTests.swift +// DevLog_Unit +// +// Created by opfic on 4/17/26. +// + +import Foundation +import Testing +@testable import DevLog + +struct HeatmapWidgetSnapshotFactoryTests { + @Test("Heatmap ์œ„์ ฏ ์Šค๋ƒ…์ƒท์€ ์ด๋ฒˆ ๋‹ฌ ๊ธฐ์ค€ ์ฃผ์ฐจ์™€ ์ผ๋ณ„ count๋ฅผ ๋งŒ๋“ ๋‹ค") + func heatmap_์œ„์ ฏ_์Šค๋ƒ…์ƒท์€_์ด๋ฒˆ_๋‹ฌ_๊ธฐ์ค€_์ฃผ์ฐจ์™€_์ผ๋ณ„_count๋ฅผ_๋งŒ๋“ ๋‹ค() throws { + let calendar = Calendar(identifier: .gregorian) + let monthStart = try #require(calendar.date(from: DateComponents(year: 2026, month: 4, day: 1))) + let aprilThirdDate = calendar.date(from: DateComponents(year: 2026, month: 4, day: 3))! + let mayFirstDate = calendar.date(from: DateComponents(year: 2026, month: 5, day: 1))! + let aprilFifteenthDate = calendar.date(from: DateComponents(year: 2026, month: 4, day: 15))! + let factory = HeatmapWidgetSnapshotFactory(calendar: calendar) + + let snapshot = factory.makeSnapshot( + createdTodos: [ + makeTodo( + id: "todo-created-apr-03", + createdAt: try #require(calendar.date(from: DateComponents(year: 2026, month: 4, day: 3))) + ), + makeTodo( + id: "todo-created-mar-31", + createdAt: try #require(calendar.date(from: DateComponents(year: 2026, month: 3, day: 31))) + ) + ], + completedTodos: [ + makeTodo( + id: "todo-completed-apr-03", + createdAt: monthStart, + completedAt: aprilThirdDate + ), + makeTodo( + id: "todo-completed-may-01", + createdAt: monthStart, + completedAt: mayFirstDate + ) + ], + deletedTodos: [ + makeTodo( + id: "todo-deleted-apr-15", + createdAt: monthStart, + deletedAt: aprilFifteenthDate + ) + ], + selectedActivityKinds: [.created, .completed], + monthStart: monthStart, + now: monthStart + ) + + #expect(snapshot.monthStart == monthStart) + #expect(snapshot.selectedActivityKindRawValues == ["created", "completed"]) + #expect(snapshot.maxCount == 2) + #expect(snapshot.weeks.count == 5) + #expect(snapshot.weeks.flatMap(\.days).filter(\.isVisible).count == 30) + + let aprilThird = try #require(day(for: DateComponents(year: 2026, month: 4, day: 3), in: snapshot, calendar: calendar)) + #expect(aprilThird.createdCount == 1) + #expect(aprilThird.completedCount == 1) + #expect(aprilThird.deletedCount == 0) + #expect(aprilThird.isVisible) + + let aprilFifteenth = try #require(day(for: DateComponents(year: 2026, month: 4, day: 15), in: snapshot, calendar: calendar)) + #expect(aprilFifteenth.createdCount == 0) + #expect(aprilFifteenth.completedCount == 0) + #expect(aprilFifteenth.deletedCount == 1) + } + + @Test("Heatmap ์œ„์ ฏ ์Šค๋ƒ…์ƒท maxCount๋Š” ์„ ํƒ๋œ activity kind๋งŒ ๊ธฐ์ค€์œผ๋กœ ๊ณ„์‚ฐํ•œ๋‹ค") + func heatmap_์œ„์ ฏ_์Šค๋ƒ…์ƒท_maxCount๋Š”_์„ ํƒ๋œ_activity_kind๋งŒ_๊ธฐ์ค€์œผ๋กœ_๊ณ„์‚ฐํ•œ๋‹ค() throws { + let calendar = Calendar(identifier: .gregorian) + let monthStart = try #require(calendar.date(from: DateComponents(year: 2026, month: 4, day: 1))) + let targetDate = try #require(calendar.date(from: DateComponents(year: 2026, month: 4, day: 10))) + let factory = HeatmapWidgetSnapshotFactory(calendar: calendar) + + let snapshot = factory.makeSnapshot( + createdTodos: [ + makeTodo(id: "created-1", createdAt: targetDate), + makeTodo(id: "created-2", createdAt: targetDate) + ], + completedTodos: [], + deletedTodos: [ + makeTodo( + id: "deleted-1", + createdAt: monthStart, + deletedAt: targetDate + ), + makeTodo( + id: "deleted-2", + createdAt: monthStart, + deletedAt: targetDate + ), + makeTodo( + id: "deleted-3", + createdAt: monthStart, + deletedAt: targetDate + ) + ], + selectedActivityKinds: [.deleted], + monthStart: monthStart, + now: monthStart + ) + + #expect(snapshot.selectedActivityKindRawValues == ["deleted"]) + #expect(snapshot.maxCount == 3) + + let targetDay = try #require(day(for: DateComponents(year: 2026, month: 4, day: 10), in: snapshot, calendar: calendar)) + #expect(targetDay.createdCount == 2) + #expect(targetDay.deletedCount == 3) + } + + private func day( + for components: DateComponents, + in snapshot: HeatmapWidgetSnapshot, + calendar: Calendar + ) -> WidgetHeatmapDaySnapshot? { + guard let date = calendar.date(from: components) else { return nil } + let targetDate = calendar.startOfDay(for: date) + + return snapshot.weeks + .flatMap(\.days) + .first { day in + calendar.isDate(day.date, inSameDayAs: targetDate) + } + } + + private func makeTodo( + id: String, + createdAt: Date, + completedAt: Date? = nil, + deletedAt: Date? = nil + ) -> Todo { + Todo( + id: id, + isPinned: false, + isCompleted: completedAt != nil, + isChecked: false, + number: 1, + title: id, + content: "", + createdAt: createdAt, + updatedAt: createdAt, + completedAt: completedAt, + deletedAt: deletedAt, + dueDate: nil, + tags: [], + category: .system(.feature) + ) + } +} From 06b5217bb973b3a3260c4a48f49b42a6596e3aef Mon Sep 17 00:00:00 2001 From: opficdev Date: Fri, 17 Apr 2026 16:35:27 +0900 Subject: [PATCH 13/17] =?UTF-8?q?feat:=20Heatmap=20=EC=9C=84=EC=A0=AF=20?= =?UTF-8?q?=EC=9B=94=EA=B0=84=20=EC=8A=A4=EB=83=85=EC=83=B7=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewModel/ProfileViewModel.swift | 18 ++++ .../HeatmapWidgetSyncCoordinator.swift | 98 +++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 DevLog/Widget/Heatmap/HeatmapWidgetSyncCoordinator.swift diff --git a/DevLog/Presentation/ViewModel/ProfileViewModel.swift b/DevLog/Presentation/ViewModel/ProfileViewModel.swift index 16ee4695..59bc6b59 100644 --- a/DevLog/Presentation/ViewModel/ProfileViewModel.swift +++ b/DevLog/Presentation/ViewModel/ProfileViewModel.swift @@ -70,6 +70,7 @@ final class ProfileViewModel: Store { private let networkConnectivityUseCase: ObserveNetworkConnectivityUseCase private let fetchHeatmapActivityTypesUseCase: FetchProfileHeatmapActivityTypesUseCase private let updateHeatmapActivityTypesUseCase: UpdateProfileHeatmapActivityTypesUseCase + private let widgetCoordinator: HeatmapWidgetSyncCoordinator private let calendar = Calendar.current private let loadingState = LoadingState() private var cancellables = Set() @@ -88,6 +89,9 @@ final class ProfileViewModel: Store { self.networkConnectivityUseCase = networkConnectivityUseCase self.fetchHeatmapActivityTypesUseCase = fetchHeatmapActivityTypesUseCase self.updateHeatmapActivityTypesUseCase = updateHeatmapActivityTypesUseCase + self.widgetCoordinator = HeatmapWidgetSyncCoordinator( + fetchTodosUseCase: fetchTodosUseCase + ) setupNetworkObserving() } @@ -185,6 +189,7 @@ final class ProfileViewModel: Store { state.showDoneButton = focused } if self.state != state { self.state = state } + coordinateHeatmapWidgetSyncIfNeeded(for: action) return effects } // swiftlint:enable cyclomatic_complexity @@ -329,6 +334,19 @@ extension ProfileViewModel { func isQuarterSelectedForPicker(_ quarter: Int) -> Bool { quarterStartForPicker(quarter: quarter) == state.selectedQuarterStart } + + func coordinateHeatmapWidgetSyncIfNeeded(for action: Action) { + switch action { + case .onAppear, .refresh, .toggleActivityKind: + Task { + await widgetCoordinator.sync( + selectedActivityKinds: state.selectedActivityKinds + ) + } + default: + break + } + } } private extension ProfileViewModel { diff --git a/DevLog/Widget/Heatmap/HeatmapWidgetSyncCoordinator.swift b/DevLog/Widget/Heatmap/HeatmapWidgetSyncCoordinator.swift new file mode 100644 index 00000000..236df845 --- /dev/null +++ b/DevLog/Widget/Heatmap/HeatmapWidgetSyncCoordinator.swift @@ -0,0 +1,98 @@ +// +// HeatmapWidgetSyncCoordinator.swift +// DevLog +// +// Created by opfic on 4/17/26. +// + +import Foundation +import WidgetKit + +final class HeatmapWidgetSyncCoordinator { + private let fetchTodosUseCase: FetchTodosUseCase + private let factory: HeatmapWidgetSnapshotFactory + private let store: WidgetSnapshotStore + private let calendar: Calendar + + init( + fetchTodosUseCase: FetchTodosUseCase, + factory: HeatmapWidgetSnapshotFactory = .init(), + store: WidgetSnapshotStore = .init(), + calendar: Calendar = .current + ) { + self.fetchTodosUseCase = fetchTodosUseCase + self.factory = factory + self.store = store + self.calendar = calendar + } + + func sync( + selectedActivityKinds: Set, + now: Date = Date() + ) async { + let monthStart = startOfMonth(for: now) + guard let nextMonthStart = calendar.date(byAdding: .month, value: 1, to: monthStart) else { + return + } + + do { + async let createdTodoPage = fetchTodosUseCase.execute( + TodoQuery( + sortDateFrom: monthStart, + sortDateTo: nextMonthStart, + includesDeleted: true, + sortTarget: .createdAt, + pageSize: 100, + fetchAllPages: true + ), + cursor: nil + ) + async let completedTodoPage = fetchTodosUseCase.execute( + TodoQuery( + sortDateFrom: monthStart, + sortDateTo: nextMonthStart, + includesDeleted: true, + sortTarget: .completedAt, + pageSize: 100, + fetchAllPages: true + ), + cursor: nil + ) + async let deletedTodoPage = fetchTodosUseCase.execute( + TodoQuery( + sortDateFrom: monthStart, + sortDateTo: nextMonthStart, + includesDeleted: true, + sortTarget: .deletedAt, + pageSize: 100, + fetchAllPages: true + ), + cursor: nil + ) + + let snapshot = factory.makeSnapshot( + createdTodos: try await createdTodoPage.items, + completedTodos: try await completedTodoPage.items, + deletedTodos: try await deletedTodoPage.items, + selectedActivityKinds: selectedActivityKinds, + monthStart: monthStart, + now: now + ) + + try store.saveHeatmapSnapshot(snapshot) + WidgetCenter.shared.reloadTimelines(ofKind: "HeatmapWidget") + } catch { + return + } + } +} + +private extension HeatmapWidgetSyncCoordinator { + func startOfMonth(for date: Date) -> Date { + guard let monthInterval = calendar.dateInterval(of: .month, for: date) else { + return calendar.startOfDay(for: date) + } + + return monthInterval.start + } +} From 622336dcd5f17f15f901aa1978bf54584fbf3bef Mon Sep 17 00:00:00 2001 From: opficdev Date: Fri, 17 Apr 2026 17:34:56 +0900 Subject: [PATCH 14/17] =?UTF-8?q?fix:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EA=B8=B0=EB=8C=80=EA=B0=92=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog_Unit/Widget/TodayWidgetSnapshotFactoryTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DevLog_Unit/Widget/TodayWidgetSnapshotFactoryTests.swift b/DevLog_Unit/Widget/TodayWidgetSnapshotFactoryTests.swift index c8452219..c936114b 100644 --- a/DevLog_Unit/Widget/TodayWidgetSnapshotFactoryTests.swift +++ b/DevLog_Unit/Widget/TodayWidgetSnapshotFactoryTests.swift @@ -25,7 +25,7 @@ struct TodayWidgetSnapshotFactoryTests { #expect(snapshot.totalCount == 5) #expect(snapshot.focusedCount == 1) #expect(snapshot.overdueCount == 1) - #expect(snapshot.dueSoonCount == 1) + #expect(snapshot.dueSoonCount == 2) #expect(snapshot.sections.map(\.category) == ["focused", "overdue", "dueSoon", "later", "unscheduled"]) #expect(snapshot.sections[0].items.map(\.title) == ["๊ณ ์ •๋œ ํ•  ์ผ"]) #expect(snapshot.sections[1].items.map(\.title) == ["์ง€๋‚œ ์ผ์ •"]) @@ -52,7 +52,7 @@ struct TodayWidgetSnapshotFactoryTests { #expect(snapshot.totalCount == 1) #expect(snapshot.focusedCount == 1) #expect(snapshot.overdueCount == 0) - #expect(snapshot.dueSoonCount == 0) + #expect(snapshot.dueSoonCount == 1) #expect(snapshot.sections.map(\.category) == ["focused"]) #expect(snapshot.sections[0].items.map(\.title) == ["๊ณ ์ •๋œ ํ•  ์ผ"]) } From fb556d8f703ffb31df8a3cfa97d5bcb40a89be3e Mon Sep 17 00:00:00 2001 From: opficdev Date: Fri, 17 Apr 2026 17:57:41 +0900 Subject: [PATCH 15/17] =?UTF-8?q?refactor:=20=EC=9C=84=EC=A0=AF=20?= =?UTF-8?q?=EB=8F=99=EA=B8=B0=ED=99=94=20SideEffect=20=EC=B2=98=EB=A6=AC?= =?UTF-8?q?=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewModel/ProfileViewModel.swift | 27 +++++++---------- .../ViewModel/TodayViewModel.swift | 29 ++++++++----------- 2 files changed, 23 insertions(+), 33 deletions(-) diff --git a/DevLog/Presentation/ViewModel/ProfileViewModel.swift b/DevLog/Presentation/ViewModel/ProfileViewModel.swift index 59bc6b59..197bf429 100644 --- a/DevLog/Presentation/ViewModel/ProfileViewModel.swift +++ b/DevLog/Presentation/ViewModel/ProfileViewModel.swift @@ -61,6 +61,7 @@ final class ProfileViewModel: Store { case fetchActivityQuarter(Date) case updateStatusMessage(String) case updateHeatmapActivityKinds(Set) + case syncHeatmapWidget } private(set) var state = State() @@ -73,6 +74,7 @@ final class ProfileViewModel: Store { private let widgetCoordinator: HeatmapWidgetSyncCoordinator private let calendar = Calendar.current private let loadingState = LoadingState() + private var syncHeatmapWidgetTask: Task? private var cancellables = Set() init( @@ -105,7 +107,7 @@ final class ProfileViewModel: Store { guard let quarterStart = quarterStart(for: Date()) else { break } state.selectedQuarterStart = quarterStart } - effects = [.fetchUserData] + effects = [.fetchUserData, .syncHeatmapWidget] let rawValues = fetchHeatmapActivityTypesUseCase.execute() let settings = normalizeActivityKinds(rawValues) if !settings.isEmpty { @@ -178,7 +180,7 @@ final class ProfileViewModel: Store { } else { state.selectedActivityKinds.insert(activityKind) } - effects = [.updateHeatmapActivityKinds(state.selectedActivityKinds)] + effects = [.updateHeatmapActivityKinds(state.selectedActivityKinds), .syncHeatmapWidget] case .willUpdateStatusMessage: if !state.isNetworkConnected { break } let message = self.state.statusMessage @@ -189,7 +191,6 @@ final class ProfileViewModel: Store { state.showDoneButton = focused } if self.state != state { self.state = state } - coordinateHeatmapWidgetSyncIfNeeded(for: action) return effects } // swiftlint:enable cyclomatic_complexity @@ -240,6 +241,13 @@ final class ProfileViewModel: Store { return activityKinds.contains(activityKind) } updateHeatmapActivityTypesUseCase.execute(rawValues) + case .syncHeatmapWidget: + syncHeatmapWidgetTask?.cancel() + syncHeatmapWidgetTask = Task { [selectedActivityKinds = state.selectedActivityKinds] in + await widgetCoordinator.sync( + selectedActivityKinds: selectedActivityKinds + ) + } } } } @@ -334,19 +342,6 @@ extension ProfileViewModel { func isQuarterSelectedForPicker(_ quarter: Int) -> Bool { quarterStartForPicker(quarter: quarter) == state.selectedQuarterStart } - - func coordinateHeatmapWidgetSyncIfNeeded(for action: Action) { - switch action { - case .onAppear, .refresh, .toggleActivityKind: - Task { - await widgetCoordinator.sync( - selectedActivityKinds: state.selectedActivityKinds - ) - } - default: - break - } - } } private extension ProfileViewModel { diff --git a/DevLog/Presentation/ViewModel/TodayViewModel.swift b/DevLog/Presentation/ViewModel/TodayViewModel.swift index d5ef9823..515a7ac5 100644 --- a/DevLog/Presentation/ViewModel/TodayViewModel.swift +++ b/DevLog/Presentation/ViewModel/TodayViewModel.swift @@ -71,6 +71,7 @@ final class TodayViewModel: Store { case fetchTodos case completeTodo(TodayTodoItem) case togglePinned(TodayTodoItem) + case syncTodayWidget } private(set) var state = State() @@ -184,7 +185,6 @@ final class TodayViewModel: Store { } if self.state != state { self.state = state } - widgetSyncIfNeeded(for: action) return effects } @@ -258,6 +258,11 @@ final class TodayViewModel: Store { send(.setAlert(true)) } } + case .syncTodayWidget: + widgetCoordinator.sync( + todos: state.todos, + displayOptions: state.displayOptions + ) } } } @@ -287,12 +292,15 @@ private extension TodayViewModel { case .setDueDateVisibility(let visibility): state.displayOptions.dueDateVisibility = visibility updateTodayDisplayOptionsUseCase.execute(state.displayOptions) + return [.syncTodayWidget] case .setFocusVisibility(let visibility): state.displayOptions.focusVisibility = visibility updateTodayDisplayOptionsUseCase.execute(state.displayOptions) + return [.syncTodayWidget] case .resetDisplayOptions: state.displayOptions = .default updateTodayDisplayOptionsUseCase.execute(state.displayOptions) + return [.syncTodayWidget] case .completeTodo(let item): return [.completeTodo(item)] case .togglePinned(let item): @@ -317,6 +325,7 @@ private extension TodayViewModel { switch action { case .fetchTodos(let items): state.todos = items + return [.syncTodayWidget] case .setLoading(let isLoading): state.isLoading = isLoading case .updateTodo(let item): @@ -325,8 +334,10 @@ private extension TodayViewModel { } else { state.todos.append(item) } + return [.syncTodayWidget] case .removeTodo(let todoId): state.todos.removeAll { $0.id == todoId } + return [.syncTodayWidget] default: break } @@ -425,20 +436,4 @@ private extension TodayViewModel { return startOfToday <= dueDay && dueDay <= windowEnd } - func widgetSyncIfNeeded(for action: Action) { - switch action { - case .setDueDateVisibility, - .setFocusVisibility, - .resetDisplayOptions, - .fetchTodos, - .updateTodo, - .removeTodo: - widgetCoordinator.sync( - todos: state.todos, - displayOptions: state.displayOptions - ) - default: - break - } - } } From 86cc7a20b075fbcc237d74fcf41450c0b1cb94e1 Mon Sep 17 00:00:00 2001 From: opficdev Date: Fri, 17 Apr 2026 18:02:36 +0900 Subject: [PATCH 16/17] =?UTF-8?q?refactor:=20=EB=8F=99=EC=9D=BC=20identifi?= =?UTF-8?q?er=EC=9D=84=20=ED=95=9C=20=ED=8C=8C=EC=9D=BC=EB=A1=9C=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog.xcodeproj/project.pbxproj | 10 +++++++++- DevLogWidget/Common/WidgetAppGroup.swift | 12 ------------ .../Common => WidgetShared}/WidgetAppGroup.swift | 0 3 files changed, 9 insertions(+), 13 deletions(-) delete mode 100644 DevLogWidget/Common/WidgetAppGroup.swift rename {DevLog/Widget/Common => WidgetShared}/WidgetAppGroup.swift (100%) diff --git a/DevLog.xcodeproj/project.pbxproj b/DevLog.xcodeproj/project.pbxproj index 8b9b7299..dcb2743b 100644 --- a/DevLog.xcodeproj/project.pbxproj +++ b/DevLog.xcodeproj/project.pbxproj @@ -105,6 +105,11 @@ path = DevLogWidget; sourceTree = ""; }; + FB02A8C62F900000001DA7CD /* WidgetShared */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = WidgetShared; + sourceTree = ""; + }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -147,11 +152,12 @@ DFD48AF72DC4D6E2005905C5 = { isa = PBXGroup; children = ( + DFD74E2E2E423EA700613803 /* README.md */, DFD6453F2EC827A10073E133 /* .gitignore */, DF8AB7982E938B0B00E50BBF /* DevLog */, DF34164A2E45F67C00F9312B /* DevLog_Unit */, - DFD74E2E2E423EA700613803 /* README.md */, DFD3A9752F8E89DD001DA7CD /* DevLogWidget */, + FB02A8C62F900000001DA7CD /* WidgetShared */, DFE28EB62DCCF26300B28FE5 /* Frameworks */, DFD48B012DC4D6E2005905C5 /* Products */, ); @@ -216,6 +222,7 @@ ); fileSystemSynchronizedGroups = ( DFD3A9752F8E89DD001DA7CD /* DevLogWidget */, + FB02A8C62F900000001DA7CD /* WidgetShared */, ); name = DevLogWidgetExtension; packageProductDependencies = ( @@ -241,6 +248,7 @@ ); fileSystemSynchronizedGroups = ( DF8AB7982E938B0B00E50BBF /* DevLog */, + FB02A8C62F900000001DA7CD /* WidgetShared */, ); name = DevLog; packageProductDependencies = ( diff --git a/DevLogWidget/Common/WidgetAppGroup.swift b/DevLogWidget/Common/WidgetAppGroup.swift deleted file mode 100644 index aff00e78..00000000 --- a/DevLogWidget/Common/WidgetAppGroup.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// WidgetAppGroup.swift -// DevLogWidget -// -// Created by opfic on 4/15/26. -// - -import Foundation - -enum WidgetAppGroup { - static let identifier = "group.opfic.DevLog" -} diff --git a/DevLog/Widget/Common/WidgetAppGroup.swift b/WidgetShared/WidgetAppGroup.swift similarity index 100% rename from DevLog/Widget/Common/WidgetAppGroup.swift rename to WidgetShared/WidgetAppGroup.swift From 2436aacbccef3d13297a323dd1fdd904a3da9b76 Mon Sep 17 00:00:00 2001 From: opficdev Date: Fri, 17 Apr 2026 20:16:09 +0900 Subject: [PATCH 17/17] =?UTF-8?q?feat:=20Logger=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/Widget/Heatmap/HeatmapWidgetSyncCoordinator.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/DevLog/Widget/Heatmap/HeatmapWidgetSyncCoordinator.swift b/DevLog/Widget/Heatmap/HeatmapWidgetSyncCoordinator.swift index 236df845..0351815e 100644 --- a/DevLog/Widget/Heatmap/HeatmapWidgetSyncCoordinator.swift +++ b/DevLog/Widget/Heatmap/HeatmapWidgetSyncCoordinator.swift @@ -13,6 +13,7 @@ final class HeatmapWidgetSyncCoordinator { private let factory: HeatmapWidgetSnapshotFactory private let store: WidgetSnapshotStore private let calendar: Calendar + private let logger = Logger(category: "HeatmapWidgetSyncCoordinator") init( fetchTodosUseCase: FetchTodosUseCase, @@ -81,8 +82,13 @@ final class HeatmapWidgetSyncCoordinator { try store.saveHeatmapSnapshot(snapshot) WidgetCenter.shared.reloadTimelines(ofKind: "HeatmapWidget") + } catch is CancellationError { + logger.debug("Heatmap widget sync cancelled.") } catch { - return + logger.error( + "Failed to sync heatmap widget snapshot.", + error: error + ) } } }