diff --git a/Keychy/Keychy.xcodeproj/project.pbxproj b/Keychy/Keychy.xcodeproj/project.pbxproj index 68840258c..fd987f4c0 100644 --- a/Keychy/Keychy.xcodeproj/project.pbxproj +++ b/Keychy/Keychy.xcodeproj/project.pbxproj @@ -402,9 +402,9 @@ C6B56F342EC061BB0049F969 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C6B56F332EC061BB0049F969 /* WidgetKit.framework */; }; C6B56F362EC061BB0049F969 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C6B56F352EC061BB0049F969 /* SwiftUI.framework */; }; C6B56F472EC061BC0049F969 /* WidgetKeychyExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = C6B56F322EC061BB0049F969 /* WidgetKeychyExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - C6B56F602EC08BCF0049F969 /* AvailableKeyring.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6B56F5F2EC08BCF0049F969 /* AvailableKeyring.swift */; }; + C6B56F602EC08BCF0049F969 /* WidgetKeyring.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6B56F5F2EC08BCF0049F969 /* WidgetKeyring.swift */; }; C6B56F612EC08CCE0049F969 /* KeyringImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6B56F212EC0341B0049F969 /* KeyringImageCache.swift */; }; - C6B56F702EC08ED40049F969 /* AvailableKeyring.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6B56F5F2EC08BCF0049F969 /* AvailableKeyring.swift */; }; + C6B56F702EC08ED40049F969 /* WidgetKeyring.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6B56F5F2EC08BCF0049F969 /* WidgetKeyring.swift */; }; C6B5707B2EC2036C0049F969 /* MultiKeyringCaptureScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6B5707A2EC2036C0049F969 /* MultiKeyringCaptureScene.swift */; }; C6B5707F2EC206CD0049F969 /* BundleImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6B5707E2EC206CD0049F969 /* BundleImageCache.swift */; }; C6B571062EC2337C0049F969 /* MultiKeyringCaptureScene+Capture.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6B571052EC2337C0049F969 /* MultiKeyringCaptureScene+Capture.swift */; }; @@ -854,7 +854,7 @@ C6B56F352EC061BB0049F969 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; C6B56F4D2EC0681C0049F969 /* KeychyRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = KeychyRelease.entitlements; sourceTree = ""; }; C6B56F5C2EC06B310049F969 /* WidgetKeychyExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetKeychyExtension.entitlements; sourceTree = ""; }; - C6B56F5F2EC08BCF0049F969 /* AvailableKeyring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvailableKeyring.swift; sourceTree = ""; }; + C6B56F5F2EC08BCF0049F969 /* WidgetKeyring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetKeyring.swift; sourceTree = ""; }; C6B5707A2EC2036C0049F969 /* MultiKeyringCaptureScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiKeyringCaptureScene.swift; sourceTree = ""; }; C6B5707E2EC206CD0049F969 /* BundleImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleImageCache.swift; sourceTree = ""; }; C6B571052EC2337C0049F969 /* MultiKeyringCaptureScene+Capture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MultiKeyringCaptureScene+Capture.swift"; sourceTree = ""; }; @@ -1747,7 +1747,7 @@ isa = PBXGroup; children = ( 4CEC61DF2EAE08C00099ECEE /* Keyring.swift */, - C6B56F5F2EC08BCF0049F969 /* AvailableKeyring.swift */, + C6B56F5F2EC08BCF0049F969 /* WidgetKeyring.swift */, 4CEC61E02EAE08C00099ECEE /* RingType.swift */, 4CEC61E12EAE08C00099ECEE /* ChainType.swift */, 4CEC61E22EAE08C00099ECEE /* BodyType.swift */, @@ -2513,7 +2513,7 @@ 382800D32EC0628D005F1332 /* CollectionViewModel+Package.swift in Sources */, 38C3C28E2EC1F56B003C5DE1 /* CollectionKeyringDetailView+Sheet.swift in Sources */, 4C6530462EBA80DA000F8154 /* PurchaseFailAlert.swift in Sources */, - C6B56F602EC08BCF0049F969 /* AvailableKeyring.swift in Sources */, + C6B56F602EC08BCF0049F969 /* WidgetKeyring.swift in Sources */, 4CEC622A2EAE08DA0099ECEE /* Font+Custom.swift in Sources */, AA2146B72F15E5B60048D40E /* BundleEditView+SelectSheet.swift in Sources */, 389080172ED3F05D00D7A49F /* FestivalKeyringDetailView.swift in Sources */, @@ -2777,7 +2777,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - C6B56F702EC08ED40049F969 /* AvailableKeyring.swift in Sources */, + C6B56F702EC08ED40049F969 /* WidgetKeyring.swift in Sources */, C6B56F612EC08CCE0049F969 /* KeyringImageCache.swift in Sources */, 4CC8D0192EF0395F00317467 /* AppIntent.swift in Sources */, 4CC8D01A2EF0395F00317467 /* WidgetConfiguration.mm in Sources */, diff --git a/Keychy/Keychy/CommonModels/Keyring/AvailableKeyring.swift b/Keychy/Keychy/CommonModels/Keyring/AvailableKeyring.swift deleted file mode 100644 index 00dd27246..000000000 --- a/Keychy/Keychy/CommonModels/Keyring/AvailableKeyring.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// AvailableKeyring.swift -// Keychy -// -// Created by Rundo on 11/9/25. -// - -import Foundation - -/// 위젯에서 사용할 키링 메타데이터 -struct AvailableKeyring: Codable, Identifiable, Hashable { - let id: String // Firestore documentId - let name: String // 키링 이름 - let imagePath: String // App Group Container 내 이미지 경로 (파일명만 저장) -} diff --git a/Keychy/Keychy/CommonModels/Keyring/WidgetKeyring.swift b/Keychy/Keychy/CommonModels/Keyring/WidgetKeyring.swift new file mode 100644 index 000000000..fc9fce84f --- /dev/null +++ b/Keychy/Keychy/CommonModels/Keyring/WidgetKeyring.swift @@ -0,0 +1,32 @@ +// +// WidgetKeyring.swift +// Keychy +// +// Created by Rundo on 11/9/25. +// + +import Foundation + +/// 위젯에서 사용할 키링 메타데이터 +struct WidgetKeyring: Codable, Identifiable, Hashable { + let id: String // Firestore documentId + let name: String // 키링 이름 + let imagePath: String // App Group 내 이미지 경로 + let createdAt: Date // 생성일 (위젯 목록 정렬용) + + // 기존 데이터 호환용 (createdAt 없는 경우 .distantPast로 처리) + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(String.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + imagePath = try container.decode(String.self, forKey: .imagePath) + createdAt = try container.decodeIfPresent(Date.self, forKey: .createdAt) ?? .distantPast + } + + init(id: String, name: String, imagePath: String, createdAt: Date) { + self.id = id + self.name = name + self.imagePath = imagePath + self.createdAt = createdAt + } +} diff --git a/Keychy/Keychy/Core/Cache/KeyringCacheManager.swift b/Keychy/Keychy/Core/Cache/KeyringCacheManager.swift index 61eb828fd..9ee1bdbe3 100644 --- a/Keychy/Keychy/Core/Cache/KeyringCacheManager.swift +++ b/Keychy/Keychy/Core/Cache/KeyringCacheManager.swift @@ -224,7 +224,8 @@ class KeyringCacheManager { KeyringImageCache.shared.syncKeyring( id: keyringID, name: keyring.name, - imageData: pngData + imageData: pngData, + createdAt: keyring.createdAt ) } diff --git a/Keychy/Keychy/Core/Cache/KeyringImageCache.swift b/Keychy/Keychy/Core/Cache/KeyringImageCache.swift index 589bde42d..d381810d2 100644 --- a/Keychy/Keychy/Core/Cache/KeyringImageCache.swift +++ b/Keychy/Keychy/Core/Cache/KeyringImageCache.swift @@ -235,10 +235,10 @@ class KeyringImageCache { print("📋 [KeyringCache] =====================================") } - // MARK: - 메타데이터 관리 (위젯용) + // MARK: - 위젯 메타데이터 관리 - /// 사용 가능한 키링 목록 저장 - func saveAvailableKeyrings(_ keyrings: [AvailableKeyring]) { + /// 위젯용 키링 목록 저장 + func saveWidgetKeyrings(_ keyrings: [WidgetKeyring]) { guard let fileURL = metadataFileURL else { print("❌ [KeyringCache] 메타데이터 파일 URL을 찾을 수 없습니다.") return @@ -254,8 +254,8 @@ class KeyringImageCache { } } - /// 사용 가능한 키링 목록 로드 - func loadAvailableKeyrings() -> [AvailableKeyring] { + /// 위젯용 키링 목록 로드 + func loadWidgetKeyrings() -> [WidgetKeyring] { guard let fileURL = metadataFileURL else { print("❌ [KeyringCache] 메타데이터 파일 URL을 찾을 수 없습니다.") return [] @@ -269,7 +269,7 @@ class KeyringImageCache { do { let data = try Data(contentsOf: fileURL) let decoder = JSONDecoder() - let keyrings = try decoder.decode([AvailableKeyring].self, from: data) + let keyrings = try decoder.decode([WidgetKeyring].self, from: data) return keyrings } catch { print("❌ [KeyringCache] 메타데이터 로드 실패: \(error.localizedDescription)") @@ -280,23 +280,23 @@ class KeyringImageCache { // MARK: - 동기화 메서드 /// 키링 추가 또는 업데이트 (이미지 + 메타데이터) - func syncKeyring(id: String, name: String, imageData: Data) { + func syncKeyring(id: String, name: String, imageData: Data, createdAt: Date) { // 1. 이미지 저장 save(pngData: imageData, for: id, type: .thumbnail) // 2. 메타데이터 업데이트 - var keyrings = loadAvailableKeyrings() + var keyrings = loadWidgetKeyrings() let imagePath = "\(id)_thumb.png" if let index = keyrings.firstIndex(where: { $0.id == id }) { // 기존 키링 업데이트 - keyrings[index] = AvailableKeyring(id: id, name: name, imagePath: imagePath) + keyrings[index] = WidgetKeyring(id: id, name: name, imagePath: imagePath, createdAt: createdAt) } else { // 새 키링 추가 - keyrings.append(AvailableKeyring(id: id, name: name, imagePath: imagePath)) + keyrings.append(WidgetKeyring(id: id, name: name, imagePath: imagePath, createdAt: createdAt)) } - saveAvailableKeyrings(keyrings) + saveWidgetKeyrings(keyrings) // 3. 위젯 타임라인 새로고침 reloadWidgets() @@ -308,9 +308,9 @@ class KeyringImageCache { delete(for: id, type: .thumbnail) // 2. 메타데이터에서 제거 - var keyrings = loadAvailableKeyrings() + var keyrings = loadWidgetKeyrings() keyrings.removeAll { $0.id == id } - saveAvailableKeyrings(keyrings) + saveWidgetKeyrings(keyrings) print("✅ [KeyringCache] 키링 완전 삭제: \(id)") @@ -337,9 +337,47 @@ class KeyringImageCache { // MARK: - 위젯 업데이트 - /// 위젯 타임라인 새로고침 private func reloadWidgets() { WidgetCenter.shared.reloadTimelines(ofKind: widgetKind) - print("🔄 [KeyringCache] 위젯 타임라인 새로고침 요청") + } + + // MARK: - 마이그레이션 + + private let migrationVersionKey = "widgetCacheMigrationVersion" + private let currentMigrationVersion = 1 + + /// 위젯 키링 데이터 마이그레이션 (한 번만 실행) + /// - 삭제된 키링 정리 + /// - createdAt 누락된 키링 업데이트 + func migrateWidgetKeyringsIfNeeded(with keyringDates: [String: Date]) { + let lastVersion = UserDefaults.standard.integer(forKey: migrationVersionKey) + guard lastVersion < currentMigrationVersion else { return } + + let originalKeyrings = loadWidgetKeyrings() + + let migratedKeyrings = originalKeyrings.compactMap { widgetKeyring -> WidgetKeyring? in + guard let actualCreatedAt = keyringDates[widgetKeyring.id] else { + delete(for: widgetKeyring.id, type: .thumbnail) + return nil + } + + if widgetKeyring.createdAt == .distantPast { + return WidgetKeyring( + id: widgetKeyring.id, + name: widgetKeyring.name, + imagePath: widgetKeyring.imagePath, + createdAt: actualCreatedAt + ) + } + + return widgetKeyring + } + + if migratedKeyrings != originalKeyrings { + saveWidgetKeyrings(migratedKeyrings) + reloadWidgets() + } + + UserDefaults.standard.set(currentMigrationVersion, forKey: migrationVersionKey) } } diff --git a/Keychy/Keychy/Presentation/Collection/ViewModels/CollectionViewModel+Edit.swift b/Keychy/Keychy/Presentation/Collection/ViewModels/CollectionViewModel+Edit.swift index 3d428ea7b..7278bd6d5 100644 --- a/Keychy/Keychy/Presentation/Collection/ViewModels/CollectionViewModel+Edit.swift +++ b/Keychy/Keychy/Presentation/Collection/ViewModels/CollectionViewModel+Edit.swift @@ -50,14 +50,16 @@ extension CollectionViewModel { // 이름이 변경된 경우 App Group 메타데이터 업데이트 if keyring.name != name { - var keyrings = KeyringImageCache.shared.loadAvailableKeyrings() - if let keyringIndex = keyrings.firstIndex(where: { $0.id == documentId }) { - keyrings[keyringIndex] = AvailableKeyring( + var widgetKeyrings = KeyringImageCache.shared.loadWidgetKeyrings() + if let keyringIndex = widgetKeyrings.firstIndex(where: { $0.id == documentId }) { + let existing = widgetKeyrings[keyringIndex] + widgetKeyrings[keyringIndex] = WidgetKeyring( id: documentId, name: name, - imagePath: keyrings[keyringIndex].imagePath + imagePath: existing.imagePath, + createdAt: existing.createdAt ) - KeyringImageCache.shared.saveAvailableKeyrings(keyrings) + KeyringImageCache.shared.saveWidgetKeyrings(widgetKeyrings) // 위젯 타임라인 새로고침 WidgetCenter.shared.reloadTimelines(ofKind: "WidgetKeychy") diff --git a/Keychy/Keychy/Presentation/Collection/ViewModels/CollectionViewModel+LoadData.swift b/Keychy/Keychy/Presentation/Collection/ViewModels/CollectionViewModel+LoadData.swift index 2bb6b24fc..3dd3e3c7f 100644 --- a/Keychy/Keychy/Presentation/Collection/ViewModels/CollectionViewModel+LoadData.swift +++ b/Keychy/Keychy/Presentation/Collection/ViewModels/CollectionViewModel+LoadData.swift @@ -116,9 +116,17 @@ extension CollectionViewModel { dispatchGroup.notify(queue: .main) { [weak self] in guard let self = self else { return } - + self.keyring = allKeyrings - + + // 위젯 캐시 마이그레이션 (최초 1회만 실행) + let keyringDates = Dictionary( + uniqueKeysWithValues: allKeyrings + .filter { !$0.isPackaged && !$0.isPublished } + .map { ($0.id.uuidString, $0.createdAt) } + ) + KeyringImageCache.shared.migrateWidgetKeyringsIfNeeded(with: keyringDates) + completion(true) } } @@ -440,7 +448,8 @@ extension CollectionViewModel { KeyringImageCache.shared.syncKeyring( id: keyringID, name: keyring.name, - imageData: pngData + imageData: pngData, + createdAt: keyring.createdAt ) } } diff --git a/Keychy/Keychy/Presentation/Collection/Views/CachedImagesDebugView.swift b/Keychy/Keychy/Presentation/Collection/Views/CachedImagesDebugView.swift index 22482b428..8533dcc6c 100644 --- a/Keychy/Keychy/Presentation/Collection/Views/CachedImagesDebugView.swift +++ b/Keychy/Keychy/Presentation/Collection/Views/CachedImagesDebugView.swift @@ -149,12 +149,11 @@ struct CachedImagesDebugView: View { // MARK: - Load Cached Images private func loadCachedImages() { - // App Group의 메타데이터 로드 - let availableKeyrings = KeyringImageCache.shared.loadAvailableKeyrings() + let widgetKeyrings = KeyringImageCache.shared.loadWidgetKeyrings() var loadedImages: [(id: String, name: String, image: Image, size: String)] = [] - for keyring in availableKeyrings { + for keyring in widgetKeyrings { // 이미지 데이터 로드 if let imageData = KeyringImageCache.shared.loadImageByPath(keyring.imagePath), let uiImage = UIImage(data: imageData) { @@ -183,10 +182,9 @@ struct CachedImagesDebugView: View { // MARK: - Clear All Cache private func clearAllCache() { - // 모든 키링 메타데이터 삭제 - let keyrings = KeyringImageCache.shared.loadAvailableKeyrings() - for keyring in keyrings { + let widgetKeyrings = KeyringImageCache.shared.loadWidgetKeyrings() + for keyring in widgetKeyrings { KeyringImageCache.shared.removeKeyring(id: keyring.id) } diff --git a/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionCellView.swift b/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionCellView.swift index e3d32ac01..c5c030ba9 100644 --- a/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionCellView.swift +++ b/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionCellView.swift @@ -170,23 +170,24 @@ struct CollectionCellView: View { // MARK: - 위젯 메타데이터 동기화 private func syncWidgetMetadata(keyringID: String) { - var keyrings = KeyringImageCache.shared.loadAvailableKeyrings() - let isInMetadata = keyrings.contains(where: { $0.id == keyringID }) + var widgetKeyrings = KeyringImageCache.shared.loadWidgetKeyrings() + let isInMetadata = widgetKeyrings.contains(where: { $0.id == keyringID }) let shouldBeInWidget = !keyring.isPackaged && !keyring.isPublished - + if shouldBeInWidget && !isInMetadata { // 위젯에 있어야 하는데 없음 → 추가 if let imageData = KeyringImageCache.shared.load(for: keyringID, type: .thumbnail) { KeyringImageCache.shared.syncKeyring( id: keyringID, name: keyring.name, - imageData: imageData + imageData: imageData, + createdAt: keyring.createdAt ) } } else if !shouldBeInWidget && isInMetadata { // 위젯에 없어야 하는데 있음 → 제거 - keyrings.removeAll { $0.id == keyringID } - KeyringImageCache.shared.saveAvailableKeyrings(keyrings) + widgetKeyrings.removeAll { $0.id == keyringID } + KeyringImageCache.shared.saveWidgetKeyrings(widgetKeyrings) } } @@ -303,7 +304,8 @@ struct CollectionCellView: View { KeyringImageCache.shared.syncKeyring( id: keyringID, name: keyring.name, - imageData: pngData + imageData: pngData, + createdAt: keyring.createdAt ) } else { print("[CollectionCell] 캡처 성공 (위젯 제외): \(keyringID)") diff --git a/Keychy/Keychy/Presentation/Intro/ViewModels/IntroViewModel+WelcomeKeyring.swift b/Keychy/Keychy/Presentation/Intro/ViewModels/IntroViewModel+WelcomeKeyring.swift index 3fa5dff6c..57729e2ed 100644 --- a/Keychy/Keychy/Presentation/Intro/ViewModels/IntroViewModel+WelcomeKeyring.swift +++ b/Keychy/Keychy/Presentation/Intro/ViewModels/IntroViewModel+WelcomeKeyring.swift @@ -145,7 +145,8 @@ extension IntroViewModel { KeyringImageCache.shared.syncKeyring( id: keyringId, name: nickname, - imageData: pngData + imageData: pngData, + createdAt: Date() ) print("[WelcomeKeyring] 위젯 캐싱 완료: \(keyringId)") } else { diff --git a/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringInfoInputView+FirebaseSave.swift b/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringInfoInputView+FirebaseSave.swift index 6a7903338..2f2a9b144 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringInfoInputView+FirebaseSave.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringInfoInputView+FirebaseSave.swift @@ -87,7 +87,8 @@ extension KeyringInfoInputView { ringType: .basic, chainType: .basic, hookOffsetY: hookOffsetY, - chainLength: chainLength + chainLength: chainLength, + createdAt: Date() ) // 모든 작업 완료 후 CompleteView로 이동 @@ -330,7 +331,8 @@ extension KeyringInfoInputView { ringType: RingType, chainType: ChainType, hookOffsetY: CGFloat?, - chainLength: Int + chainLength: Int, + createdAt: Date ) async { await withCheckedContinuation { continuation in // 이미지 로딩 완료 콜백 @@ -385,7 +387,8 @@ extension KeyringInfoInputView { KeyringImageCache.shared.syncKeyring( id: keyringId, name: keyringName, - imageData: pngData + imageData: pngData, + createdAt: createdAt ) } else { diff --git a/Keychy/WidgetKeychy/AppIntent.swift b/Keychy/WidgetKeychy/AppIntent.swift index 240dc8a54..45292b860 100644 --- a/Keychy/WidgetKeychy/AppIntent.swift +++ b/Keychy/WidgetKeychy/AppIntent.swift @@ -2,9 +2,7 @@ // AppIntent.swift // WidgetKeychy // -// 위젯 설정용 AppIntent -// - 위젯 편집 시 키링 선택 UI 제공 -// - App Group에서 사용 가능한 키링 목록 조회 +// 위젯 키링 선택 Intent // import WidgetKit @@ -12,55 +10,51 @@ import AppIntents // MARK: - Keyring Entity -/// 위젯에서 선택 가능한 키링 엔티티 +/// 위젯에서 선택 가능한 키링 struct KeyringEntity: AppEntity { - let id: String // Firestore documentId - let name: String // 키링 이름 + let id: String + let name: String - /// 타입 표시명 (위젯 설정 UI에 "키링" 표시) static var typeDisplayRepresentation: TypeDisplayRepresentation = "키링" - /// 개별 키링 표시 방식 (키링 이름 표시) var displayRepresentation: DisplayRepresentation { DisplayRepresentation(title: "\(name)") } - /// 키링 목록 조회용 쿼리 static var defaultQuery = KeyringEntityQuery() } // MARK: - Keyring Entity Query -/// 위젯에서 선택 가능한 키링 목록을 App Group에서 조회 +/// App Group에서 키링 목록 조회 struct KeyringEntityQuery: EntityQuery { - /// 특정 ID로 키링 찾기 (위젯이 이미 설정된 키링을 복원할 때 사용) + /// ID로 키링 찾기 (위젯 복원 시 사용) func entities(for identifiers: [String]) async throws -> [KeyringEntity] { - let availableKeyrings = KeyringImageCache.shared.loadAvailableKeyrings() - return availableKeyrings + let widgetKeyrings = KeyringImageCache.shared.loadWidgetKeyrings() + return widgetKeyrings .filter { identifiers.contains($0.id) } .map { KeyringEntity(id: $0.id, name: $0.name) } } - /// 위젯 설정 UI에 표시할 키링 목록 (사용자가 선택 가능한 전체 목록) + /// 선택 가능한 키링 목록 (최신순) func suggestedEntities() async throws -> [KeyringEntity] { - let availableKeyrings = KeyringImageCache.shared.loadAvailableKeyrings() - return availableKeyrings.map { KeyringEntity(id: $0.id, name: $0.name) } + let widgetKeyrings = KeyringImageCache.shared.loadWidgetKeyrings() + .sorted { $0.createdAt > $1.createdAt } + return widgetKeyrings.map { KeyringEntity(id: $0.id, name: $0.name) } } - /// 기본 선택 키링 (없음 - 사용자가 직접 선택하도록) func defaultResult() async -> KeyringEntity? { - return nil + nil } } -// MARK: - Configuration Intent +// MARK: - Selection Intent -/// 위젯 설정 인텐트 (위젯 편집 시 표시되는 설정 화면) -struct ConfigurationAppIntent: WidgetConfigurationIntent { +/// 위젯 키링 선택 인텐트 +struct KeyringSelectionIntent: WidgetConfigurationIntent { static var title: LocalizedStringResource { "키링 선택" } static var description: IntentDescription { "위젯에 표시할 키링을 선택하세요" } - /// 사용자가 선택한 키링 (nil이면 플레이스홀더 표시) @Parameter(title: "키링") var selectedKeyring: KeyringEntity? } diff --git a/Keychy/WidgetKeychy/WidgetKeychy.swift b/Keychy/WidgetKeychy/WidgetKeychy.swift index d31302249..fd2f23063 100644 --- a/Keychy/WidgetKeychy/WidgetKeychy.swift +++ b/Keychy/WidgetKeychy/WidgetKeychy.swift @@ -8,30 +8,38 @@ import WidgetKit import SwiftUI -// MARK: - Timeline Provider (AppIntent) -struct Provider: AppIntentTimelineProvider { - func placeholder(in context: Context) -> SimpleEntry { - SimpleEntry(date: Date(), configuration: ConfigurationAppIntent()) +// MARK: - Timeline Provider + +struct KeyringWidgetProvider: AppIntentTimelineProvider { + func placeholder(in context: Context) -> KeyringWidgetEntry { + KeyringWidgetEntry(date: Date(), configuration: KeyringSelectionIntent()) } - func snapshot(for configuration: ConfigurationAppIntent, in context: Context) async -> SimpleEntry { - // 위젯 설정 화면에서는 항상 플레이스홀더 표시 - SimpleEntry(date: Date(), configuration: ConfigurationAppIntent()) + func snapshot(for configuration: KeyringSelectionIntent, in context: Context) async -> KeyringWidgetEntry { + KeyringWidgetEntry(date: Date(), configuration: KeyringSelectionIntent()) } - func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -> Timeline { - let entry = SimpleEntry(date: Date(), configuration: configuration) + func timeline(for configuration: KeyringSelectionIntent, in context: Context) async -> Timeline { + let entry = KeyringWidgetEntry(date: Date(), configuration: configuration) return Timeline(entries: [entry], policy: .never) } } +// MARK: - Widget Entry + +struct KeyringWidgetEntry: TimelineEntry { + let date: Date + let configuration: KeyringSelectionIntent +} + // MARK: - Widget + struct WidgetKeychy: Widget { let kind: String = "WidgetKeychy" var body: some WidgetConfiguration { - AppIntentConfiguration(kind: kind, intent: ConfigurationAppIntent.self, provider: Provider()) { entry in - WidgetKeychyEntryView(entry: entry) + AppIntentConfiguration(kind: kind, intent: KeyringSelectionIntent.self, provider: KeyringWidgetProvider()) { entry in + KeyringWidgetEntryView(entry: entry) .containerBackground(.clear, for: .widget) } .configurationDisplayName("Keychy 위젯") @@ -41,38 +49,34 @@ struct WidgetKeychy: Widget { } } -// MARK: - Entry -struct SimpleEntry: TimelineEntry { - let date: Date - let configuration: ConfigurationAppIntent -} - // MARK: - Entry View -struct WidgetKeychyEntryView: View { - var entry: Provider.Entry + +struct KeyringWidgetEntryView: View { + var entry: KeyringWidgetEntry @Environment(\.widgetFamily) var widgetFamily var body: some View { - if let selectedKeyring = entry.configuration.selectedKeyring, - let imageData = KeyringImageCache.shared.loadImageByPath("\(selectedKeyring.id)_thumb.png"), + if let keyring = entry.configuration.selectedKeyring, + let imageData = KeyringImageCache.shared.loadImageByPath("\(keyring.id)_thumb.png"), let uiImage = UIImage(data: imageData) { - // 선택된 키링 이미지 표시 Image(uiImage: uiImage) .resizable() .scaledToFit() } else { - // 플레이스홀더 - VStack(spacing: 8) { - if widgetFamily == .systemSmall { - Image(.smallPlace) - .resizable() - .scaledToFill() - } else { - Image(.bigPlace) - .resizable() - .scaledToFill() - } - } + placeholderView + } + } + + @ViewBuilder + private var placeholderView: some View { + if widgetFamily == .systemSmall { + Image(.smallPlace) + .resizable() + .scaledToFill() + } else { + Image(.bigPlace) + .resizable() + .scaledToFill() } } }