diff --git a/Keychy/Keychy.xcodeproj/project.pbxproj b/Keychy/Keychy.xcodeproj/project.pbxproj index c8b6ccf69..8c3e8cf2f 100644 --- a/Keychy/Keychy.xcodeproj/project.pbxproj +++ b/Keychy/Keychy.xcodeproj/project.pbxproj @@ -241,6 +241,8 @@ 4C77753F2EB1343600981C3E /* IntroViewModel+Login.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C77753A2EB1343600981C3E /* IntroViewModel+Login.swift */; }; 4C7775402EB1343600981C3E /* IntroViewModel+Signup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C77753C2EB1343600981C3E /* IntroViewModel+Signup.swift */; }; 4C7775412EB1343600981C3E /* IntroViewModel+NicknameSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C77753B2EB1343600981C3E /* IntroViewModel+NicknameSetup.swift */; }; + 4C7A9EC72F2B0567008B520C /* KeyringPackageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7A9EC62F2B0567008B520C /* KeyringPackageManager.swift */; }; + 4C7A9EC92F2B0586008B520C /* KeyringPackageCompleteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7A9EC82F2B0586008B520C /* KeyringPackageCompleteView.swift */; }; 4C8426602ED3585A0050B6FE /* gulimche-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 4C84265F2ED3585A0050B6FE /* gulimche-Regular.ttf */; }; 4C8426642ED375840050B6FE /* ColorPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8426632ED375840050B6FE /* ColorPalette.swift */; }; 4C84A1602EB134BD008FFE57 /* ProfileSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A596832EAFEAA20003D712 /* ProfileSetupView.swift */; }; @@ -687,6 +689,8 @@ 4C77753A2EB1343600981C3E /* IntroViewModel+Login.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IntroViewModel+Login.swift"; sourceTree = ""; }; 4C77753B2EB1343600981C3E /* IntroViewModel+NicknameSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IntroViewModel+NicknameSetup.swift"; sourceTree = ""; }; 4C77753C2EB1343600981C3E /* IntroViewModel+Signup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IntroViewModel+Signup.swift"; sourceTree = ""; }; + 4C7A9EC62F2B0567008B520C /* KeyringPackageManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyringPackageManager.swift; sourceTree = ""; }; + 4C7A9EC82F2B0586008B520C /* KeyringPackageCompleteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyringPackageCompleteView.swift; sourceTree = ""; }; 4C84265F2ED3585A0050B6FE /* gulimche-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = text; path = "gulimche-Regular.ttf"; sourceTree = ""; }; 4C8426632ED375840050B6FE /* ColorPalette.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPalette.swift; sourceTree = ""; }; 4C86A6112F25C0B10023AA2D /* WorkshopBundleGridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkshopBundleGridView.swift; sourceTree = ""; }; @@ -938,6 +942,7 @@ C645AEA22EB1B8FC004BFE69 /* DataInitializer.swift */, C6830F032EB8A4000059379A /* WorkshopDataManager.swift */, 4C07024A2ECF10760026D6DC /* EffectSyncManager.swift */, + 4C7A9EC62F2B0567008B520C /* KeyringPackageManager.swift */, ); path = Firebase; sourceTree = ""; @@ -1207,6 +1212,7 @@ 4C47333F2F1FA388005D2376 /* KeyringCompleteView+ReviewCheck.swift */, 4C4733402F1FA388005D2376 /* KeyringCompleteView+SaveImage.swift */, 4C4733412F1FA388005D2376 /* KeyringCompleteView+VideoGen.swift */, + 4C7A9EC82F2B0586008B520C /* KeyringPackageCompleteView.swift */, 4C4733422F1FA388005D2376 /* KeyringCustomizingView.swift */, 4C4733432F1FA388005D2376 /* KeyringCustomizingView+Cart.swift */, 4C4733442F1FA388005D2376 /* KeyringCustomizingView+Purchase.swift */, @@ -2671,6 +2677,7 @@ 3828F54B2EC4D0C500F1B040 /* CollectionView+Handlers.swift in Sources */, 4CF2A96A2F0B94EA00BA9FDA /* View+PullToRefresh.swift in Sources */, 4C86A61F2F29E52D0023AA2D /* PurchaseHistoryView.swift in Sources */, + 4C7A9EC72F2B0567008B520C /* KeyringPackageManager.swift in Sources */, 38C147BB2EB13B2F00A8E511 /* CircleGlassButton.swift in Sources */, AA6298542EC39065001576C0 /* BundleCreateView.swift in Sources */, 38C147C72EB1F57F00A8E511 /* StorageManager.swift in Sources */, @@ -2711,6 +2718,7 @@ 4CC8D01F2EF0447100317467 /* ChangeNameViewModel.swift in Sources */, 4CC8D0202EF0447100317467 /* MyPageViewModel.swift in Sources */, 4C4734072F226B81005D2376 /* WorkshopMakeMenu.swift in Sources */, + 4C7A9EC92F2B0586008B520C /* KeyringPackageCompleteView.swift in Sources */, 38C147C52EB1F16A00A8E511 /* CollectionViewModel+LoadData.swift in Sources */, C6C35F3A2ED2A3C2009642F4 /* FestivalViewModel.swift in Sources */, AAEB46AF2EC1D648002B13E5 /* BundleNameEditView.swift in Sources */, diff --git a/Keychy/Keychy/Core/Components/Keyring/KeyringSceneView.swift b/Keychy/Keychy/Core/Components/Keyring/KeyringSceneView.swift index 98682b63c..f3d1f8af1 100644 --- a/Keychy/Keychy/Core/Components/Keyring/KeyringSceneView.swift +++ b/Keychy/Keychy/Core/Components/Keyring/KeyringSceneView.swift @@ -106,7 +106,6 @@ extension KeyringSceneView { Group { if let scene { SpriteView(scene: scene, options: [.allowsTransparency]) - .ignoresSafeArea() .contentShape(Rectangle()) .frame(maxWidth: .infinity) } diff --git a/Keychy/Keychy/Core/Firebase/KeyringPackageManager.swift b/Keychy/Keychy/Core/Firebase/KeyringPackageManager.swift new file mode 100644 index 000000000..c0511bfc9 --- /dev/null +++ b/Keychy/Keychy/Core/Firebase/KeyringPackageManager.swift @@ -0,0 +1,177 @@ +// +// KeyringPackageManager.swift +// Keychy +// +// 키링 선물 포장 로직 (Collection, Workshop 공용) +// + +import Foundation +import FirebaseFirestore + +/// 키링 선물 포장 관리자 +enum KeyringPackageManager { + + // MARK: - 키링 포장하기 + + /// 키링을 선물용으로 포장합니다. + /// - Parameters: + /// - uid: 사용자 ID + /// - keyringDocumentId: 키링 Firestore Document ID + /// - completion: 완료 콜백 (성공 여부, PostOffice ID, Share Link) + static func packageKeyring( + uid: String, + keyringDocumentId: String, + completion: @escaping (Bool, String?, String?) -> Void + ) { + let db = Firestore.firestore() + + // 1. Keyring 상태 업데이트 (isPackaged = true) + db.collection("Keyring") + .document(keyringDocumentId) + .updateData(["isPackaged": true]) { error in + if let error = error { + print("[Package] Keyring 상태 업데이트 실패: \(error.localizedDescription)") + completion(false, nil, nil) + return + } + + print("[Package] Keyring 상태 업데이트 완료") + + // 2. PostOffice 문서 생성 + createPostOffice( + db: db, + uid: uid, + keyringDocumentId: keyringDocumentId, + completion: completion + ) + } + } + + // MARK: - Private Helpers + + /// PostOffice 문서 생성 + private static func createPostOffice( + db: Firestore, + uid: String, + keyringDocumentId: String, + completion: @escaping (Bool, String?, String?) -> Void + ) { + let postOfficeRef = db.collection("PostOffice").document() + let postOfficeId = postOfficeRef.documentID + + // 중복 체크 (희귀 케이스) + postOfficeRef.getDocument { checkSnapshot, checkError in + if checkSnapshot?.exists == true { + print("[Package] PostOffice ID 중복 발견 - 재시도") + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + packageKeyring(uid: uid, keyringDocumentId: keyringDocumentId, completion: completion) + } + return + } + + // 공유 링크 생성 + guard let shareLink = DeepLinkManager.createShareLink(postOfficeId: postOfficeId) else { + print("[Package] 공유 링크 생성 실패") + completion(false, nil, nil) + return + } + + let shareLinkString = shareLink.absoluteString + print("[Package] 공유 링크 생성: \(shareLinkString)") + + // PostOffice 문서 데이터 + let postOfficeData: [String: Any] = [ + "type": "receive", + "senderId": uid, + "keyringId": keyringDocumentId, + "shareLink": shareLinkString, + "createdAt": Timestamp(date: Date()) + ] + + // 문서 생성 + postOfficeRef.setData(postOfficeData) { error in + if let error = error { + print("[Package] PostOffice 문서 생성 실패: \(error.localizedDescription)") + completion(false, nil, nil) + return + } + + print("[Package] PostOffice 문서 생성 완료: \(postOfficeId)") + + // Bundle에서 키링 제거 + removeKeyringFromBundles(db: db, uid: uid, keyringDocumentId: keyringDocumentId) { _ in + completion(true, postOfficeId, shareLinkString) + } + } + } + } + + /// Bundle에서 키링 제거 + private static func removeKeyringFromBundles( + db: Firestore, + uid: String, + keyringDocumentId: String, + completion: @escaping (Bool) -> Void + ) { + db.collection("KeyringBundle") + .whereField("userId", isEqualTo: uid) + .getDocuments { snapshot, error in + if error != nil { + completion(false) + return + } + + guard let documents = snapshot?.documents, !documents.isEmpty else { + print("[Package] Bundle 없음") + completion(true) + return + } + + let batch = db.batch() + var affectedBundleIds: [String] = [] + + for document in documents { + guard var keyrings = document.data()["keyrings"] as? [String] else { + continue + } + + var needsUpdate = false + + for (index, keyring) in keyrings.enumerated() { + if keyring == keyringDocumentId { + keyrings[index] = "none" + needsUpdate = true + } + } + + if needsUpdate { + let bundleRef = db.collection("KeyringBundle").document(document.documentID) + batch.updateData(["keyrings": keyrings], forDocument: bundleRef) + affectedBundleIds.append(document.documentID) + } + } + + if affectedBundleIds.isEmpty { + completion(true) + return + } + + batch.commit { error in + if let error = error { + print("[Package] Bundle 업데이트 실패: \(error.localizedDescription)") + completion(false) + return + } + + print("[Package] \(affectedBundleIds.count)개 Bundle에서 키링 제거 완료") + + // Bundle 캡처 캐시 삭제 + for bundleId in affectedBundleIds { + BundleImageCache.shared.delete(for: bundleId) + } + + completion(true) + } + } + } +} diff --git a/Keychy/Keychy/Core/Navigation/Routes/WorkshopRoute.swift b/Keychy/Keychy/Core/Navigation/Routes/WorkshopRoute.swift index 911cc3010..5995cd382 100644 --- a/Keychy/Keychy/Core/Navigation/Routes/WorkshopRoute.swift +++ b/Keychy/Keychy/Core/Navigation/Routes/WorkshopRoute.swift @@ -68,6 +68,9 @@ enum WorkshopRoute: Hashable, BundleRoute { case speechBubbleInfoInput case speechBubbleComplete + // MARK: - 선물 포장 완료 + case packageComplete(keyringDocumentId: String, postOfficeId: String, templateId: String, shareLink: String) + /// template.id 문자열을 WorkshopRoute로 변환 static func from(string: String) -> WorkshopRoute? { switch string { diff --git a/Keychy/Keychy/Core/Navigation/TabBarManager.swift b/Keychy/Keychy/Core/Navigation/TabBarManager.swift index 5331ba74c..dfebf2e3a 100644 --- a/Keychy/Keychy/Core/Navigation/TabBarManager.swift +++ b/Keychy/Keychy/Core/Navigation/TabBarManager.swift @@ -10,6 +10,14 @@ import UIKit /// 탭바 표시/숨김 전역 관리 enum TabBarManager { + /// 탭 인덱스 + enum TabIndex: Int { + case home = 0 + case workshop = 1 + case collection = 2 + case festival = 3 + } + /// 탭바 숨기기 static func hide() { guard let tabBarController = findTabBarController() else { return } @@ -24,6 +32,12 @@ enum TabBarManager { } } + /// 특정 탭으로 전환 + static func switchTo(_ tab: TabIndex) { + guard let tabBarController = findTabBarController() else { return } + tabBarController.selectedIndex = tab.rawValue + } + /// TabBarController 찾기 private static func findTabBarController() -> UITabBarController? { guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, diff --git a/Keychy/Keychy/Core/Video/Keyring/KeyringVideoGenerator+Keyring.swift b/Keychy/Keychy/Core/Video/Keyring/KeyringVideoGenerator+Keyring.swift index 2fbb648f1..50c24ea48 100644 --- a/Keychy/Keychy/Core/Video/Keyring/KeyringVideoGenerator+Keyring.swift +++ b/Keychy/Keychy/Core/Video/Keyring/KeyringVideoGenerator+Keyring.swift @@ -161,6 +161,10 @@ private class KeyringAdapter: KeyringViewModelProtocol { func isInCache(particleId: String) -> Bool { false } func downloadSound(_ sound: Sound) async { } func downloadParticle(_ particle: Particle) async { } + var savedKeyringDocumentId: String? + var packagedPostOfficeId: String? + var packagedShareLink: String? + func resetCustomizingData() { } func resetInfoData() { } func resetAll() { } diff --git a/Keychy/Keychy/Presentation/Collection/ViewModels/CollectionViewModel+Package.swift b/Keychy/Keychy/Presentation/Collection/ViewModels/CollectionViewModel+Package.swift index b26e9af1e..617d85d54 100644 --- a/Keychy/Keychy/Presentation/Collection/ViewModels/CollectionViewModel+Package.swift +++ b/Keychy/Keychy/Presentation/Collection/ViewModels/CollectionViewModel+Package.swift @@ -10,7 +10,7 @@ import FirebaseFirestore // MARK: - 포장 처리 extension CollectionViewModel { - + // MARK: - 포장 상태 업데이트 func packageKeyring( uid: String, @@ -18,180 +18,31 @@ extension CollectionViewModel { completion: @escaping (Bool, String?) -> Void ) { guard let documentId = keyringDocumentIdByLocalId[keyring.id] else { - print("키링 문서 ID 없음") + print("[Collection] 키링 문서 ID 없음") completion(false, nil) return } - - let db = Firestore.firestore() - - // 1. Keyring 상태 업데이트 - let keyringUpdateData: [String: Any] = [ - "isPackaged": true - ] - - db.collection("Keyring") - .document(documentId) - .updateData(keyringUpdateData) { [weak self] error in - guard let self = self else { - completion(false, nil) - return - } - - if let error = error { - print("Keyring 상태 업데이트 실패: \(error.localizedDescription)") - completion(false, nil) - return - } - - print("Keyring 상태 업데이트 완료") - - // 2. PostOffice 문서 먼저 생성 (shareLink 없이) - let postOfficeRef = db.collection("PostOffice").document() - let postOfficeId = postOfficeRef.documentID - - postOfficeRef.getDocument { [weak self] checkSnapshot, checkError in - guard let self = self else { - completion(false, nil) - return - } - - if checkSnapshot?.exists == true { - print("[희귀 케이스] PostOffice ID 중복 발견 - 재시도") - - // 재귀 호출로 다시 시도 (최대 3회 정도) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - self.packageKeyring(uid: uid, keyring: keyring, completion: completion) - } - return - } - - // 3. PostOffice ID로 공유 링크 생성 - guard let shareLink = DeepLinkManager.createShareLink(postOfficeId: postOfficeId) else { - print("공유 링크 생성 실패") - completion(false, nil) - return - } - - print("공유 링크 생성: \(shareLink.absoluteString)") - - // 4. PostOffice 문서 생성 - let postOfficeData: [String: Any] = [ - "type": "receive", - "senderId": uid, - "keyringId": documentId, - "shareLink": shareLink.absoluteString, - "createdAt": Timestamp(date: Date()) - ] - - postOfficeRef.setData(postOfficeData) { error in - if let error = error { - print("PostOffice 문서 생성 실패: \(error.localizedDescription)") - completion(false, nil) - return - } - - print("PostOffice 문서 생성 완료: \(postOfficeId)") - - // 5. Bundle에서 키링 제거 - self.removeKeyringFromBundles( - uid: uid, - keyringId: documentId - ) { bundleSuccess in - if bundleSuccess { - print("Bundle에서 키링 제거 완료") - } else { - print("Bundle에서 키링 제거 실패 (Bundle 없음)") - } - - // 로컬 상태 업데이트 - if let index = self.keyring.firstIndex(where: { $0.id == keyring.id }) { - self.keyring[index].isPackaged = true - self.keyring[index].isEditable = false - } - - completion(true, postOfficeId) - } - } - } + + // KeyringPackageManager를 사용하여 포장 처리 + KeyringPackageManager.packageKeyring( + uid: uid, + keyringDocumentId: documentId + ) { [weak self] success, postOfficeId, _ in // shareLink는 Collection에서 미사용 + guard let self = self else { + completion(false, nil) + return } - } - - // MARK: - Bundle에서 키링 제거 - private func removeKeyringFromBundles( - uid: String, - keyringId: String, - completion: @escaping (Bool) -> Void - ) { - let db = Firestore.firestore() - - // 해당 사용자의 모든 Bundle 조회 - db.collection("KeyringBundle") - .whereField("userId", isEqualTo: uid) - .getDocuments { snapshot, error in - if error != nil { - completion(false) - return - } - - guard let documents = snapshot?.documents, !documents.isEmpty else { - print("Bundle 없음") - completion(true) - return - } - - let batch = db.batch() - var affectedBundleIds: [String] = [] - - // 각 Bundle에서 해당 키링 ID 제거 - for document in documents { - guard var keyrings = document.data()["keyrings"] as? [String] else { - continue - } - - var needsUpdate = false - - // 배열을 순회하면서 keyringId를 "none"으로 변경 - for (index, keyring) in keyrings.enumerated() { - if keyring == keyringId { - keyrings[index] = "none" - needsUpdate = true - print("Bundle '\(document.documentID)'의 인덱스 \(index)를 'none'으로 변경 예정") - } - } - - if needsUpdate { - let bundleRef = db.collection("KeyringBundle").document(document.documentID) - batch.updateData(["keyrings": keyrings], forDocument: bundleRef) - affectedBundleIds.append(document.documentID) - } - } - - if affectedBundleIds.isEmpty { - print("키링이 포함된 Bundle 없음") - completion(true) - return - } - - // Batch 커밋 - batch.commit { error in - if let error = error { - print("Bundle 업데이트 실패: \(error.localizedDescription)") - completion(false) - return - } - - print("\(affectedBundleIds.count)개 Bundle에서 키링 제거 완료") - - // 변경된 Bundle들의 캡처 캐시 삭제 - for bundleId in affectedBundleIds { - BundleImageCache.shared.delete(for: bundleId) - print("Bundle 캡처 캐시 삭제: \(bundleId)") - } - - completion(true) + + if success { + // 로컬 상태 업데이트 + if let index = self.keyring.firstIndex(where: { $0.id == keyring.id }) { + self.keyring[index].isPackaged = true + self.keyring[index].isEditable = false } } + + completion(success, postOfficeId) + } } // MARK: - PostOffice 데이터 가져오기 diff --git a/Keychy/Keychy/Presentation/Collection/Views/Detail/PackagedKeyringView.swift b/Keychy/Keychy/Presentation/Collection/Views/Detail/PackagedKeyringView.swift index a5b29449e..b7683256b 100644 --- a/Keychy/Keychy/Presentation/Collection/Views/Detail/PackagedKeyringView.swift +++ b/Keychy/Keychy/Presentation/Collection/Views/Detail/PackagedKeyringView.swift @@ -48,12 +48,16 @@ struct PackagedKeyringView: View { .padding(.horizontal, 20) .onAppear { loadCachedImage() + // shareLink가 이미 있으면 QR 생성 (Workshop에서 직접 전달받는 경우) + if !shareLink.isEmpty { + generateQRCodeImage() + } } .onDisappear { cleanupImages() } .onChange(of: shareLink) { oldValue, newValue in - // shareLink가 업데이트되면 QR 코드 생성 + // shareLink가 업데이트되면 QR 코드 생성 (Collection에서 비동기 로드하는 경우) if !newValue.isEmpty { generateQRCodeImage() } diff --git a/Keychy/Keychy/Presentation/Intro/ViewModels/WelcomeKeyringViewModel.swift b/Keychy/Keychy/Presentation/Intro/ViewModels/WelcomeKeyringViewModel.swift index 5400aea78..ae1ec8c5b 100644 --- a/Keychy/Keychy/Presentation/Intro/ViewModels/WelcomeKeyringViewModel.swift +++ b/Keychy/Keychy/Presentation/Intro/ViewModels/WelcomeKeyringViewModel.swift @@ -42,6 +42,10 @@ class WelcomeKeyringViewModel: KeyringViewModelProtocol { var particleId: String = "Confetti" var effectSubject = PassthroughSubject<(soundId: String, particleId: String, type: KeyringUpdateType), Never>() + var savedKeyringDocumentId: String? + var packagedPostOfficeId: String? + var packagedShareLink: String? + init(nickname: String, bodyImage: UIImage) { self.nameText = nickname self.bodyImage = bodyImage diff --git a/Keychy/Keychy/Presentation/KeyringMaker/Shared/Protocols/KeyringViewModelProtocol+Reset.swift b/Keychy/Keychy/Presentation/KeyringMaker/Shared/Protocols/KeyringViewModelProtocol+Reset.swift index bbc2a0944..351cf142d 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Shared/Protocols/KeyringViewModelProtocol+Reset.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Shared/Protocols/KeyringViewModelProtocol+Reset.swift @@ -36,5 +36,8 @@ extension KeyringViewModelProtocol { func resetAll() { resetCustomizingData() resetInfoData() + savedKeyringDocumentId = nil + packagedPostOfficeId = nil + packagedShareLink = nil } } diff --git a/Keychy/Keychy/Presentation/KeyringMaker/Shared/Protocols/KeyringViewModelProtocol.swift b/Keychy/Keychy/Presentation/KeyringMaker/Shared/Protocols/KeyringViewModelProtocol.swift index fc6fc3562..685f91a58 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Shared/Protocols/KeyringViewModelProtocol.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Shared/Protocols/KeyringViewModelProtocol.swift @@ -37,6 +37,13 @@ protocol KeyringViewModelProtocol: AnyObject, Observable { var maxMemoCount: Int { get } var createdAt: Date { get set } + /// 저장된 키링 Document ID (Firebase 저장 후 설정됨) + var savedKeyringDocumentId: String? { get set } + + /// 포장 완료 정보 (선물하기 후 저장됨) + var packagedPostOfficeId: String? { get set } + var packagedShareLink: String? { get set } + /// 태그 관련 var selectedTags: [String] { get set } diff --git a/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringCompleteView.swift b/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringCompleteView.swift index ae0a25c42..0d9bac16c 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringCompleteView.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringCompleteView.swift @@ -14,118 +14,166 @@ struct KeyringCompleteView: View { @Bindable var router: NavigationRouter @Bindable var viewModel: VM let navigationTitle: String - + var userManager: UserManager = UserManager.shared var reviewManager: ReviewManager = ReviewManager.shared - + // Festival에서 왔을 때 처리용 옵셔널 콜백 var onCloseFromFestival: ((NavigationRouter) -> Void)? // 이미지 저장 @State var showImageSaved = false @State var isCapturingImage = false - + // 영상 생성 @State var isGeneratingVideo = false @State var showVideoSaved = false - + // 씬 인터랙션 @State var isInteractionEnabled = false + // 선물 포장 + @State var showPackageAlert = false + @State var showPackingAlert = false + // 비디오 생성기 let videoGenerator = KeyringVideoGenerator() var body: some View { + ZStack { + // 1. 배경 + backgroundView + + // 2. 메인 컨텐츠 + mainContent + + // 3. Alerts 오버레이 + alertsOverlay + + // 4. 로딩 오버레이 + loadingOverlay + } + .navigationBarBackButtonHidden() + .toolbar { + closeToolbarItem + titleToolbarItem + collectionToolbarItem + } + .onAppear { + checkReviewTriggers() + } + .withToast(position: .default) + } +} + +// MARK: - View Components +extension KeyringCompleteView { + /// 배경 이미지 + private var backgroundView: some View { + Image(.completeBG2) + .resizable() + .scaledToFill() + .ignoresSafeArea() + .cinematicAppear(delay: 0, duration: 0.6, style: .fadeIn) + .blur(radius: isAlertShowing ? 15 : 0) + } + + /// 메인 컨텐츠 (키링씬 + 정보 + 버튼) + private var mainContent: some View { GeometryReader { geometry in - ZStack { - Image(.completeBG2) - .resizable() - .scaledToFill() - .frame(width: geometry.size.width, height: geometry.size.height) - .clipped() - .ignoresSafeArea() - .cinematicAppear(delay: 0, duration: 0.6, style: .fadeIn) - .blur(radius: showImageSaved ? 15 : 0) + VStack(spacing: 0) { + // 키링 씬 (화면 높이의 55%) + keyringScene + .frame(height: geometry.size.height * 0.53) + .cinematicAppear(delay: 0.2, duration: 0.8, style: .full) + .offset(x: 0, y: -20) - VStack(spacing: 0) { - Spacer() - // 키링 씬 - - ZStack(alignment: .center) { - keyringScene - .frame(height: geometry.size.height * 0.72) - .cinematicAppear(delay: 0.2, duration: 0.8, style: .full) - .position(x: geometry.size.width / 2, y: geometry.size.height * 0.4) - - VStack { - // 키링 정보 - keyringInfo - .cinematicAppear(delay: 0.6, duration: 0.8, style: .slideUp) - - // 이미지 저장 버튼 - saveButton - .padding(.top, 10) - .cinematicAppear(delay: 1.0, duration: 0.8, style: .fadeIn) - .opacity(isCapturingImage ? 0 : 1) - //.adaptiveBottomPadding() - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .position(x: geometry.size.width / 2, y: geometry.size.height * 0.8) - } - } - .blur(radius: showImageSaved ? 15 : 0) - - /// 이미지 저장 완료 alert - KeychyAlert( - type: .imageSave, - message: "이미지가 저장되었어요!", - isPresented: $showImageSaved - ) - - /// 영상 저장 완료 alert - KeychyAlert( - type: .imageSave, - message: "영상이 저장되었어요!", - isPresented: $showVideoSaved - ) - - /// 영상 생성 중 로딩 - if isGeneratingVideo { - ZStack { - Color.black20 - .ignoresSafeArea() + // 키링 정보 + keyringInfo + .cinematicAppear(delay: 0.6, duration: 0.8, style: .slideUp) + .padding(.bottom, 30) + + // 액션 버튼 + actionButtons + .cinematicAppear(delay: 1.0, duration: 0.8, style: .fadeIn) + .opacity(isCapturingImage ? 0 : 1) + } + .adaptiveTopPaddingAlt() + } + .blur(radius: isAlertShowing ? 15 : 0) + } + + /// Alerts 오버레이 + @ViewBuilder + private var alertsOverlay: some View { + KeychyAlert( + type: .imageSave, + message: "이미지가 저장되었어요!", + isPresented: $showImageSaved + ) - VStack(spacing: 20) { - ProgressView() - .scaleEffect(1.5) - .tint(.white) + KeychyAlert( + type: .imageSave, + message: "영상이 저장되었어요!", + isPresented: $showVideoSaved + ) - Text("영상 생성 중...") - .typography(.suit17SB) - .foregroundColor(.white) + // 선물 포장 확인 팝업 + if showPackageAlert { + Color.black20 + .ignoresSafeArea() + .zIndex(99) - Text("5~10초 소요") - .typography(.suit14M) - .foregroundColor(.white.opacity(0.7)) - } - .padding(40) - .background(.ultraThinMaterial) - .cornerRadius(20) + PackagePopup( + onCancel: { + withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { + showPackageAlert = false } - .zIndex(999) + }, + onConfirm: { + handlePackageConfirm() } + ) + .zIndex(100) + } - // 커스텀 네비게이션 바 - customNavigationBar - .blur(radius: showImageSaved ? 15 : 0) - .opacity(isCapturingImage ? 0 : 1) - .adaptiveTopPadding() - } + // 포장 중 로딩 + if showPackingAlert { + Color.black20 + .ignoresSafeArea() + .zIndex(99) + + LoadingAlert( + type: .longWithPresent, + message: "선물 포장 중.." + ) + .zIndex(101) } - .ignoresSafeArea() - .navigationBarBackButtonHidden(true) - .onAppear { - checkReviewTriggers() + } + + /// 로딩 오버레이 + @ViewBuilder + private var loadingOverlay: some View { + if isGeneratingVideo { + Color.black20 + .ignoresSafeArea() + + VStack(spacing: 20) { + ProgressView() + .scaleEffect(1.5) + .tint(.white) + + Text("영상 생성 중...") + .typography(.suit17SB) + .foregroundColor(.white) + + Text("5~10초 소요") + .typography(.suit14M) + .foregroundColor(.white.opacity(0.7)) + } + .padding(40) + .background(.ultraThinMaterial) + .cornerRadius(20) } } } @@ -149,14 +197,18 @@ extension KeyringCompleteView { } } -//MARK: - 커스텀 네비게이션 바 +// MARK: - Toolbar Items extension KeyringCompleteView { - private var customNavigationBar: some View { - CustomNavigationBar { - // Leading (왼쪽) - CloseToolbarButton { + /// Alert 표시 중 여부 + private var isAlertShowing: Bool { + showImageSaved || showVideoSaved || isGeneratingVideo || showPackageAlert || showPackingAlert + } + + var closeToolbarItem: some ToolbarContent { + ToolbarItem(placement: .topBarLeading) { + Button { viewModel.resetAll() - + // Festival에서 온 경우 콜백 실행 if let onCloseFromFestival = onCloseFromFestival { onCloseFromFestival(router) @@ -165,18 +217,48 @@ extension KeyringCompleteView { TabBarManager.show() router.reset() } + } label: { + Image(.dismissGray600) } - } center: { - // Center (중앙) - Text("키링이 완성되었어요!") - .typography(.suit17B) + .opacity(isAlertShowing ? 0 : 1) + .allowsHitTesting(!isAlertShowing) + } + .sharedBackgroundVisibility(isAlertShowing ? .hidden : .visible) + } + + var titleToolbarItem: some ToolbarContent { + ToolbarItem(placement: .principal) { + Text("키링 완성!") + .typography(.notosans17M) .foregroundStyle(.black100) - } trailing: { - // Trailing (오른쪽) - 빈 공간 유지 - Spacer() - .frame(width: 44, height: 44) + .opacity(isAlertShowing ? 0 : 1) + } + } + + var collectionToolbarItem: some ToolbarContent { + ToolbarItem(placement: .topBarTrailing) { + Button { + navigateToCollection() + } label: { + Image(.goToCollection) + } + .opacity(isAlertShowing ? 0 : 1) + .allowsHitTesting(!isAlertShowing) + } + .sharedBackgroundVisibility(isAlertShowing ? .hidden : .visible) + } + + /// 콜렉션으로 이동 (부드러운 전환) + private func navigateToCollection() { + // 1. 탭 전환 먼저 (현재 뷰가 보이는 상태에서) + TabBarManager.switchTo(.collection) + TabBarManager.show() + + // 2. 백그라운드에서 Workshop 스택 정리 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + viewModel.resetAll() + router.reset() } - .cinematicAppear(delay: 0.6, duration: 0.8, style: .fadeIn) } } @@ -187,6 +269,7 @@ extension KeyringCompleteView { Text(viewModel.nameText) .typography(getBottomPadding(0) == 0 ? .malang24B : .malang26B) .foregroundStyle(.black100) + .padding(.bottom, 2) Text(formattedDate(date: viewModel.createdAt)) .typography(.suit14M) @@ -196,7 +279,7 @@ extension KeyringCompleteView { if let nickname = userManager.currentUser?.nickname { Text("@\(nickname)") .typography(getBottomPadding(0) == 0 ? .notosans12R : .notosans14R) - .foregroundStyle(.black100) + .foregroundStyle(.gray500) .padding(.vertical, 1) } } @@ -210,53 +293,160 @@ extension KeyringCompleteView { } } -// MARK: - 저장 버튼 +// MARK: - 버튼 extension KeyringCompleteView { - private var saveButton: some View { - HStack(spacing: 20) { - // 이미지 저장 버튼 - VStack(spacing: 9) { - Button(action: { - captureAndSaveImage() - }) { - Image(.imageDownload) + /// 버튼 사이즈 (디바이스별) + private var buttonSize: CGFloat { + getBottomPadding(0) == 0 ? 55 : 65 + } + + /// 액션 버튼 컴포넌트 + private func actionButton( + image: ImageResource, + title: String, + action: @escaping () -> Void + ) -> some View { + Button(action: action) { + VStack(spacing: 4) { + Image(image) + Text(title) + .typography(.suit12M) + .foregroundStyle(.black100) + } + .frame(width: 74, height: 47) + .padding(.vertical, 11.5) + .padding(.horizontal, 8) + } + .glassEffect(.regular.interactive(), in: .rect(cornerRadius: 24)) + } + + /// 하단 액션 버튼 영역 + private var actionButtons: some View { + HStack(spacing: 17) { + // 이미지 저장 + actionButton(image: .saveBlack, title: "이미지 저장") { + captureAndSaveImage() + } + + // 공유 + actionButton(image: .share, title: "공유") { + // TODO: 공유 기능 + } + + // 선물하기 + actionButton(image: .presentBlack, title: "선물하기") { + // 이미 포장된 경우 바로 이동 + if let keyringDocumentId = viewModel.savedKeyringDocumentId, + let postOfficeId = viewModel.packagedPostOfficeId, + let shareLink = viewModel.packagedShareLink { + router.push(.packageComplete( + keyringDocumentId: keyringDocumentId, + postOfficeId: postOfficeId, + templateId: viewModel.templateId, + shareLink: shareLink + )) + return } - .frame( - width: getBottomPadding(0) == 0 ? 55 : 65, - height: getBottomPadding(0) == 0 ? 55 : 65 - ) - .buttonStyle(.plain) - .glassEffect(.regular.interactive(), in: .circle) - Text("이미지 저장") - .typography(.suit13SB) - .foregroundStyle(.black100) + // 네트워크 체크 + guard NetworkManager.shared.isConnected else { + ToastManager.shared.show() + return + } + + withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { + showPackageAlert = true + } + } + } + } +} + +// MARK: - 선물 포장 처리 +extension KeyringCompleteView { + /// 선물 포장 확인 처리 + private func handlePackageConfirm() { + withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { + showPackageAlert = false + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + guard let keyringDocumentId = viewModel.savedKeyringDocumentId else { + print("[Package] keyringDocumentId 없음") + return + } + + // 이미 포장된 경우 바로 이동 + if let postOfficeId = viewModel.packagedPostOfficeId, + let shareLink = viewModel.packagedShareLink { + print("[Package] 이미 포장됨 - 바로 이동") + router.push(.packageComplete( + keyringDocumentId: keyringDocumentId, + postOfficeId: postOfficeId, + templateId: viewModel.templateId, + shareLink: shareLink + )) + return + } + + // 새로 포장하는 경우 + guard let uid = userManager.currentUser?.id else { + print("[Package] uid 없음") + return } - // 영상 생성 버튼 - VStack(spacing: 9) { - Button(action: { - Task { - await generateAndSaveVideo() + // 포장 중 로딩 표시 + withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { + showPackingAlert = true + } + + // 최소 로딩 시간 보장을 위한 시작 시간 기록 + let startTime = Date() + let minimumLoadingDuration: TimeInterval = 1.0 + + // 패키징 실행 + KeyringPackageManager.packageKeyring( + uid: uid, + keyringDocumentId: keyringDocumentId + ) { success, postOfficeId, shareLink in + let elapsed = Date().timeIntervalSince(startTime) + let remainingDelay = max(0, minimumLoadingDuration - elapsed) + + // 최소 1초 로딩 후 처리 + DispatchQueue.main.asyncAfter(deadline: .now() + remainingDelay) { + showPackingAlert = false + + if success, let postOfficeId = postOfficeId, let shareLink = shareLink { + // 포장 정보 저장 (뒤로갔다 다시 올 때 사용) + viewModel.packagedPostOfficeId = postOfficeId + viewModel.packagedShareLink = shareLink + + // 성공 - 포장 완료 화면으로 이동 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + router.push(.packageComplete( + keyringDocumentId: keyringDocumentId, + postOfficeId: postOfficeId, + templateId: viewModel.templateId, + shareLink: shareLink + )) + } + } else { + print("[Package] 포장 실패") + ToastManager.shared.show() } - }) { - Image(systemName: "video.fill") - .font(.system(size: 24)) - .foregroundColor(.black100) } - .frame( - width: getBottomPadding(0) == 0 ? 55 : 65, - height: getBottomPadding(0) == 0 ? 55 : 65 - ) - .buttonStyle(.plain) - .glassEffect(.regular.interactive(), in: .circle) - .disabled(isGeneratingVideo) - - Text("영상 생성") - .typography(.suit13SB) - .foregroundStyle(.black100) } - .opacity(isGeneratingVideo ? 0.5 : 1) } } } + +// MARK: - Preview +#Preview { + NavigationStack { + KeyringCompleteView( + router: NavigationRouter(), + viewModel: PolaroidVM(), + navigationTitle: "키링 완성" + ) + } +} diff --git a/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringInfoInputView+FirebaseSave.swift b/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringInfoInputView+FirebaseSave.swift index b25439bf5..6a7903338 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringInfoInputView+FirebaseSave.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringInfoInputView+FirebaseSave.swift @@ -70,6 +70,9 @@ extension KeyringInfoInputView { ) { success, keyringId in // 백그라운드로 위젯용 이미지 캡처 및 저장 if success, let keyringId = keyringId { + // Document ID 저장 (선물하기 등에서 사용) + self.viewModel.savedKeyringDocumentId = keyringId + // viewModel이 reset되기 전에 이름과 hookOffsetY, chainLength를 미리 캡처 let keyringName = self.viewModel.nameText let chainLength = self.viewModel.chainLength diff --git a/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringPackageCompleteView.swift b/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringPackageCompleteView.swift new file mode 100644 index 000000000..130e6ca83 --- /dev/null +++ b/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringPackageCompleteView.swift @@ -0,0 +1,243 @@ +// +// KeyringPackageCompleteView.swift +// Keychy +// +// 키링 선물 포장 완료 화면 (Workshop용) +// + +import SwiftUI +import FirebaseFirestore + +struct KeyringPackageCompleteView: View { + @Bindable var router: NavigationRouter + var viewModel: any KeyringViewModelProtocol + + let keyringDocumentId: String + let postOfficeId: String + let shareLink: String // 패키징 시 생성된 링크 직접 전달 + + @State private var keyring: Keyring? + @State private var authorName: String = "" + @State private var isLoading: Bool = true + @State private var showLinkCopied: Bool = false + @State private var showImageSaved: Bool = false + + var body: some View { + GeometryReader { geometry in + ZStack { + if let keyring = keyring { + packagedView(keyring: keyring) + .blur(radius: shouldApplyBlur ? 10 : 0) + .animation(.easeInOut(duration: 0.3), value: shouldApplyBlur) + } + + // 로딩 오버레이 + if isLoading { + Color.black20 + .ignoresSafeArea() + + LoadingAlert(type: .short40, message: nil) + .zIndex(101) + } + + // 이미지 저장 Alert + if showImageSaved { + Color.black20 + .ignoresSafeArea() + .zIndex(99) + + KeychyAlert( + type: .imageSave, + message: "이미지가 저장되었어요!", + isPresented: $showImageSaved + ) + .position(x: geometry.size.width / 2, y: geometry.size.height / 2) + .zIndex(101) + } + + // 링크 복사 Alert + if showLinkCopied { + Color.black20 + .ignoresSafeArea() + .zIndex(99) + + KeychyAlert( + type: .linkCopy, + message: "링크가 복사되었어요!", + isPresented: $showLinkCopied + ) + .position(x: geometry.size.width / 2, y: geometry.size.height / 2) + .zIndex(101) + } + + // 네비게이션 바 + customNavigationBar + .blur(radius: shouldApplyBlur ? 15 : 0) + .adaptiveTopPadding() + .zIndex(0) + } + .padding(.top, 1) + } + .ignoresSafeArea() + .navigationBarBackButtonHidden(true) + .onAppear { + TabBarManager.hide() + loadKeyringData() + } + } + + private var shouldApplyBlur: Bool { + isLoading || showLinkCopied || showImageSaved + } +} + +// MARK: - Views +extension KeyringPackageCompleteView { + private func packagedView(keyring: Keyring) -> some View { + GeometryReader { geometry in + let heightRatio = geometry.size.height / 852 + let isSmallScreen = geometry.size.height < 700 + + ZStack { + // 배경 + Image(.greenBackground) + .resizable() + .scaledToFill() + .ignoresSafeArea() + + VStack(spacing: 0) { + Spacer() + .adaptiveTopPadding() + + // 헤더 텍스트 + VStack(spacing: 15) { + Text("키링 포장이 완료되었어요!") + .typography(.suit20B) + .foregroundColor(.black100) + .padding(.top, 10) + + Text("링크나 QR로 바로 공유할 수 있어요") + .font(.suit16M) + .foregroundColor(.black100) + } + .padding(.top, isSmallScreen ? -70 : 78) + + Spacer() + .frame(height: isSmallScreen ? 24 : 48) + + // 포장된 키링 뷰 (기존 컴포넌트 재사용) + PackagedKeyringView( + keyring: keyring, + postOfficeId: postOfficeId, + shareLink: shareLink, + authorName: authorName, + isLoading: $isLoading, + onImageSaved: { + withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { + showImageSaved = true + } + }, + onLinkCopied: { + withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { + showLinkCopied = true + } + } + ) + .frame(height: isSmallScreen ? 500 : 600) + .scaleEffect(heightRatio) + + Spacer() + .adaptiveBottomPadding() + } + } + } + } + + private var customNavigationBar: some View { + CustomNavigationBar { + // Leading - 뒤로가기 버튼 (키링 완성뷰로) + Button { + router.pop() + } label: { + Image(.backIcon) + } + .frame(width: 44, height: 44) + .glassEffect(.regular.interactive(), in: .circle) + } center: { + Spacer() + } trailing: { + // Trailing - 홈 버튼 + Button { + navigateToHome() + } label: { + Image(.homeBlack) + } + .frame(width: 44, height: 44) + .glassEffect(.regular.interactive(), in: .circle) + } + } + + /// 홈 탭으로 이동 + private func navigateToHome() { + TabBarManager.switchTo(.home) + TabBarManager.show() + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + viewModel.resetAll() + router.reset() + } + } +} + +// MARK: - Data Loading +extension KeyringPackageCompleteView { + private func loadKeyringData() { + let db = Firestore.firestore() + + print("[PackageComplete] 키링 로드 시작 - ID: \(keyringDocumentId)") + + db.collection("Keyring") + .document(keyringDocumentId) + .getDocument { snapshot, error in + if let error = error { + print("[PackageComplete] 키링 로드 실패: \(error.localizedDescription)") + isLoading = false + return + } + + guard let data = snapshot?.data() else { + print("[PackageComplete] 키링 데이터 없음 - documentId: \(keyringDocumentId)") + isLoading = false + return + } + + print("[PackageComplete] 키링 데이터 수신: \(data.keys)") + + // Keyring 파싱 + if let keyring = Keyring(documentId: keyringDocumentId, data: data) { + print("[PackageComplete] 키링 파싱 성공: \(keyring.name)") + self.keyring = keyring + loadAuthorName(authorId: keyring.authorId) + } else { + print("[PackageComplete] 키링 파싱 실패 - 필수 필드 누락") + isLoading = false + } + } + } + + private func loadAuthorName(authorId: String) { + let db = Firestore.firestore() + + db.collection("User") + .document(authorId) + .getDocument { snapshot, error in + if let data = snapshot?.data(), + let name = data["nickname"] as? String { + self.authorName = name + } else { + self.authorName = "알 수 없음" + } + } + } + +} diff --git a/Keychy/Keychy/Presentation/KeyringMaker/Templates/AcrylicPhoto/ViewModels/AcrylicPhotoVM.swift b/Keychy/Keychy/Presentation/KeyringMaker/Templates/AcrylicPhoto/ViewModels/AcrylicPhotoVM.swift index b62c196a4..b85351ab3 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Templates/AcrylicPhoto/ViewModels/AcrylicPhotoVM.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Templates/AcrylicPhoto/ViewModels/AcrylicPhotoVM.swift @@ -125,6 +125,9 @@ class AcrylicPhotoVM: KeyringViewModelProtocol { var maxMemoCount: Int = 500 var selectedTags: [String] = [] var createdAt: Date = Date() + var savedKeyringDocumentId: String? + var packagedPostOfficeId: String? + var packagedShareLink: String? // MARK: - 초기화 init(userManager: UserManager = UserManager.shared) { diff --git a/Keychy/Keychy/Presentation/KeyringMaker/Templates/ClearSketch/ViewModels/ClearSketchVM.swift b/Keychy/Keychy/Presentation/KeyringMaker/Templates/ClearSketch/ViewModels/ClearSketchVM.swift index d439195b4..c7ab1c5ec 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Templates/ClearSketch/ViewModels/ClearSketchVM.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Templates/ClearSketch/ViewModels/ClearSketchVM.swift @@ -130,6 +130,9 @@ class ClearSketchVM: KeyringViewModelProtocol { var maxMemoCount: Int = 500 var selectedTags: [String] = [] var createdAt: Date = Date() + var savedKeyringDocumentId: String? + var packagedPostOfficeId: String? + var packagedShareLink: String? // MARK: - 초기화 init(userManager: UserManager = UserManager.shared) { diff --git a/Keychy/Keychy/Presentation/KeyringMaker/Templates/NeonSign/ViewModels/NeonSignVM.swift b/Keychy/Keychy/Presentation/KeyringMaker/Templates/NeonSign/ViewModels/NeonSignVM.swift index 449cdfeb0..c6cc42a7a 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Templates/NeonSign/ViewModels/NeonSignVM.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Templates/NeonSign/ViewModels/NeonSignVM.swift @@ -109,6 +109,9 @@ class NeonSignVM: KeyringViewModelProtocol { var maxMemoCount: Int = 500 var selectedTags: [String] = [] var createdAt: Date = Date() + var savedKeyringDocumentId: String? + var packagedPostOfficeId: String? + var packagedShareLink: String? // MARK: - 초기화 init(userManager: UserManager = UserManager.shared) { diff --git a/Keychy/Keychy/Presentation/KeyringMaker/Templates/Pixel/ViewModels/PixelVM.swift b/Keychy/Keychy/Presentation/KeyringMaker/Templates/Pixel/ViewModels/PixelVM.swift index f3d28f257..a5328a102 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Templates/Pixel/ViewModels/PixelVM.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Templates/Pixel/ViewModels/PixelVM.swift @@ -114,6 +114,9 @@ class PixelVM: KeyringViewModelProtocol { var maxMemoCount: Int = 500 var selectedTags: [String] = [] var createdAt: Date = Date() + var savedKeyringDocumentId: String? + var packagedPostOfficeId: String? + var packagedShareLink: String? // MARK: - 초기화 init(userManager: UserManager = UserManager.shared) { diff --git a/Keychy/Keychy/Presentation/KeyringMaker/Templates/Polaroid/ViewModels/PolaroidVM.swift b/Keychy/Keychy/Presentation/KeyringMaker/Templates/Polaroid/ViewModels/PolaroidVM.swift index 11951e3b0..93f7d8ccd 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Templates/Polaroid/ViewModels/PolaroidVM.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Templates/Polaroid/ViewModels/PolaroidVM.swift @@ -120,6 +120,9 @@ class PolaroidVM: KeyringViewModelProtocol { var maxMemoCount: Int = 500 var selectedTags: [String] = [] var createdAt: Date = Date() + var savedKeyringDocumentId: String? + var packagedPostOfficeId: String? + var packagedShareLink: String? // MARK: - 초기화 init(userManager: UserManager = UserManager.shared) { diff --git a/Keychy/Keychy/Presentation/KeyringMaker/Templates/SpeechBubble/ViewModels/SpeechBubbleVM.swift b/Keychy/Keychy/Presentation/KeyringMaker/Templates/SpeechBubble/ViewModels/SpeechBubbleVM.swift index f19933b53..bbb03f183 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Templates/SpeechBubble/ViewModels/SpeechBubbleVM.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Templates/SpeechBubble/ViewModels/SpeechBubbleVM.swift @@ -72,7 +72,10 @@ class SpeechBubbleVM: KeyringViewModelProtocol { var maxMemoCount: Int = 500 var selectedTags: [String] = [] var createdAt: Date = Date() - + var savedKeyringDocumentId: String? + var packagedPostOfficeId: String? + var packagedShareLink: String? + // MARK: - Dependencies var userManager: UserManager var errorMessage: String? diff --git a/Keychy/Keychy/Presentation/Tab/Views/WorkshopTab.swift b/Keychy/Keychy/Presentation/Tab/Views/WorkshopTab.swift index d74c2f2a5..0a4b37f18 100644 --- a/Keychy/Keychy/Presentation/Tab/Views/WorkshopTab.swift +++ b/Keychy/Keychy/Presentation/Tab/Views/WorkshopTab.swift @@ -238,6 +238,16 @@ struct WorkshopTab: View { } : nil ) + // MARK: - 선물 포장 완료 + case .packageComplete(let keyringDocumentId, let postOfficeId, let templateId, let shareLink): + KeyringPackageCompleteView( + router: router, + viewModel: getViewModelForTemplate(templateId), + keyringDocumentId: keyringDocumentId, + postOfficeId: postOfficeId, + shareLink: shareLink + ) + // MARK: - Bundle case .bundleInventoryView: BundleInventoryView(router: router, collectionVM: collectionViewModel, bundleVM: bundleViewModel) @@ -311,6 +321,26 @@ struct WorkshopTab: View { return viewModel } + // MARK: - ViewModel by TemplateId + private func getViewModelForTemplate(_ templateId: String) -> any KeyringViewModelProtocol { + switch templateId { + case "AcrylicPhoto": + return getAcrylicPhotoVM() + case "NeonSign": + return getNeonSignVM() + case "Polaroid": + return getPolaroidVM() + case "ClearSketch": + return getClearSketchVM() + case "PixelKeyring": + return getPixelKeyringVM() + case "SpeechBubble": + return getSpeechBubbleVM() + default: + return getPolaroidVM() + } + } + // MARK: - ViewModel Reset func resetAcrylicPhotoVM() { acrylicPhotoVM = nil diff --git a/Keychy/Keychy/Resources/Assets.xcassets/02. WorkShop/goToCollection.imageset/Contents.json b/Keychy/Keychy/Resources/Assets.xcassets/02. WorkShop/goToCollection.imageset/Contents.json new file mode 100644 index 000000000..ce5c5ecf0 --- /dev/null +++ b/Keychy/Keychy/Resources/Assets.xcassets/02. WorkShop/goToCollection.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "goToCollection.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Keychy/Keychy/Resources/Assets.xcassets/02. WorkShop/goToCollection.imageset/goToCollection.pdf b/Keychy/Keychy/Resources/Assets.xcassets/02. WorkShop/goToCollection.imageset/goToCollection.pdf new file mode 100644 index 000000000..709cf3ef8 Binary files /dev/null and b/Keychy/Keychy/Resources/Assets.xcassets/02. WorkShop/goToCollection.imageset/goToCollection.pdf differ diff --git a/Keychy/Keychy/Resources/Assets.xcassets/14. Package/homeBlack.imageset/Contents.json b/Keychy/Keychy/Resources/Assets.xcassets/14. Package/homeBlack.imageset/Contents.json new file mode 100644 index 000000000..c9df2d692 --- /dev/null +++ b/Keychy/Keychy/Resources/Assets.xcassets/14. Package/homeBlack.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "homeBlack.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Keychy/Keychy/Resources/Assets.xcassets/14. Package/homeBlack.imageset/homeBlack.pdf b/Keychy/Keychy/Resources/Assets.xcassets/14. Package/homeBlack.imageset/homeBlack.pdf new file mode 100644 index 000000000..511da05c3 Binary files /dev/null and b/Keychy/Keychy/Resources/Assets.xcassets/14. Package/homeBlack.imageset/homeBlack.pdf differ diff --git a/Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Actions/presentBlack.imageset/Contents.json b/Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Actions/presentBlack.imageset/Contents.json new file mode 100644 index 000000000..4b511bc26 --- /dev/null +++ b/Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Actions/presentBlack.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "presentBlack.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Actions/presentBlack.imageset/presentBlack.pdf b/Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Actions/presentBlack.imageset/presentBlack.pdf new file mode 100644 index 000000000..e024bf3ab Binary files /dev/null and b/Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Actions/presentBlack.imageset/presentBlack.pdf differ diff --git a/Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Actions/saveBlack.imageset/Contents.json b/Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Actions/saveBlack.imageset/Contents.json new file mode 100644 index 000000000..ca29fb68d --- /dev/null +++ b/Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Actions/saveBlack.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "saveBlack.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Actions/saveBlack.imageset/saveBlack.pdf b/Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Actions/saveBlack.imageset/saveBlack.pdf new file mode 100644 index 000000000..9428fbb79 Binary files /dev/null and b/Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Actions/saveBlack.imageset/saveBlack.pdf differ diff --git a/Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Actions/share.imageset/Contents.json b/Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Actions/share.imageset/Contents.json new file mode 100644 index 000000000..462b2d902 --- /dev/null +++ b/Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Actions/share.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "share.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Actions/share.imageset/share.pdf b/Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Actions/share.imageset/share.pdf new file mode 100644 index 000000000..0eb3bca29 Binary files /dev/null and b/Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Actions/share.imageset/share.pdf differ