From 3a0fb15cccd0dfcd56f3ba6fc679a75508cfc087 Mon Sep 17 00:00:00 2001 From: giljihun Date: Thu, 29 Jan 2026 10:33:26 +0900 Subject: [PATCH 01/13] =?UTF-8?q?refactor:=20=EC=99=84=EC=84=B1=EB=B7=B0?= =?UTF-8?q?=20-=20=EC=BB=A4=EC=8A=A4=ED=85=80=EB=84=A4=EB=B9=84=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EA=B8=B0=EB=B3=B8=EB=84=A4=EB=B9=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Shared/Views/KeyringCompleteView.swift | 46 ++++++++++-------- .../goToCollection.imageset/Contents.json | 12 +++++ .../goToCollection.pdf | Bin 0 -> 5928 bytes 3 files changed, 38 insertions(+), 20 deletions(-) create mode 100644 Keychy/Keychy/Resources/Assets.xcassets/02. WorkShop/goToCollection.imageset/Contents.json create mode 100644 Keychy/Keychy/Resources/Assets.xcassets/02. WorkShop/goToCollection.imageset/goToCollection.pdf diff --git a/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringCompleteView.swift b/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringCompleteView.swift index ae0a25c4..9150d700 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringCompleteView.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringCompleteView.swift @@ -67,7 +67,6 @@ struct KeyringCompleteView: View { .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) @@ -115,15 +114,14 @@ struct KeyringCompleteView: View { .zIndex(999) } - // 커스텀 네비게이션 바 - customNavigationBar - .blur(radius: showImageSaved ? 15 : 0) - .opacity(isCapturingImage ? 0 : 1) - .adaptiveTopPadding() } } .ignoresSafeArea() - .navigationBarBackButtonHidden(true) + .navigationBarBackButtonHidden() + .toolbar { + closeToolbarItem + titleToolbarItem + } .onAppear { checkReviewTriggers() } @@ -149,12 +147,16 @@ extension KeyringCompleteView { } } -//MARK: - 커스텀 네비게이션 바 +// MARK: - Toolbar Items extension KeyringCompleteView { - private var customNavigationBar: some View { - CustomNavigationBar { - // Leading (왼쪽) - CloseToolbarButton { + /// Alert 표시 중 여부 + private var isAlertShowing: Bool { + showImageSaved || showVideoSaved || isGeneratingVideo + } + + var closeToolbarItem: some ToolbarContent { + ToolbarItem(placement: .topBarLeading) { + Button { viewModel.resetAll() // Festival에서 온 경우 콜백 실행 @@ -165,18 +167,22 @@ extension KeyringCompleteView { TabBarManager.show() router.reset() } + } label: { + Image(.dismissGray600) } - } center: { - // Center (중앙) - Text("키링이 완성되었어요!") + .opacity(isAlertShowing ? 0 : 1) + .allowsHitTesting(!isAlertShowing) + } + .sharedBackgroundVisibility(isAlertShowing ? .hidden : .visible) + } + + var titleToolbarItem: some ToolbarContent { + ToolbarItem(placement: .principal) { + Text("키링 완성!") .typography(.suit17B) .foregroundStyle(.black100) - } trailing: { - // Trailing (오른쪽) - 빈 공간 유지 - Spacer() - .frame(width: 44, height: 44) + .opacity(isAlertShowing ? 0 : 1) } - .cinematicAppear(delay: 0.6, duration: 0.8, style: .fadeIn) } } 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 00000000..ce5c5ecf --- /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 0000000000000000000000000000000000000000..709cf3ef8d611f6bbd24333567a9f52c3b429a16 GIT binary patch literal 5928 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 Date: Thu, 29 Jan 2026 11:09:38 +0900 Subject: [PATCH 02/13] =?UTF-8?q?refactor:=20KeyringSceneView=20-=20ignore?= =?UTF-8?q?SafeArea=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 이게 왜 그동안 있었는지 모르겠습니다. - 완성뷰에서 뷰를 그릴때 이 조건때문에 까다롭기만 했네요. --- Keychy/Keychy/Core/Components/Keyring/KeyringSceneView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Keychy/Keychy/Core/Components/Keyring/KeyringSceneView.swift b/Keychy/Keychy/Core/Components/Keyring/KeyringSceneView.swift index 98682b63..f3d1f8af 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) } From 027b95a8879635da3f2db65417f4953d35557a90 Mon Sep 17 00:00:00 2001 From: giljihun Date: Thu, 29 Jan 2026 11:10:01 +0900 Subject: [PATCH 03/13] =?UTF-8?q?refactor:=20=EC=99=84=EC=84=B1=EB=B7=B0?= =?UTF-8?q?=20-=20=EC=BB=A4=EC=8A=A4=ED=85=80=EB=84=A4=EB=B9=84=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EB=B7=B0=20?= =?UTF-8?q?=EA=B3=84=EC=B8=B5=20=EB=8B=A8=EC=88=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Shared/Views/KeyringCompleteView.swift | 179 ++++++++++-------- 1 file changed, 102 insertions(+), 77 deletions(-) diff --git a/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringCompleteView.swift b/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringCompleteView.swift index 9150d700..74303fe4 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringCompleteView.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringCompleteView.swift @@ -36,87 +36,19 @@ struct KeyringCompleteView: View { let videoGenerator = KeyringVideoGenerator() var body: 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) { - 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) - } - .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 - ) + ZStack { + // 1. 배경 + backgroundView - /// 영상 저장 완료 alert - KeychyAlert( - type: .imageSave, - message: "영상이 저장되었어요!", - isPresented: $showVideoSaved - ) - - /// 영상 생성 중 로딩 - if isGeneratingVideo { - ZStack { - Color.black20 - .ignoresSafeArea() - - VStack(spacing: 20) { - ProgressView() - .scaleEffect(1.5) - .tint(.white) + // 2. 메인 컨텐츠 + mainContent - Text("영상 생성 중...") - .typography(.suit17SB) - .foregroundColor(.white) - - Text("5~10초 소요") - .typography(.suit14M) - .foregroundColor(.white.opacity(0.7)) - } - .padding(40) - .background(.ultraThinMaterial) - .cornerRadius(20) - } - .zIndex(999) - } + // 3. Alerts 오버레이 + alertsOverlay - } + // 4. 로딩 오버레이 + loadingOverlay } - .ignoresSafeArea() .navigationBarBackButtonHidden() .toolbar { closeToolbarItem @@ -128,6 +60,87 @@ struct KeyringCompleteView: View { } } +// 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 + VStack(spacing: 0) { + // 키링 씬 (화면 높이의 55%) + keyringScene + .frame(height: geometry.size.height * 0.53) + .cinematicAppear(delay: 0.2, duration: 0.8, style: .full) + .border(.red) + .offset(x: 0, y: -20) + + // 키링 정보 + keyringInfo + .cinematicAppear(delay: 0.6, duration: 0.8, style: .slideUp) + .padding(.bottom, 30) + + // 저장 버튼 + saveButton + .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 + ) + + KeychyAlert( + type: .imageSave, + message: "영상이 저장되었어요!", + isPresented: $showVideoSaved + ) + } + + /// 로딩 오버레이 + @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) + } + } +} + // MARK: - KeyringScene Section extension KeyringCompleteView { private var keyringScene: some View { @@ -193,6 +206,7 @@ extension KeyringCompleteView { Text(viewModel.nameText) .typography(getBottomPadding(0) == 0 ? .malang24B : .malang26B) .foregroundStyle(.black100) + .padding(.bottom, 2) Text(formattedDate(date: viewModel.createdAt)) .typography(.suit14M) @@ -266,3 +280,14 @@ extension KeyringCompleteView { } } } + +// MARK: - Preview +#Preview { + NavigationStack { + KeyringCompleteView( + router: NavigationRouter(), + viewModel: PolaroidVM(), + navigationTitle: "키링 완성" + ) + } +} From a15c4f7fdd238b8276ab76b3003a21d9bba4abe5 Mon Sep 17 00:00:00 2001 From: giljihun Date: Thu, 29 Jan 2026 11:43:28 +0900 Subject: [PATCH 04/13] =?UTF-8?q?feat:=20TabbarManager=20-=20=ED=83=AD=20?= =?UTF-8?q?=EC=A0=84=ED=99=98=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Keychy/Core/Navigation/TabBarManager.swift | 14 ++++++++++++++ .../Actions/share.imageset/Contents.json | 12 ++++++++++++ .../18. Icons/Actions/share.imageset/share.pdf | Bin 0 -> 4531 bytes 3 files changed, 26 insertions(+) create mode 100644 Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Actions/share.imageset/Contents.json create mode 100644 Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Actions/share.imageset/share.pdf diff --git a/Keychy/Keychy/Core/Navigation/TabBarManager.swift b/Keychy/Keychy/Core/Navigation/TabBarManager.swift index 5331ba74..dfebf2e3 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/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 00000000..462b2d90 --- /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 0000000000000000000000000000000000000000..0eb3bca2957882343daf90d0cc22034c7d120206 GIT binary patch literal 4531 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 literal 0 HcmV?d00001 From 0090d18547cdc12bf5991fcb83beaf73b47ed554 Mon Sep 17 00:00:00 2001 From: giljihun Date: Thu, 29 Jan 2026 11:43:46 +0900 Subject: [PATCH 05/13] =?UTF-8?q?style:=20=EC=99=84=EC=84=B1=EB=B7=B0=20-?= =?UTF-8?q?=20=EC=95=A1=EC=85=98=20=EB=B2=84=ED=8A=BC=20=EC=B6=94=EC=83=81?= =?UTF-8?q?=ED=99=94=20=EB=B0=8F=20=ED=95=98=EC=9D=B4=ED=8C=8C=EC=9D=B4=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Shared/Views/KeyringCompleteView.swift | 159 ++++++++++-------- 1 file changed, 92 insertions(+), 67 deletions(-) diff --git a/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringCompleteView.swift b/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringCompleteView.swift index 74303fe4..1a3bcb4f 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringCompleteView.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringCompleteView.swift @@ -14,24 +14,24 @@ 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 - + // 비디오 생성기 let videoGenerator = KeyringVideoGenerator() @@ -39,13 +39,13 @@ struct KeyringCompleteView: View { ZStack { // 1. 배경 backgroundView - + // 2. 메인 컨텐츠 mainContent - + // 3. Alerts 오버레이 alertsOverlay - + // 4. 로딩 오버레이 loadingOverlay } @@ -53,6 +53,7 @@ struct KeyringCompleteView: View { .toolbar { closeToolbarItem titleToolbarItem + collectionToolbarItem } .onAppear { checkReviewTriggers() @@ -71,7 +72,7 @@ extension KeyringCompleteView { .cinematicAppear(delay: 0, duration: 0.6, style: .fadeIn) .blur(radius: isAlertShowing ? 15 : 0) } - + /// 메인 컨텐츠 (키링씬 + 정보 + 버튼) private var mainContent: some View { GeometryReader { geometry in @@ -82,14 +83,14 @@ extension KeyringCompleteView { .cinematicAppear(delay: 0.2, duration: 0.8, style: .full) .border(.red) .offset(x: 0, y: -20) - + // 키링 정보 keyringInfo .cinematicAppear(delay: 0.6, duration: 0.8, style: .slideUp) .padding(.bottom, 30) - - // 저장 버튼 - saveButton + + // 액션 버튼 + actionButtons .cinematicAppear(delay: 1.0, duration: 0.8, style: .fadeIn) .opacity(isCapturingImage ? 0 : 1) } @@ -97,7 +98,7 @@ extension KeyringCompleteView { } .blur(radius: isAlertShowing ? 15 : 0) } - + /// Alerts 오버레이 @ViewBuilder private var alertsOverlay: some View { @@ -106,30 +107,30 @@ extension KeyringCompleteView { message: "이미지가 저장되었어요!", isPresented: $showImageSaved ) - + KeychyAlert( type: .imageSave, message: "영상이 저장되었어요!", isPresented: $showVideoSaved ) } - + /// 로딩 오버레이 @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)) @@ -166,12 +167,12 @@ extension KeyringCompleteView { private var isAlertShowing: Bool { showImageSaved || showVideoSaved || isGeneratingVideo } - + var closeToolbarItem: some ToolbarContent { ToolbarItem(placement: .topBarLeading) { Button { viewModel.resetAll() - + // Festival에서 온 경우 콜백 실행 if let onCloseFromFestival = onCloseFromFestival { onCloseFromFestival(router) @@ -188,15 +189,41 @@ extension KeyringCompleteView { } .sharedBackgroundVisibility(isAlertShowing ? .hidden : .visible) } - + var titleToolbarItem: some ToolbarContent { ToolbarItem(placement: .principal) { Text("키링 완성!") - .typography(.suit17B) + .typography(.notosans17M) .foregroundStyle(.black100) .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() + } + } } // MARK: - 키링 정보 뷰 @@ -216,7 +243,7 @@ extension KeyringCompleteView { if let nickname = userManager.currentUser?.nickname { Text("@\(nickname)") .typography(getBottomPadding(0) == 0 ? .notosans12R : .notosans14R) - .foregroundStyle(.black100) + .foregroundStyle(.gray500) .padding(.vertical, 1) } } @@ -230,53 +257,51 @@ extension KeyringCompleteView { } } -// MARK: - 저장 버튼 +// MARK: - 버튼 extension KeyringCompleteView { - private var saveButton: some View { - HStack(spacing: 20) { - // 이미지 저장 버튼 - VStack(spacing: 9) { - Button(action: { - captureAndSaveImage() - }) { - Image(.imageDownload) - } - .frame( - width: getBottomPadding(0) == 0 ? 55 : 65, - height: getBottomPadding(0) == 0 ? 55 : 65 - ) - .buttonStyle(.plain) - .glassEffect(.regular.interactive(), in: .circle) - - Text("이미지 저장") - .typography(.suit13SB) + /// 버튼 사이즈 (디바이스별) + 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) } - - // 영상 생성 버튼 - VStack(spacing: 9) { - Button(action: { - Task { - await generateAndSaveVideo() - } - }) { - 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) + .frame(width: 74, height: 47) + .padding(.vertical, 11.5) + .padding(.horizontal, 8) + } + .buttonStyle(.plain) + .glassEffect(.clear.interactive(), in: .rect(cornerRadius: 24)) + } + + /// 하단 액션 버튼 영역 + private var actionButtons: some View { + HStack(spacing: 17) { + // 이미지 저장 + actionButton(image: .save, title: "이미지 저장") { + captureAndSaveImage() + } + + // 공유 + actionButton(image: .share, title: "공유") { + // TODO: 공유 기능 + } + + // 선물하기 + actionButton(image: .present, title: "선물하기") { + // TODO: 선물하기 기능 } - .opacity(isGeneratingVideo ? 0.5 : 1) } } } From 3c76bd0fff2bd2e97cdc4dc44981150b6a1e0ecc Mon Sep 17 00:00:00 2001 From: giljihun Date: Thu, 29 Jan 2026 12:24:19 +0900 Subject: [PATCH 06/13] =?UTF-8?q?feat:=20KeyringViewModelProtocol=EC=97=90?= =?UTF-8?q?=20savedKeyringDocumentId=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 프로토콜에 저장된 키링 Document ID 프로퍼티 추가 - 6개 템플릿 VM에 프로퍼티 구현 - KeyringAdapter, WelcomeKeyringViewModel 프로토콜 준수 - Firebase 저장 후 Document ID 저장 로직 추가 --- .../Core/Video/Keyring/KeyringVideoGenerator+Keyring.swift | 2 ++ .../Intro/ViewModels/WelcomeKeyringViewModel.swift | 2 ++ .../Shared/Protocols/KeyringViewModelProtocol+Reset.swift | 1 + .../Shared/Protocols/KeyringViewModelProtocol.swift | 3 +++ .../Shared/Views/KeyringInfoInputView+FirebaseSave.swift | 3 +++ .../Templates/AcrylicPhoto/ViewModels/AcrylicPhotoVM.swift | 1 + .../Templates/ClearSketch/ViewModels/ClearSketchVM.swift | 1 + .../Templates/NeonSign/ViewModels/NeonSignVM.swift | 1 + .../KeyringMaker/Templates/Pixel/ViewModels/PixelVM.swift | 1 + .../Templates/Polaroid/ViewModels/PolaroidVM.swift | 1 + .../Templates/SpeechBubble/ViewModels/SpeechBubbleVM.swift | 3 ++- 11 files changed, 18 insertions(+), 1 deletion(-) diff --git a/Keychy/Keychy/Core/Video/Keyring/KeyringVideoGenerator+Keyring.swift b/Keychy/Keychy/Core/Video/Keyring/KeyringVideoGenerator+Keyring.swift index 2fbb648f..b5a43483 100644 --- a/Keychy/Keychy/Core/Video/Keyring/KeyringVideoGenerator+Keyring.swift +++ b/Keychy/Keychy/Core/Video/Keyring/KeyringVideoGenerator+Keyring.swift @@ -161,6 +161,8 @@ private class KeyringAdapter: KeyringViewModelProtocol { func isInCache(particleId: String) -> Bool { false } func downloadSound(_ sound: Sound) async { } func downloadParticle(_ particle: Particle) async { } + var savedKeyringDocumentId: String? + func resetCustomizingData() { } func resetInfoData() { } func resetAll() { } diff --git a/Keychy/Keychy/Presentation/Intro/ViewModels/WelcomeKeyringViewModel.swift b/Keychy/Keychy/Presentation/Intro/ViewModels/WelcomeKeyringViewModel.swift index 5400aea7..33798fa9 100644 --- a/Keychy/Keychy/Presentation/Intro/ViewModels/WelcomeKeyringViewModel.swift +++ b/Keychy/Keychy/Presentation/Intro/ViewModels/WelcomeKeyringViewModel.swift @@ -42,6 +42,8 @@ class WelcomeKeyringViewModel: KeyringViewModelProtocol { var particleId: String = "Confetti" var effectSubject = PassthroughSubject<(soundId: String, particleId: String, type: KeyringUpdateType), Never>() + var savedKeyringDocumentId: 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 bbc2a094..f0e25428 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Shared/Protocols/KeyringViewModelProtocol+Reset.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Shared/Protocols/KeyringViewModelProtocol+Reset.swift @@ -36,5 +36,6 @@ extension KeyringViewModelProtocol { func resetAll() { resetCustomizingData() resetInfoData() + savedKeyringDocumentId = nil } } diff --git a/Keychy/Keychy/Presentation/KeyringMaker/Shared/Protocols/KeyringViewModelProtocol.swift b/Keychy/Keychy/Presentation/KeyringMaker/Shared/Protocols/KeyringViewModelProtocol.swift index fc6fc356..f401c0b3 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Shared/Protocols/KeyringViewModelProtocol.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Shared/Protocols/KeyringViewModelProtocol.swift @@ -37,6 +37,9 @@ protocol KeyringViewModelProtocol: AnyObject, Observable { var maxMemoCount: Int { get } var createdAt: Date { get set } + /// 저장된 키링 Document ID (Firebase 저장 후 설정됨) + var savedKeyringDocumentId: String? { get set } + /// 태그 관련 var selectedTags: [String] { get set } diff --git a/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringInfoInputView+FirebaseSave.swift b/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringInfoInputView+FirebaseSave.swift index b25439bf..6a790333 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/Templates/AcrylicPhoto/ViewModels/AcrylicPhotoVM.swift b/Keychy/Keychy/Presentation/KeyringMaker/Templates/AcrylicPhoto/ViewModels/AcrylicPhotoVM.swift index b62c196a..f563c897 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Templates/AcrylicPhoto/ViewModels/AcrylicPhotoVM.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Templates/AcrylicPhoto/ViewModels/AcrylicPhotoVM.swift @@ -125,6 +125,7 @@ class AcrylicPhotoVM: KeyringViewModelProtocol { var maxMemoCount: Int = 500 var selectedTags: [String] = [] var createdAt: Date = Date() + var savedKeyringDocumentId: 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 d439195b..da813cf2 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Templates/ClearSketch/ViewModels/ClearSketchVM.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Templates/ClearSketch/ViewModels/ClearSketchVM.swift @@ -130,6 +130,7 @@ class ClearSketchVM: KeyringViewModelProtocol { var maxMemoCount: Int = 500 var selectedTags: [String] = [] var createdAt: Date = Date() + var savedKeyringDocumentId: 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 449cdfeb..57f8a4ec 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Templates/NeonSign/ViewModels/NeonSignVM.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Templates/NeonSign/ViewModels/NeonSignVM.swift @@ -109,6 +109,7 @@ class NeonSignVM: KeyringViewModelProtocol { var maxMemoCount: Int = 500 var selectedTags: [String] = [] var createdAt: Date = Date() + var savedKeyringDocumentId: 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 f3d28f25..f04998de 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Templates/Pixel/ViewModels/PixelVM.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Templates/Pixel/ViewModels/PixelVM.swift @@ -114,6 +114,7 @@ class PixelVM: KeyringViewModelProtocol { var maxMemoCount: Int = 500 var selectedTags: [String] = [] var createdAt: Date = Date() + var savedKeyringDocumentId: 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 11951e3b..f57756be 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Templates/Polaroid/ViewModels/PolaroidVM.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Templates/Polaroid/ViewModels/PolaroidVM.swift @@ -120,6 +120,7 @@ class PolaroidVM: KeyringViewModelProtocol { var maxMemoCount: Int = 500 var selectedTags: [String] = [] var createdAt: Date = Date() + var savedKeyringDocumentId: 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 f19933b5..0fb2f061 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Templates/SpeechBubble/ViewModels/SpeechBubbleVM.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Templates/SpeechBubble/ViewModels/SpeechBubbleVM.swift @@ -72,7 +72,8 @@ class SpeechBubbleVM: KeyringViewModelProtocol { var maxMemoCount: Int = 500 var selectedTags: [String] = [] var createdAt: Date = Date() - + var savedKeyringDocumentId: String? + // MARK: - Dependencies var userManager: UserManager var errorMessage: String? From b96497aaa9f790b7b47df78bd838edb38e3d559d Mon Sep 17 00:00:00 2001 From: giljihun Date: Thu, 29 Jan 2026 12:24:59 +0900 Subject: [PATCH 07/13] =?UTF-8?q?feat:=20Workshop=20=EC=84=A0=EB=AC=BC?= =?UTF-8?q?=ED=95=98=EA=B8=B0=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - KeyringPackageManager 생성 (공용 포장 로직) - KeyringPackageCompleteView 추가 (Workshop용) - WorkshopRoute에 packageComplete 라우트 추가 - WorkshopTab에 네비게이션 연결 --- Keychy/Keychy.xcodeproj/project.pbxproj | 8 + .../Core/Firebase/KeyringPackageManager.swift | 176 ++++++++++++++ .../Navigation/Routes/WorkshopRoute.swift | 3 + .../Shared/Views/KeyringCompleteView.swift | 111 ++++++++- .../Views/KeyringPackageCompleteView.swift | 229 ++++++++++++++++++ .../Presentation/Tab/Views/WorkshopTab.swift | 8 + 6 files changed, 530 insertions(+), 5 deletions(-) create mode 100644 Keychy/Keychy/Core/Firebase/KeyringPackageManager.swift create mode 100644 Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringPackageCompleteView.swift diff --git a/Keychy/Keychy.xcodeproj/project.pbxproj b/Keychy/Keychy.xcodeproj/project.pbxproj index c8b6ccf6..2e4fdb32 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 = ""; @@ -1216,6 +1221,7 @@ 4C4733482F1FA388005D2376 /* KeyringInfoInputView+Helpers.swift */, 4C4733492F1FA388005D2376 /* KeyringInfoInputView+Sheet.swift */, 4C47334A2F1FA388005D2376 /* KeyringInfoInputView+TagManagement.swift */, + 4C7A9EC82F2B0586008B520C /* KeyringPackageCompleteView.swift */, ); path = Views; sourceTree = ""; @@ -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/Firebase/KeyringPackageManager.swift b/Keychy/Keychy/Core/Firebase/KeyringPackageManager.swift new file mode 100644 index 00000000..b38624df --- /dev/null +++ b/Keychy/Keychy/Core/Firebase/KeyringPackageManager.swift @@ -0,0 +1,176 @@ +// +// KeyringPackageManager.swift +// Keychy +// +// 키링 선물 포장 로직 (Collection, Workshop 공용) +// + +import Foundation +import FirebaseFirestore + +/// 키링 선물 포장 관리자 +enum KeyringPackageManager { + + // MARK: - 키링 포장하기 + + /// 키링을 선물용으로 포장합니다. + /// - Parameters: + /// - uid: 사용자 ID + /// - keyringDocumentId: 키링 Firestore Document ID + /// - completion: 완료 콜백 (성공 여부, PostOffice ID) + static func packageKeyring( + uid: String, + keyringDocumentId: String, + completion: @escaping (Bool, 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) + 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?) -> 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) + return + } + + print("[Package] 공유 링크 생성: \(shareLink.absoluteString)") + + // PostOffice 문서 데이터 + let postOfficeData: [String: Any] = [ + "type": "receive", + "senderId": uid, + "keyringId": keyringDocumentId, + "shareLink": shareLink.absoluteString, + "createdAt": Timestamp(date: Date()) + ] + + // 문서 생성 + postOfficeRef.setData(postOfficeData) { error in + if let error = error { + print("[Package] PostOffice 문서 생성 실패: \(error.localizedDescription)") + completion(false, nil) + return + } + + print("[Package] PostOffice 문서 생성 완료: \(postOfficeId)") + + // Bundle에서 키링 제거 + removeKeyringFromBundles(db: db, uid: uid, keyringDocumentId: keyringDocumentId) { _ in + completion(true, postOfficeId) + } + } + } + } + + /// 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 911cc301..ef657dd1 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) + /// template.id 문자열을 WorkshopRoute로 변환 static func from(string: String) -> WorkshopRoute? { switch string { diff --git a/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringCompleteView.swift b/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringCompleteView.swift index 1a3bcb4f..4ab8bd16 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringCompleteView.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringCompleteView.swift @@ -31,7 +31,11 @@ struct KeyringCompleteView: View { // 씬 인터랙션 @State var isInteractionEnabled = false - + + // 선물 포장 + @State var showPackageAlert = false + @State var showPackingAlert = false + // 비디오 생성기 let videoGenerator = KeyringVideoGenerator() @@ -81,7 +85,6 @@ extension KeyringCompleteView { keyringScene .frame(height: geometry.size.height * 0.53) .cinematicAppear(delay: 0.2, duration: 0.8, style: .full) - .border(.red) .offset(x: 0, y: -20) // 키링 정보 @@ -107,12 +110,44 @@ extension KeyringCompleteView { message: "이미지가 저장되었어요!", isPresented: $showImageSaved ) - + KeychyAlert( type: .imageSave, message: "영상이 저장되었어요!", isPresented: $showVideoSaved ) + + // 선물 포장 확인 팝업 + if showPackageAlert { + Color.black20 + .ignoresSafeArea() + .zIndex(99) + + PackagePopup( + onCancel: { + withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { + showPackageAlert = false + } + }, + onConfirm: { + handlePackageConfirm() + } + ) + .zIndex(100) + } + + // 포장 중 로딩 + if showPackingAlert { + Color.black20 + .ignoresSafeArea() + .zIndex(99) + + LoadingAlert( + type: .longWithPresent, + message: "선물 포장 중.." + ) + .zIndex(101) + } } /// 로딩 오버레이 @@ -165,7 +200,7 @@ extension KeyringCompleteView { extension KeyringCompleteView { /// Alert 표시 중 여부 private var isAlertShowing: Bool { - showImageSaved || showVideoSaved || isGeneratingVideo + showImageSaved || showVideoSaved || isGeneratingVideo || showPackageAlert || showPackingAlert } var closeToolbarItem: some ToolbarContent { @@ -300,7 +335,73 @@ extension KeyringCompleteView { // 선물하기 actionButton(image: .present, title: "선물하기") { - // TODO: 선물하기 기능 + // 네트워크 체크 + 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 uid = userManager.currentUser?.id, + let keyringDocumentId = viewModel.savedKeyringDocumentId else { + print("[Package] uid 또는 keyringDocumentId 없음") + return + } + + // 포장 중 로딩 표시 + 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 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 { + // 성공 - 포장 완료 화면으로 이동 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + router.push(.packageComplete( + keyringDocumentId: keyringDocumentId, + postOfficeId: postOfficeId + )) + + // 네비게이션 애니메이션 완료 후 리셋 (뒤에 있는 뷰가 보이지 않을 때) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + viewModel.resetAll() + } + } + } else { + print("[Package] 포장 실패") + } + } } } } 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 00000000..0f8a46de --- /dev/null +++ b/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringPackageCompleteView.swift @@ -0,0 +1,229 @@ +// +// KeyringPackageCompleteView.swift +// Keychy +// +// 키링 선물 포장 완료 화면 (Workshop용) +// + +import SwiftUI +import FirebaseFirestore + +struct KeyringPackageCompleteView: View { + @Bindable var router: NavigationRouter + + let keyringDocumentId: String + let postOfficeId: String + + @State private var keyring: Keyring? + @State private var authorName: String = "" + @State private var shareLink: 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() + loadShareLink() + } + } + + 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: 0) { + Text("키링 포장이 완료되었어요!") + .typography(.suit20B) + .foregroundColor(.black100) + .padding(.bottom, 9) + + Text("링크나 QR로 바로 공유할 수 있어요.") + .typography(.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 { + TabBarManager.show() + router.reset() + } label: { + Image(.dismiss) + .foregroundColor(.primary) + } + .frame(width: 44, height: 44) + .glassEffect(.regular.interactive(), in: .circle) + } center: { + Spacer() + } trailing: { + Spacer() + } + } +} + +// MARK: - Data Loading +extension KeyringPackageCompleteView { + private func loadKeyringData() { + let db = Firestore.firestore() + + db.collection("Keyring") + .document(keyringDocumentId) + .getDocument { snapshot, error in + if let error = error { + print("[PackageComplete] 키링 로드 실패: \(error.localizedDescription)") + return + } + + guard let data = snapshot?.data() else { + print("[PackageComplete] 키링 데이터 없음") + return + } + + // Keyring 파싱 + if let keyring = Keyring(documentId: keyringDocumentId, data: data) { + self.keyring = keyring + loadAuthorName(authorId: keyring.authorId) + } + } + } + + 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 = "알 수 없음" + } + } + } + + private func loadShareLink() { + let db = Firestore.firestore() + + db.collection("PostOffice") + .document(postOfficeId) + .getDocument { snapshot, error in + if let data = snapshot?.data(), + let link = data["shareLink"] as? String { + self.shareLink = link + } + } + } +} diff --git a/Keychy/Keychy/Presentation/Tab/Views/WorkshopTab.swift b/Keychy/Keychy/Presentation/Tab/Views/WorkshopTab.swift index d74c2f2a..fc2f42c8 100644 --- a/Keychy/Keychy/Presentation/Tab/Views/WorkshopTab.swift +++ b/Keychy/Keychy/Presentation/Tab/Views/WorkshopTab.swift @@ -238,6 +238,14 @@ struct WorkshopTab: View { } : nil ) + // MARK: - 선물 포장 완료 + case .packageComplete(let keyringDocumentId, let postOfficeId): + KeyringPackageCompleteView( + router: router, + keyringDocumentId: keyringDocumentId, + postOfficeId: postOfficeId + ) + // MARK: - Bundle case .bundleInventoryView: BundleInventoryView(router: router, collectionVM: collectionViewModel, bundleVM: bundleViewModel) From cd359dcd444d3634d274ce6f96a0a6e3a3eb2e42 Mon Sep 17 00:00:00 2001 From: giljihun Date: Thu, 29 Jan 2026 12:25:19 +0900 Subject: [PATCH 08/13] =?UTF-8?q?refactor:=20CollectionViewModel=EC=9D=B4?= =?UTF-8?q?=20KeyringPackageManager=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 중복된 포장 로직 제거 (~70줄) - KeyringPackageManager.packageKeyring() 위임 --- .../CollectionViewModel+Package.swift | 189 ++---------------- 1 file changed, 20 insertions(+), 169 deletions(-) diff --git a/Keychy/Keychy/Presentation/Collection/ViewModels/CollectionViewModel+Package.swift b/Keychy/Keychy/Presentation/Collection/ViewModels/CollectionViewModel+Package.swift index b26e9af1..3b17b7ad 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 + 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 데이터 가져오기 From 10aa9f95df66e17e79fa8311703ff5f41e258b75 Mon Sep 17 00:00:00 2001 From: giljihun Date: Thu, 29 Jan 2026 13:49:10 +0900 Subject: [PATCH 09/13] =?UTF-8?q?fix:=20=EC=84=A0=EB=AC=BC=20=ED=8F=AC?= =?UTF-8?q?=EC=9E=A5=20=EC=8B=9C,=20QR=EC=BD=94=EB=93=9C=EA=B0=80=20?= =?UTF-8?q?=EC=95=88=EB=B3=B4=EC=9D=B4=EB=8D=98=20=ED=83=80=EC=9D=B4?= =?UTF-8?q?=EB=B0=8D=20=EC=9D=B4=EC=8A=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - shareLink를 직접 전달해서 로딩 이슈 해결을 하고자함 --- .../Core/Firebase/KeyringPackageManager.swift | 19 ++--- .../Navigation/Routes/WorkshopRoute.swift | 2 +- .../CollectionViewModel+Package.swift | 2 +- .../Views/Detail/PackagedKeyringView.swift | 6 +- .../Views/KeyringPackageCompleteView.swift | 66 +++++++++++------- .../Presentation/Tab/Views/WorkshopTab.swift | 26 ++++++- .../homeBlack.imageset/Contents.json | 12 ++++ .../homeBlack.imageset/homeBlack.pdf | Bin 0 -> 4850 bytes 8 files changed, 94 insertions(+), 39 deletions(-) create mode 100644 Keychy/Keychy/Resources/Assets.xcassets/14. Package/homeBlack.imageset/Contents.json create mode 100644 Keychy/Keychy/Resources/Assets.xcassets/14. Package/homeBlack.imageset/homeBlack.pdf diff --git a/Keychy/Keychy/Core/Firebase/KeyringPackageManager.swift b/Keychy/Keychy/Core/Firebase/KeyringPackageManager.swift index b38624df..c0511bfc 100644 --- a/Keychy/Keychy/Core/Firebase/KeyringPackageManager.swift +++ b/Keychy/Keychy/Core/Firebase/KeyringPackageManager.swift @@ -17,11 +17,11 @@ enum KeyringPackageManager { /// - Parameters: /// - uid: 사용자 ID /// - keyringDocumentId: 키링 Firestore Document ID - /// - completion: 완료 콜백 (성공 여부, PostOffice ID) + /// - completion: 완료 콜백 (성공 여부, PostOffice ID, Share Link) static func packageKeyring( uid: String, keyringDocumentId: String, - completion: @escaping (Bool, String?) -> Void + completion: @escaping (Bool, String?, String?) -> Void ) { let db = Firestore.firestore() @@ -31,7 +31,7 @@ enum KeyringPackageManager { .updateData(["isPackaged": true]) { error in if let error = error { print("[Package] Keyring 상태 업데이트 실패: \(error.localizedDescription)") - completion(false, nil) + completion(false, nil, nil) return } @@ -54,7 +54,7 @@ enum KeyringPackageManager { db: Firestore, uid: String, keyringDocumentId: String, - completion: @escaping (Bool, String?) -> Void + completion: @escaping (Bool, String?, String?) -> Void ) { let postOfficeRef = db.collection("PostOffice").document() let postOfficeId = postOfficeRef.documentID @@ -72,18 +72,19 @@ enum KeyringPackageManager { // 공유 링크 생성 guard let shareLink = DeepLinkManager.createShareLink(postOfficeId: postOfficeId) else { print("[Package] 공유 링크 생성 실패") - completion(false, nil) + completion(false, nil, nil) return } - print("[Package] 공유 링크 생성: \(shareLink.absoluteString)") + let shareLinkString = shareLink.absoluteString + print("[Package] 공유 링크 생성: \(shareLinkString)") // PostOffice 문서 데이터 let postOfficeData: [String: Any] = [ "type": "receive", "senderId": uid, "keyringId": keyringDocumentId, - "shareLink": shareLink.absoluteString, + "shareLink": shareLinkString, "createdAt": Timestamp(date: Date()) ] @@ -91,7 +92,7 @@ enum KeyringPackageManager { postOfficeRef.setData(postOfficeData) { error in if let error = error { print("[Package] PostOffice 문서 생성 실패: \(error.localizedDescription)") - completion(false, nil) + completion(false, nil, nil) return } @@ -99,7 +100,7 @@ enum KeyringPackageManager { // Bundle에서 키링 제거 removeKeyringFromBundles(db: db, uid: uid, keyringDocumentId: keyringDocumentId) { _ in - completion(true, postOfficeId) + completion(true, postOfficeId, shareLinkString) } } } diff --git a/Keychy/Keychy/Core/Navigation/Routes/WorkshopRoute.swift b/Keychy/Keychy/Core/Navigation/Routes/WorkshopRoute.swift index ef657dd1..5995cd38 100644 --- a/Keychy/Keychy/Core/Navigation/Routes/WorkshopRoute.swift +++ b/Keychy/Keychy/Core/Navigation/Routes/WorkshopRoute.swift @@ -69,7 +69,7 @@ enum WorkshopRoute: Hashable, BundleRoute { case speechBubbleComplete // MARK: - 선물 포장 완료 - case packageComplete(keyringDocumentId: String, postOfficeId: String) + case packageComplete(keyringDocumentId: String, postOfficeId: String, templateId: String, shareLink: String) /// template.id 문자열을 WorkshopRoute로 변환 static func from(string: String) -> WorkshopRoute? { diff --git a/Keychy/Keychy/Presentation/Collection/ViewModels/CollectionViewModel+Package.swift b/Keychy/Keychy/Presentation/Collection/ViewModels/CollectionViewModel+Package.swift index 3b17b7ad..617d85d5 100644 --- a/Keychy/Keychy/Presentation/Collection/ViewModels/CollectionViewModel+Package.swift +++ b/Keychy/Keychy/Presentation/Collection/ViewModels/CollectionViewModel+Package.swift @@ -27,7 +27,7 @@ extension CollectionViewModel { KeyringPackageManager.packageKeyring( uid: uid, keyringDocumentId: documentId - ) { [weak self] success, postOfficeId in + ) { [weak self] success, postOfficeId, _ in // shareLink는 Collection에서 미사용 guard let self = self else { completion(false, nil) return diff --git a/Keychy/Keychy/Presentation/Collection/Views/Detail/PackagedKeyringView.swift b/Keychy/Keychy/Presentation/Collection/Views/Detail/PackagedKeyringView.swift index a5b29449..b7683256 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/KeyringMaker/Shared/Views/KeyringPackageCompleteView.swift b/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringPackageCompleteView.swift index 0f8a46de..4cb92a16 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringPackageCompleteView.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringPackageCompleteView.swift @@ -10,13 +10,14 @@ 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 shareLink: String = "" @State private var isLoading: Bool = true @State private var showLinkCopied: Bool = false @State private var showImageSaved: Bool = false @@ -82,7 +83,6 @@ struct KeyringPackageCompleteView: View { .onAppear { TabBarManager.hide() loadKeyringData() - loadShareLink() } } @@ -110,15 +110,17 @@ extension KeyringPackageCompleteView { .adaptiveTopPadding() // 헤더 텍스트 - VStack(spacing: 0) { + VStack(spacing: 15) { Text("키링 포장이 완료되었어요!") .typography(.suit20B) .foregroundColor(.black100) - .padding(.bottom, 9) + .padding(.top, 10) - Text("링크나 QR로 바로 공유할 수 있어요.") - .typography(.suit16M) + Text("링크나 QR로 바로 공유할 수 있어요\n포장은 보관함에서 풀 수 있습니다") + .font(.suit16M) .foregroundColor(.black100) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) } .padding(.top, isSmallScreen ? -70 : 78) @@ -155,20 +157,36 @@ extension KeyringPackageCompleteView { private var customNavigationBar: some View { CustomNavigationBar { - // Leading - 닫기 버튼 + // Leading - 뒤로가기 버튼 (키링 완성뷰로) Button { - TabBarManager.show() - router.reset() + router.pop() } label: { - Image(.dismiss) - .foregroundColor(.primary) + Image(.backIcon) } .frame(width: 44, height: 44) .glassEffect(.regular.interactive(), in: .circle) } center: { Spacer() } trailing: { - Spacer() + // 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() } } } @@ -178,23 +196,33 @@ 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] 키링 데이터 없음") + 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 } } } @@ -214,16 +242,4 @@ extension KeyringPackageCompleteView { } } - private func loadShareLink() { - let db = Firestore.firestore() - - db.collection("PostOffice") - .document(postOfficeId) - .getDocument { snapshot, error in - if let data = snapshot?.data(), - let link = data["shareLink"] as? String { - self.shareLink = link - } - } - } } diff --git a/Keychy/Keychy/Presentation/Tab/Views/WorkshopTab.swift b/Keychy/Keychy/Presentation/Tab/Views/WorkshopTab.swift index fc2f42c8..0a4b37f1 100644 --- a/Keychy/Keychy/Presentation/Tab/Views/WorkshopTab.swift +++ b/Keychy/Keychy/Presentation/Tab/Views/WorkshopTab.swift @@ -239,11 +239,13 @@ struct WorkshopTab: View { ) // MARK: - 선물 포장 완료 - case .packageComplete(let keyringDocumentId, let postOfficeId): + case .packageComplete(let keyringDocumentId, let postOfficeId, let templateId, let shareLink): KeyringPackageCompleteView( router: router, + viewModel: getViewModelForTemplate(templateId), keyringDocumentId: keyringDocumentId, - postOfficeId: postOfficeId + postOfficeId: postOfficeId, + shareLink: shareLink ) // MARK: - Bundle @@ -319,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/14. Package/homeBlack.imageset/Contents.json b/Keychy/Keychy/Resources/Assets.xcassets/14. Package/homeBlack.imageset/Contents.json new file mode 100644 index 00000000..c9df2d69 --- /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 0000000000000000000000000000000000000000..511da05c32d6e08e4b264adf8f1a904f0b85f5ed GIT binary patch literal 4850 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 Xu{42*_9pJxN}6IC;HgvU1{(hX3w+I> literal 0 HcmV?d00001 From 99ca7165510dba2ee58ffaf8b4b60c2e6b00187d Mon Sep 17 00:00:00 2001 From: giljihun Date: Thu, 29 Jan 2026 13:50:31 +0900 Subject: [PATCH 10/13] =?UTF-8?q?feat:=20=ED=82=A4=EB=A7=81=EC=99=84?= =?UTF-8?q?=EC=84=B1=EB=B7=B0=20->=20=EC=84=A0=EB=AC=BC=ED=8F=AC=EC=9E=A5?= =?UTF-8?q?=20->=20=EB=92=A4=EB=A1=9C=EA=B0=80=EA=B8=B0=20->=20=EC=84=A0?= =?UTF-8?q?=EB=AC=BC=ED=8F=AC=EC=9E=A5=20->=20=EB=8B=A4=EC=8B=9C=20?= =?UTF-8?q?=ED=8F=AC=EC=9E=A5=20=EB=B0=A9=EC=A7=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 이미 포장된 경우 팝업 없이, 바로 포장완료뷰로 이동 - 포장 실패 네트워크 토스트도 추가 - KeyringViewModelProtocol에 packagedPostOfficeId, packagedShareLink 추가함 --- Keychy/Keychy.xcodeproj/project.pbxproj | 2 +- .../KeyringVideoGenerator+Keyring.swift | 2 + .../ViewModels/WelcomeKeyringViewModel.swift | 2 + .../KeyringViewModelProtocol+Reset.swift | 2 + .../Protocols/KeyringViewModelProtocol.swift | 4 ++ .../Shared/Views/KeyringCompleteView.swift | 56 +++++++++++++++---- .../ViewModels/AcrylicPhotoVM.swift | 2 + .../ViewModels/ClearSketchVM.swift | 2 + .../NeonSign/ViewModels/NeonSignVM.swift | 2 + .../Templates/Pixel/ViewModels/PixelVM.swift | 2 + .../Polaroid/ViewModels/PolaroidVM.swift | 2 + .../ViewModels/SpeechBubbleVM.swift | 2 + 12 files changed, 68 insertions(+), 12 deletions(-) diff --git a/Keychy/Keychy.xcodeproj/project.pbxproj b/Keychy/Keychy.xcodeproj/project.pbxproj index 2e4fdb32..8c3e8cf2 100644 --- a/Keychy/Keychy.xcodeproj/project.pbxproj +++ b/Keychy/Keychy.xcodeproj/project.pbxproj @@ -1212,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 */, @@ -1221,7 +1222,6 @@ 4C4733482F1FA388005D2376 /* KeyringInfoInputView+Helpers.swift */, 4C4733492F1FA388005D2376 /* KeyringInfoInputView+Sheet.swift */, 4C47334A2F1FA388005D2376 /* KeyringInfoInputView+TagManagement.swift */, - 4C7A9EC82F2B0586008B520C /* KeyringPackageCompleteView.swift */, ); path = Views; sourceTree = ""; diff --git a/Keychy/Keychy/Core/Video/Keyring/KeyringVideoGenerator+Keyring.swift b/Keychy/Keychy/Core/Video/Keyring/KeyringVideoGenerator+Keyring.swift index b5a43483..50c24ea4 100644 --- a/Keychy/Keychy/Core/Video/Keyring/KeyringVideoGenerator+Keyring.swift +++ b/Keychy/Keychy/Core/Video/Keyring/KeyringVideoGenerator+Keyring.swift @@ -162,6 +162,8 @@ private class KeyringAdapter: KeyringViewModelProtocol { func downloadSound(_ sound: Sound) async { } func downloadParticle(_ particle: Particle) async { } var savedKeyringDocumentId: String? + var packagedPostOfficeId: String? + var packagedShareLink: String? func resetCustomizingData() { } func resetInfoData() { } diff --git a/Keychy/Keychy/Presentation/Intro/ViewModels/WelcomeKeyringViewModel.swift b/Keychy/Keychy/Presentation/Intro/ViewModels/WelcomeKeyringViewModel.swift index 33798fa9..ae1ec8c5 100644 --- a/Keychy/Keychy/Presentation/Intro/ViewModels/WelcomeKeyringViewModel.swift +++ b/Keychy/Keychy/Presentation/Intro/ViewModels/WelcomeKeyringViewModel.swift @@ -43,6 +43,8 @@ class WelcomeKeyringViewModel: KeyringViewModelProtocol { 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 diff --git a/Keychy/Keychy/Presentation/KeyringMaker/Shared/Protocols/KeyringViewModelProtocol+Reset.swift b/Keychy/Keychy/Presentation/KeyringMaker/Shared/Protocols/KeyringViewModelProtocol+Reset.swift index f0e25428..351cf142 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Shared/Protocols/KeyringViewModelProtocol+Reset.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Shared/Protocols/KeyringViewModelProtocol+Reset.swift @@ -37,5 +37,7 @@ extension KeyringViewModelProtocol { 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 f401c0b3..685f91a5 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Shared/Protocols/KeyringViewModelProtocol.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Shared/Protocols/KeyringViewModelProtocol.swift @@ -40,6 +40,10 @@ protocol KeyringViewModelProtocol: AnyObject, Observable { /// 저장된 키링 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 4ab8bd16..4f5823ae 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringCompleteView.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringCompleteView.swift @@ -62,6 +62,7 @@ struct KeyringCompleteView: View { .onAppear { checkReviewTriggers() } + .withToast(position: .default) } } @@ -335,6 +336,19 @@ extension KeyringCompleteView { // 선물하기 actionButton(image: .present, 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 + } + // 네트워크 체크 guard NetworkManager.shared.isConnected else { ToastManager.shared.show() @@ -358,9 +372,27 @@ extension KeyringCompleteView { } DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { - guard let uid = userManager.currentUser?.id, - let keyringDocumentId = viewModel.savedKeyringDocumentId else { - print("[Package] uid 또는 keyringDocumentId 없음") + 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 } @@ -377,7 +409,7 @@ extension KeyringCompleteView { KeyringPackageManager.packageKeyring( uid: uid, keyringDocumentId: keyringDocumentId - ) { success, postOfficeId in + ) { success, postOfficeId, shareLink in let elapsed = Date().timeIntervalSince(startTime) let remainingDelay = max(0, minimumLoadingDuration - elapsed) @@ -385,21 +417,23 @@ extension KeyringCompleteView { DispatchQueue.main.asyncAfter(deadline: .now() + remainingDelay) { showPackingAlert = false - if success, let postOfficeId = postOfficeId { + 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 + postOfficeId: postOfficeId, + templateId: viewModel.templateId, + shareLink: shareLink )) - - // 네비게이션 애니메이션 완료 후 리셋 (뒤에 있는 뷰가 보이지 않을 때) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - viewModel.resetAll() - } } } else { print("[Package] 포장 실패") + ToastManager.shared.show() } } } diff --git a/Keychy/Keychy/Presentation/KeyringMaker/Templates/AcrylicPhoto/ViewModels/AcrylicPhotoVM.swift b/Keychy/Keychy/Presentation/KeyringMaker/Templates/AcrylicPhoto/ViewModels/AcrylicPhotoVM.swift index f563c897..b85351ab 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Templates/AcrylicPhoto/ViewModels/AcrylicPhotoVM.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Templates/AcrylicPhoto/ViewModels/AcrylicPhotoVM.swift @@ -126,6 +126,8 @@ class AcrylicPhotoVM: KeyringViewModelProtocol { 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 da813cf2..c7ab1c5e 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Templates/ClearSketch/ViewModels/ClearSketchVM.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Templates/ClearSketch/ViewModels/ClearSketchVM.swift @@ -131,6 +131,8 @@ class ClearSketchVM: KeyringViewModelProtocol { 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 57f8a4ec..c6cc42a7 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Templates/NeonSign/ViewModels/NeonSignVM.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Templates/NeonSign/ViewModels/NeonSignVM.swift @@ -110,6 +110,8 @@ class NeonSignVM: KeyringViewModelProtocol { 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 f04998de..a5328a10 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Templates/Pixel/ViewModels/PixelVM.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Templates/Pixel/ViewModels/PixelVM.swift @@ -115,6 +115,8 @@ class PixelVM: KeyringViewModelProtocol { 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 f57756be..93f7d8cc 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Templates/Polaroid/ViewModels/PolaroidVM.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Templates/Polaroid/ViewModels/PolaroidVM.swift @@ -121,6 +121,8 @@ class PolaroidVM: KeyringViewModelProtocol { 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 0fb2f061..bbb03f18 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Templates/SpeechBubble/ViewModels/SpeechBubbleVM.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Templates/SpeechBubble/ViewModels/SpeechBubbleVM.swift @@ -73,6 +73,8 @@ class SpeechBubbleVM: KeyringViewModelProtocol { var selectedTags: [String] = [] var createdAt: Date = Date() var savedKeyringDocumentId: String? + var packagedPostOfficeId: String? + var packagedShareLink: String? // MARK: - Dependencies var userManager: UserManager From 7ea9e66e5dc62e6408084249ad90fa9560a6c953 Mon Sep 17 00:00:00 2001 From: giljihun Date: Thu, 29 Jan 2026 14:05:39 +0900 Subject: [PATCH 11/13] =?UTF-8?q?style:=20=ED=82=A4=EB=A7=81=EC=99=84?= =?UTF-8?q?=EC=84=B1to=ED=8F=AC=EC=9E=A5=EB=B7=B0=20-=20=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=ED=8C=85=20=EC=9E=AC=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Shared/Views/KeyringPackageCompleteView.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringPackageCompleteView.swift b/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringPackageCompleteView.swift index 4cb92a16..130e6ca8 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringPackageCompleteView.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringPackageCompleteView.swift @@ -116,11 +116,9 @@ extension KeyringPackageCompleteView { .foregroundColor(.black100) .padding(.top, 10) - Text("링크나 QR로 바로 공유할 수 있어요\n포장은 보관함에서 풀 수 있습니다") + Text("링크나 QR로 바로 공유할 수 있어요") .font(.suit16M) .foregroundColor(.black100) - .multilineTextAlignment(.center) - .fixedSize(horizontal: false, vertical: true) } .padding(.top, isSmallScreen ? -70 : 78) From 36ea5467c155d12b61e7bee8c47a96c2ab047483 Mon Sep 17 00:00:00 2001 From: giljihun Date: Thu, 29 Jan 2026 14:16:14 +0900 Subject: [PATCH 12/13] =?UTF-8?q?style:=20=EC=99=84=EC=84=B1=EB=B7=B0=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=20=EA=B8=80=EB=9E=98=EC=8A=A4=20=ED=9A=A8?= =?UTF-8?q?=EA=B3=BC=20clear=20->=20regular?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../KeyringMaker/Shared/Views/KeyringCompleteView.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringCompleteView.swift b/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringCompleteView.swift index 4f5823ae..ba86a1bd 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringCompleteView.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringCompleteView.swift @@ -317,8 +317,7 @@ extension KeyringCompleteView { .padding(.vertical, 11.5) .padding(.horizontal, 8) } - .buttonStyle(.plain) - .glassEffect(.clear.interactive(), in: .rect(cornerRadius: 24)) + .glassEffect(.regular.interactive(), in: .rect(cornerRadius: 24)) } /// 하단 액션 버튼 영역 From 5ef0cec574396752602d1f6a8604c7ccad9393ab Mon Sep 17 00:00:00 2001 From: giljihun Date: Thu, 29 Jan 2026 14:22:41 +0900 Subject: [PATCH 13/13] =?UTF-8?q?style:=20=EC=99=84=EC=84=B1=EB=B7=B0=20-?= =?UTF-8?q?=20=EC=95=A1=EC=85=98=EB=B2=84=ED=8A=BC=20=EC=95=84=EC=9D=B4?= =?UTF-8?q?=EC=BD=98=20=EC=97=90=EC=85=8B=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Shared/Views/KeyringCompleteView.swift | 4 ++-- .../Actions/presentBlack.imageset/Contents.json | 12 ++++++++++++ .../presentBlack.imageset/presentBlack.pdf | Bin 0 -> 14277 bytes .../Actions/saveBlack.imageset/Contents.json | 12 ++++++++++++ .../Actions/saveBlack.imageset/saveBlack.pdf | Bin 0 -> 12837 bytes 5 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Actions/presentBlack.imageset/Contents.json create mode 100644 Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Actions/presentBlack.imageset/presentBlack.pdf create mode 100644 Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Actions/saveBlack.imageset/Contents.json create mode 100644 Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Actions/saveBlack.imageset/saveBlack.pdf diff --git a/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringCompleteView.swift b/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringCompleteView.swift index ba86a1bd..0d9bac16 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringCompleteView.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Shared/Views/KeyringCompleteView.swift @@ -324,7 +324,7 @@ extension KeyringCompleteView { private var actionButtons: some View { HStack(spacing: 17) { // 이미지 저장 - actionButton(image: .save, title: "이미지 저장") { + actionButton(image: .saveBlack, title: "이미지 저장") { captureAndSaveImage() } @@ -334,7 +334,7 @@ extension KeyringCompleteView { } // 선물하기 - actionButton(image: .present, title: "선물하기") { + actionButton(image: .presentBlack, title: "선물하기") { // 이미 포장된 경우 바로 이동 if let keyringDocumentId = viewModel.savedKeyringDocumentId, let postOfficeId = viewModel.packagedPostOfficeId, 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 00000000..4b511bc2 --- /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 0000000000000000000000000000000000000000..e024bf3ab69a904cd880bb3788d01c89c5f0f6e4 GIT binary patch literal 14277 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 literal 0 HcmV?d00001 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 00000000..ca29fb68 --- /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 0000000000000000000000000000000000000000..9428fbb79c56a4eb009f13b2af94e70349226f9b GIT binary patch literal 12837 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 literal 0 HcmV?d00001