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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions Keychy/Keychy.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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 = "<group>"; };
C6B56F5C2EC06B310049F969 /* WidgetKeychyExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetKeychyExtension.entitlements; sourceTree = "<group>"; };
C6B56F5F2EC08BCF0049F969 /* AvailableKeyring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvailableKeyring.swift; sourceTree = "<group>"; };
C6B56F5F2EC08BCF0049F969 /* WidgetKeyring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetKeyring.swift; sourceTree = "<group>"; };
C6B5707A2EC2036C0049F969 /* MultiKeyringCaptureScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiKeyringCaptureScene.swift; sourceTree = "<group>"; };
C6B5707E2EC206CD0049F969 /* BundleImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleImageCache.swift; sourceTree = "<group>"; };
C6B571052EC2337C0049F969 /* MultiKeyringCaptureScene+Capture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MultiKeyringCaptureScene+Capture.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
15 changes: 0 additions & 15 deletions Keychy/Keychy/CommonModels/Keyring/AvailableKeyring.swift

This file was deleted.

32 changes: 32 additions & 0 deletions Keychy/Keychy/CommonModels/Keyring/WidgetKeyring.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
3 changes: 2 additions & 1 deletion Keychy/Keychy/Core/Cache/KeyringCacheManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,8 @@ class KeyringCacheManager {
KeyringImageCache.shared.syncKeyring(
id: keyringID,
name: keyring.name,
imageData: pngData
imageData: pngData,
createdAt: keyring.createdAt
)
}

Expand Down
68 changes: 53 additions & 15 deletions Keychy/Keychy/Core/Cache/KeyringImageCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -254,8 +254,8 @@ class KeyringImageCache {
}
}

/// 사용 가능한 키링 목록 로드
func loadAvailableKeyrings() -> [AvailableKeyring] {
/// 위젯용 키링 목록 로드
func loadWidgetKeyrings() -> [WidgetKeyring] {
guard let fileURL = metadataFileURL else {
print("❌ [KeyringCache] 메타데이터 파일 URL을 찾을 수 없습니다.")
return []
Expand All @@ -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)")
Expand All @@ -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()
Expand All @@ -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)")

Expand All @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -440,7 +448,8 @@ extension CollectionViewModel {
KeyringImageCache.shared.syncKeyring(
id: keyringID,
name: keyring.name,
imageData: pngData
imageData: pngData,
createdAt: keyring.createdAt
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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)")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,8 @@ extension IntroViewModel {
KeyringImageCache.shared.syncKeyring(
id: keyringId,
name: nickname,
imageData: pngData
imageData: pngData,
createdAt: Date()
)
print("[WelcomeKeyring] 위젯 캐싱 완료: \(keyringId)")
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ extension KeyringInfoInputView {
ringType: .basic,
chainType: .basic,
hookOffsetY: hookOffsetY,
chainLength: chainLength
chainLength: chainLength,
createdAt: Date()
)

// 모든 작업 완료 후 CompleteView로 이동
Expand Down Expand Up @@ -330,7 +331,8 @@ extension KeyringInfoInputView {
ringType: RingType,
chainType: ChainType,
hookOffsetY: CGFloat?,
chainLength: Int
chainLength: Int,
createdAt: Date
) async {
await withCheckedContinuation { continuation in
// 이미지 로딩 완료 콜백
Expand Down Expand Up @@ -385,7 +387,8 @@ extension KeyringInfoInputView {
KeyringImageCache.shared.syncKeyring(
id: keyringId,
name: keyringName,
imageData: pngData
imageData: pngData,
createdAt: createdAt
)

} else {
Expand Down
Loading