diff --git a/Keychy/Keychy.xcodeproj/project.pbxproj b/Keychy/Keychy.xcodeproj/project.pbxproj index dc9b5cc57..46bb0339b 100644 --- a/Keychy/Keychy.xcodeproj/project.pbxproj +++ b/Keychy/Keychy.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + AA0000022F17000100000001 /* BundleSwitchPopup.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0000012F17000100000001 /* BundleSwitchPopup.swift */; }; 38173D082EB8AD3900E36F7E /* CollectionViewModel+Tags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38173D072EB8AD3900E36F7E /* CollectionViewModel+Tags.swift */; }; 38173D0A2EB8AD7900E36F7E /* CategoryTabBarWithLongPress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38173D092EB8AD7900E36F7E /* CategoryTabBarWithLongPress.swift */; }; 38173D0C2EB8AD8800E36F7E /* CategoryContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38173D0B2EB8AD8800E36F7E /* CategoryContextMenu.swift */; }; @@ -293,7 +294,6 @@ 4CEBB14E2EFAA52F00CF53E2 /* DeepLinkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEBB14B2EFAA52F00CF53E2 /* DeepLinkManager.swift */; }; 4CEBB1552EFACFA900CF53E2 /* HomeTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEBB1512EFACFA900CF53E2 /* HomeTab.swift */; }; 4CEBB1572EFACFA900CF53E2 /* CollectionTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEBB14F2EFACFA900CF53E2 /* CollectionTab.swift */; }; - 4CEBB1582EFACFA900CF53E2 /* FestivalTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEBB1502EFACFA900CF53E2 /* FestivalTab.swift */; }; 4CEBB1592EFACFA900CF53E2 /* WorkshopTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEBB1532EFACFA900CF53E2 /* WorkshopTab.swift */; }; 4CEBB15F2EFAD3D600CF53E2 /* MainTabViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEBB15E2EFAD3D600CF53E2 /* MainTabViewModel.swift */; }; 4CEBB1612EFAD3F800CF53E2 /* MainTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEBB1602EFAD3F800CF53E2 /* MainTabView.swift */; }; @@ -321,7 +321,6 @@ 4CEC62232EAE08DA0099ECEE /* KeyringRingComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEC61FE2EAE08DA0099ECEE /* KeyringRingComponent.swift */; }; 4CEC62252EAE08DA0099ECEE /* WorkshopRoute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEC62092EAE08DA0099ECEE /* WorkshopRoute.swift */; }; 4CEC62262EAE08DA0099ECEE /* Radius.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEC62132EAE08DA0099ECEE /* Radius.swift */; }; - 4CEC62282EAE08DA0099ECEE /* FestivalRoute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEC620A2EAE08DA0099ECEE /* FestivalRoute.swift */; }; 4CEC62292EAE08DA0099ECEE /* KeyringCellScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEC61F32EAE08DA0099ECEE /* KeyringCellScene.swift */; }; 4CEC622A2EAE08DA0099ECEE /* Font+Custom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEC62102EAE08DA0099ECEE /* Font+Custom.swift */; }; 4CEC622B2EAE08DA0099ECEE /* Font+Styles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEC62112EAE08DA0099ECEE /* Font+Styles.swift */; }; @@ -464,6 +463,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + AA0000012F17000100000001 /* BundleSwitchPopup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleSwitchPopup.swift; sourceTree = ""; }; 38173D072EB8AD3900E36F7E /* CollectionViewModel+Tags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CollectionViewModel+Tags.swift"; sourceTree = ""; }; 38173D092EB8AD7900E36F7E /* CategoryTabBarWithLongPress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryTabBarWithLongPress.swift; sourceTree = ""; }; 38173D0B2EB8AD8800E36F7E /* CategoryContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryContextMenu.swift; sourceTree = ""; }; @@ -742,7 +742,6 @@ 4CEBB14A2EFAA52F00CF53E2 /* DeepLinkHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLinkHandler.swift; sourceTree = ""; }; 4CEBB14B2EFAA52F00CF53E2 /* DeepLinkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLinkManager.swift; sourceTree = ""; }; 4CEBB14F2EFACFA900CF53E2 /* CollectionTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionTab.swift; sourceTree = ""; }; - 4CEBB1502EFACFA900CF53E2 /* FestivalTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FestivalTab.swift; sourceTree = ""; }; 4CEBB1512EFACFA900CF53E2 /* HomeTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTab.swift; sourceTree = ""; }; 4CEBB1532EFACFA900CF53E2 /* WorkshopTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkshopTab.swift; sourceTree = ""; }; 4CEBB15E2EFAD3D600CF53E2 /* MainTabViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabViewModel.swift; sourceTree = ""; }; @@ -776,7 +775,6 @@ 4CEC62072EAE08DA0099ECEE /* HomeRoute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeRoute.swift; sourceTree = ""; }; 4CEC62082EAE08DA0099ECEE /* CollectionRoute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionRoute.swift; sourceTree = ""; }; 4CEC62092EAE08DA0099ECEE /* WorkshopRoute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkshopRoute.swift; sourceTree = ""; }; - 4CEC620A2EAE08DA0099ECEE /* FestivalRoute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FestivalRoute.swift; sourceTree = ""; }; 4CEC620C2EAE08DA0099ECEE /* NavigationRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRouter.swift; sourceTree = ""; }; 4CEC62102EAE08DA0099ECEE /* Font+Custom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Font+Custom.swift"; sourceTree = ""; }; 4CEC62112EAE08DA0099ECEE /* Font+Styles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Font+Styles.swift"; sourceTree = ""; }; @@ -913,6 +911,7 @@ 38173D0D2EB902FD00E36F7E /* Popup */ = { isa = PBXGroup; children = ( + AA0000012F17000100000001 /* BundleSwitchPopup.swift */, 38173D0E2EB9083900E36F7E /* DeletePopup.swift */, 38173D102EB90CCE00E36F7E /* TagInputPopup.swift */, 3822DDBD2EBAAC80003125BE /* PurchasePopup.swift */, @@ -1661,7 +1660,6 @@ 4CEBB1512EFACFA900CF53E2 /* HomeTab.swift */, 4CEBB1532EFACFA900CF53E2 /* WorkshopTab.swift */, 4CEBB14F2EFACFA900CF53E2 /* CollectionTab.swift */, - 4CEBB1502EFACFA900CF53E2 /* FestivalTab.swift */, ); path = Views; sourceTree = ""; @@ -1866,7 +1864,6 @@ 4CEC62072EAE08DA0099ECEE /* HomeRoute.swift */, 4CEC62082EAE08DA0099ECEE /* CollectionRoute.swift */, 4CEC62092EAE08DA0099ECEE /* WorkshopRoute.swift */, - 4CEC620A2EAE08DA0099ECEE /* FestivalRoute.swift */, AA390CE42ECC60A700D87EEC /* BundleRoute.swift */, ); path = Routes; @@ -2496,7 +2493,6 @@ AA8C9B8C2F0F349500A352D2 /* BundleDetailView+Alert.swift in Sources */, 4CEC62252EAE08DA0099ECEE /* WorkshopRoute.swift in Sources */, 4CEC62262EAE08DA0099ECEE /* Radius.swift in Sources */, - 4CEC62282EAE08DA0099ECEE /* FestivalRoute.swift in Sources */, 4CEC62292EAE08DA0099ECEE /* KeyringCellScene.swift in Sources */, AA2146BB2F161D0C0048D40E /* BundleEditView+Initialization.swift in Sources */, 3828F5492EC4CCE400F1B040 /* CollectionView+SearchMode.swift in Sources */, @@ -2508,6 +2504,7 @@ AA69DD262F14C60000C0A41C /* BundleViewModel+Edit.swift in Sources */, 4CC3D3C52EC701610009D376 /* IntroViewModel+Bundle.swift in Sources */, 38173D0F2EB9083900E36F7E /* DeletePopup.swift in Sources */, + AA0000022F17000100000001 /* BundleSwitchPopup.swift in Sources */, AA390CE52ECC60A700D87EEC /* BundleRoute.swift in Sources */, 38C3C27E2EC08794003C5DE1 /* PopupManager.swift in Sources */, C6C4028D2EB2741D006B58DF /* Sound.swift in Sources */, @@ -2703,7 +2700,6 @@ 38173D0A2EB8AD7900E36F7E /* CategoryTabBarWithLongPress.swift in Sources */, 4CEBB1552EFACFA900CF53E2 /* HomeTab.swift in Sources */, 4CEBB1572EFACFA900CF53E2 /* CollectionTab.swift in Sources */, - 4CEBB1582EFACFA900CF53E2 /* FestivalTab.swift in Sources */, 4CEBB1592EFACFA900CF53E2 /* WorkshopTab.swift in Sources */, 38F832CD2EC90DEF00D3A248 /* WidgetOnboardingStepView+Helpers.swift in Sources */, 38A22A9D2EC27AC400B4C7C5 /* PackagedKeyringView+SaveImage.swift in Sources */, diff --git a/Keychy/Keychy/Core/Components/KeyringBundle/MultiKeyringScene.swift b/Keychy/Keychy/Core/Components/KeyringBundle/MultiKeyringScene.swift index a4f0bfc88..c09ec0b99 100644 --- a/Keychy/Keychy/Core/Components/KeyringBundle/MultiKeyringScene.swift +++ b/Keychy/Keychy/Core/Components/KeyringBundle/MultiKeyringScene.swift @@ -181,35 +181,45 @@ class MultiKeyringScene: SKScene { } private func beginLoading() { - // 카라비너 이미지와 키링들을 로드 - Task { - async let carabinerBackTask: Void = { - if let carabinerBackURL = await carabinerBackImageURL { - if carabinerBackURL != "none" { - await setupCarabinerBackImageAsync(url: carabinerBackURL) - } - } - await MainActor.run { self.carabinerBackReady = true } - }() + Task { [weak self] in + guard let self else { return } + guard !self.isCleaningUp else { return } - async let carabinerFrontTask: Void = { - if let carabinerFrontURL = await carabinerFrontImageURL { - await setupCarabinerFrontImageAsync(url: carabinerFrontURL) - } - await MainActor.run { self.carabinerFrontReady = true } - }() + // 카라비너 이미지 병렬 로드 + async let backLoaded: Void = self.loadCarabinerBack() + async let frontLoaded: Void = self.loadCarabinerFront() - // 카라비너 이미지 로드 병렬 실행 - await carabinerBackTask - await carabinerFrontTask + await backLoaded + await frontLoaded // 키링 설정 (카라비너 준비 후) - await MainActor.run { + await MainActor.run { [weak self] in + guard let self, !self.isCleaningUp else { return } self.setupKeyringsIfNeeded() } } } + /// 카라비너 뒷면 이미지 로드 + private func loadCarabinerBack() async { + if let url = carabinerBackImageURL, url != "none" { + await setupCarabinerBackImageAsync(url: url) + } + await MainActor.run { [weak self] in + self?.carabinerBackReady = true + } + } + + /// 카라비너 앞면 이미지 로드 + private func loadCarabinerFront() async { + if let url = carabinerFrontImageURL { + await setupCarabinerFrontImageAsync(url: url) + } + await MainActor.run { [weak self] in + self?.carabinerFrontReady = true + } + } + // MARK: - Shadow Helper /// 노드에 수직 그림자 추가 (z축 위에서 내려오는 광원) @@ -328,7 +338,6 @@ class MultiKeyringScene: SKScene { private func setupKeyringsIfNeeded() { guard !didStartKeyringSetup else { return } - // 카라비너가 준비된 뒤에만 시작 guard carabinerBackReady && carabinerFrontReady else { return } didStartKeyringSetup = true setupKeyrings() diff --git a/Keychy/Keychy/Core/Components/KeyringBundle/MultiKeyringSceneView.swift b/Keychy/Keychy/Core/Components/KeyringBundle/MultiKeyringSceneView.swift index 10f9f3c01..1b0b686d3 100644 --- a/Keychy/Keychy/Core/Components/KeyringBundle/MultiKeyringSceneView.swift +++ b/Keychy/Keychy/Core/Components/KeyringBundle/MultiKeyringSceneView.swift @@ -78,19 +78,11 @@ struct MultiKeyringSceneView: View { particleEffectsView } .onAppear { - // 씬이 없을 때 초기 설정 (키링이 없어도 배경과 카라비너는 표시) if scene == nil { loadBackgroundImage() setupScene() } } - .onChange(of: keyringDataList) { oldValue, newValue in - // 데이터가 실제로 변경된 경우에만 씬 재생성 - if oldValue != newValue { - loadBackgroundImage() - setupScene() - } - } .onChange(of: backgroundImageURL) { _, _ in loadBackgroundImage() } @@ -172,20 +164,18 @@ extension MultiKeyringSceneView { /// 씬 초기화 및 설정 private func setupScene() { - // 기존 씬이 있다면 명시적으로 정리 if scene != nil { cleanupScene() } - - // 키링 준비 카운터 리셋 + visibleKeyringCount = 0 - + let newScene = MultiKeyringScene( keyringDataList: keyringDataList, ringType: ringType, chainType: chainType, - backgroundColor: .clear, // 배경은 투명하게 - backgroundImageURL: nil, // 배경은 SwiftUI에서 처리 + backgroundColor: .clear, + backgroundImageURL: nil, carabinerBackImageURL: carabinerBackImageURL, carabinerFrontImageURL: carabinerFrontImageURL, carabinerX: carabinerX, diff --git a/Keychy/Keychy/Core/Components/View/Popup/BundleSwitchPopup.swift b/Keychy/Keychy/Core/Components/View/Popup/BundleSwitchPopup.swift new file mode 100644 index 000000000..c7f1fec40 --- /dev/null +++ b/Keychy/Keychy/Core/Components/View/Popup/BundleSwitchPopup.swift @@ -0,0 +1,140 @@ +// +// BundleSwitchPopup.swift +// Keychy +// +// Created by Claude on 2/3/26. +// + +import SwiftUI + +/// 홈 화면에서 뭉치를 변경할 수 있는 드롭다운 팝업 +struct BundleSwitchPopup: View { + let bundles: [KeyringBundle] + let currentBundle: KeyringBundle? + let onSelect: (KeyringBundle) -> Void + + /// 대표 뭉치 (isMain == true) + private var mainBundle: KeyringBundle? { + bundles.first(where: { $0.isMain }) + } + + /// 선택 가능한 뭉치들 (대표 제외) + private var selectableBundles: [KeyringBundle] { + bundles.filter { !$0.isMain } + } + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + // 대표 섹션 + if let main = mainBundle { + mainSection(bundle: main) + } + + // 구분선 + Rectangle() + .fill(.gray100) + .frame(height: 1) + .padding(.horizontal, 18) + + // 선택 섹션 + if !selectableBundles.isEmpty { + selectSection(bundles: selectableBundles) + } + } + .frame(width: 196) + .padding(.vertical, 5) + .glassEffect(.regular.interactive(), in: .rect(cornerRadius: 34)) + } + + // MARK: - 대표 섹션 + + private func mainSection(bundle: KeyringBundle) -> some View { + VStack(alignment: .leading, spacing: 0) { + Text("대표") + .typography(.suit13M) + .foregroundStyle(.gray200) + .padding(.horizontal, 20) + .padding(.top, 10) + .padding(.bottom, 8) + + Button { + onSelect(bundle) + } label: { + HStack { + Text(bundle.name) + .typography(.suit16M) + .foregroundStyle(currentBundle?.documentId == bundle.documentId ? .gray600 : .gray400) + Spacer() + } + .padding(.horizontal, 20) + .padding(.bottom, 12) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + } + } + + // MARK: - 선택 섹션 + + private func selectSection(bundles: [KeyringBundle]) -> some View { + VStack(alignment: .leading, spacing: 0) { + Text("선택") + .typography(.suit13M) + .foregroundStyle(.gray200) + .padding(.horizontal, 20) + .padding(.top, 12) + .padding(.bottom, 10) + + VStack(alignment: .leading, spacing: 25) { + ForEach(bundles, id: \.documentId) { bundle in + Button { + onSelect(bundle) + } label: { + HStack { + Text(bundle.name) + .typography(.suit16M) + .foregroundStyle(currentBundle?.documentId == bundle.documentId ? .gray600 : .gray400) + Spacer() + } + .padding(.horizontal, 20) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + } + } + .padding(.bottom, 10) + } + } +} + +// MARK: - 드롭다운 버튼 +struct BundleSwitchButton: View { + let bundleName: String + let isExpanded: Bool + let isEnabled: Bool + let onTap: () -> Void + + var body: some View { + Button(action: onTap) { + HStack(spacing: 12) { + Text(bundleName) + .typography(.nanum24EB) + .foregroundStyle(.black100) + + if isEnabled { + Image(systemName: "chevron.down") + .font(.system(size: 11, weight: .bold)) + .foregroundStyle(.black) + .rotationEffect(.degrees(isExpanded ? 180 : 0)) + .offset(y: 0.5) + .frame(width: 24, height: 24) + .background(Color.white) + .clipShape(Circle()) + .overlay(Circle().stroke(Color(#colorLiteral(red: 0.8861967921, green: 0.8861967921, blue: 0.8861967921, alpha: 1)), lineWidth: 1)) + } + } + } + .buttonStyle(.plain) + .disabled(!isEnabled) + } +} diff --git a/Keychy/Keychy/Core/Navigation/Routes/FestivalRoute.swift b/Keychy/Keychy/Core/Navigation/Routes/FestivalRoute.swift deleted file mode 100644 index ce8b1602d..000000000 --- a/Keychy/Keychy/Core/Navigation/Routes/FestivalRoute.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// HomeRoute.swift -// KeytschPrototype -// -// Created by 길지훈 on 10/16/25. -// - -/// 페스티벌 탭 -enum FestivalRoute: Hashable { - case festivalView - case showcase25BoardView - - case festivalKeyringDetailView(Keyring) - - case coinCharge - -} diff --git a/Keychy/Keychy/Core/Navigation/Routes/HomeRoute.swift b/Keychy/Keychy/Core/Navigation/Routes/HomeRoute.swift index 7a2c39fdc..399675abb 100644 --- a/Keychy/Keychy/Core/Navigation/Routes/HomeRoute.swift +++ b/Keychy/Keychy/Core/Navigation/Routes/HomeRoute.swift @@ -7,7 +7,7 @@ /// 홈 탭 enum HomeRoute: Hashable, BundleRoute { - // 나중에 추가 + // Bundle case bundleInventoryView case bundleDetailView case bundleCreateView @@ -15,6 +15,8 @@ enum HomeRoute: Hashable, BundleRoute { case bundleNameInputView case bundleNameEditView case bundleEditView + + // Home case coinCharge case myPageView case changeName @@ -23,4 +25,9 @@ enum HomeRoute: Hashable, BundleRoute { case notificationGiftView(postOfficeId: String) case introView case termsAndPolicy + + // Festival + case festivalView + case showcase25BoardView + case festivalKeyringDetailView(Keyring) } diff --git a/Keychy/Keychy/Core/Navigation/Routes/WorkshopRoute.swift b/Keychy/Keychy/Core/Navigation/Routes/WorkshopRoute.swift index 5995cd382..b0c7e8acb 100644 --- a/Keychy/Keychy/Core/Navigation/Routes/WorkshopRoute.swift +++ b/Keychy/Keychy/Core/Navigation/Routes/WorkshopRoute.swift @@ -23,10 +23,6 @@ enum WorkshopRoute: Hashable, BundleRoute { case bundleNameEditView case bundleEditView - // MARK: - Festival 임시 라우트 (추후 정리 예정) - case showcase25BoardView - case festivalKeyringDetailView(Keyring) - // MARK: - 아크릴 포토 템플릿 case acrylicPhotoPreview case acrylicPhotoCrop diff --git a/Keychy/Keychy/Presentation/Festival/ViewModels/Showcase25BoardViewModel.swift b/Keychy/Keychy/Presentation/Festival/ViewModels/Showcase25BoardViewModel.swift index 023d07da6..e555877c8 100644 --- a/Keychy/Keychy/Presentation/Festival/ViewModels/Showcase25BoardViewModel.swift +++ b/Keychy/Keychy/Presentation/Festival/ViewModels/Showcase25BoardViewModel.swift @@ -115,9 +115,9 @@ class Showcase25BoardViewModel { /// 실시간 리스너 중지 func stopListening() { + guard listener != nil else { return } listener?.remove() listener = nil - print("🛑 Stopped listening to ShowcaseFestivalKeyring") } // MARK: - 쇼케이스 키링 로드 diff --git a/Keychy/Keychy/Presentation/Festival/Views/FestivalKeyringDetailView+Alerts.swift b/Keychy/Keychy/Presentation/Festival/Views/FestivalKeyringDetailView+Alerts.swift index d65939017..7922820c6 100644 --- a/Keychy/Keychy/Presentation/Festival/Views/FestivalKeyringDetailView+Alerts.swift +++ b/Keychy/Keychy/Presentation/Festival/Views/FestivalKeyringDetailView+Alerts.swift @@ -74,8 +74,8 @@ extension FestivalKeyringDetailView { } DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { isSheetPresented = false - - festivalRouter.push(.coinCharge) + + router.push(.coinCharge) } } ) diff --git a/Keychy/Keychy/Presentation/Festival/Views/FestivalKeyringDetailView.swift b/Keychy/Keychy/Presentation/Festival/Views/FestivalKeyringDetailView.swift index bf1e6b5b3..3f0d6d29f 100644 --- a/Keychy/Keychy/Presentation/Festival/Views/FestivalKeyringDetailView.swift +++ b/Keychy/Keychy/Presentation/Festival/Views/FestivalKeyringDetailView.swift @@ -11,7 +11,7 @@ import FirebaseFirestore import Photos struct FestivalKeyringDetailView: View { - @Bindable var festivalRouter: NavigationRouter + @Bindable var router: NavigationRouter @Bindable var viewModel: Showcase25BoardViewModel @State var userManager = UserManager.shared @@ -225,7 +225,7 @@ extension FestivalKeyringDetailView { // Leading (왼쪽) - 뒤로가기 버튼 BackToolbarButton { isSheetPresented = false - festivalRouter.pop() + router.pop() } } center: { // Center (중앙) - 빈 공간 diff --git a/Keychy/Keychy/Presentation/Festival/Views/FestivalView.swift b/Keychy/Keychy/Presentation/Festival/Views/FestivalView.swift index 6ae8fccfa..745550694 100644 --- a/Keychy/Keychy/Presentation/Festival/Views/FestivalView.swift +++ b/Keychy/Keychy/Presentation/Festival/Views/FestivalView.swift @@ -11,7 +11,7 @@ import CoreLocation struct FestivalView: View { - @Bindable var router: NavigationRouter + @Bindable var router: NavigationRouter @State private var viewModel = FestivalViewModel() @State private var locationManager = LocationManager() diff --git a/Keychy/Keychy/Presentation/Festival/Views/Showcase25Board/Showcase25BoardView+Detail.swift b/Keychy/Keychy/Presentation/Festival/Views/Showcase25Board/Showcase25BoardView+Detail.swift index 9d8b8b494..610da91a9 100644 --- a/Keychy/Keychy/Presentation/Festival/Views/Showcase25Board/Showcase25BoardView+Detail.swift +++ b/Keychy/Keychy/Presentation/Festival/Views/Showcase25Board/Showcase25BoardView+Detail.swift @@ -37,7 +37,7 @@ extension Showcase25BoardView { if let keyring = Keyring(documentId: document.documentID, data: data) { // 3. DetailView로 이동 (Main thread에서 실행) await MainActor.run { - festivalRouter.push(.festivalKeyringDetailView(keyring)) + router.push(.festivalKeyringDetailView(keyring)) } } else { print("Keyring 변환 실패") diff --git a/Keychy/Keychy/Presentation/Festival/Views/Showcase25Board/Showcase25BoardView.swift b/Keychy/Keychy/Presentation/Festival/Views/Showcase25Board/Showcase25BoardView.swift index dcd9ceb1f..7c0baf5d7 100644 --- a/Keychy/Keychy/Presentation/Festival/Views/Showcase25Board/Showcase25BoardView.swift +++ b/Keychy/Keychy/Presentation/Festival/Views/Showcase25Board/Showcase25BoardView.swift @@ -11,7 +11,7 @@ import FirebaseFirestore struct Showcase25BoardView: View { - @Bindable var festivalRouter: NavigationRouter + @Bindable var router: NavigationRouter @Bindable var viewModel: Showcase25BoardViewModel @Environment(\.scenePhase) private var scenePhase @@ -277,7 +277,7 @@ struct Showcase25BoardView: View { private var customNavigationBar: some View { CustomNavigationBar { BackToolbarButton { - festivalRouter.pop() + router.pop() } } center: { HStack { diff --git a/Keychy/Keychy/Presentation/Home/ViewModels/HomeViewModel.swift b/Keychy/Keychy/Presentation/Home/ViewModels/HomeViewModel.swift index 393f40fda..974db5f3f 100644 --- a/Keychy/Keychy/Presentation/Home/ViewModels/HomeViewModel.swift +++ b/Keychy/Keychy/Presentation/Home/ViewModels/HomeViewModel.swift @@ -37,6 +37,11 @@ class HomeViewModel { @MainActor func loadMainBundle(collectionViewModel: CollectionViewModel, bundleViewModel: BundleViewModel, onBackgroundLoaded: (() -> Void)?) async { + // 이미 데이터가 로드되었고 선택된 뭉치가 있으면 스킵 (탭 전환 후 돌아올 때) + if isDataLoaded && bundleViewModel.selectedBundle != nil { + return + } + let uid = UserManager.shared.userUID guard !uid.isEmpty else { return } @@ -178,12 +183,50 @@ class HomeViewModel { /// 번들의 배경을 Firebase에 업데이트 private func updateBundleBackground(documentId: String, backgroundId: String) async { + try? await db.collection("KeyringBundle").document(documentId).updateData([ + "selectedBackground": backgroundId + ]) + } + + /// 대표뭉치 설정 (isMain 업데이트) - Batch write로 원자성 보장 + /// - Parameters: + /// - newMainBundle: 새로 대표뭉치로 설정할 뭉치 + /// - bundleViewModel: BundleViewModel + @MainActor + private func updateMainBundle(newMainBundle: KeyringBundle, bundleViewModel: BundleViewModel) async { + guard let newMainDocId = newMainBundle.documentId else { return } + + let previousMainBundle = bundleViewModel.bundles.first(where: { $0.isMain }) + let batch = db.batch() + + // 1. 기존 대표뭉치 해제 (batch에 추가) + if let previousMain = previousMainBundle, + let previousDocId = previousMain.documentId, + previousDocId != newMainDocId { + let previousRef = db.collection("KeyringBundle").document(previousDocId) + batch.updateData(["isMain": false], forDocument: previousRef) + } + + // 2. 새 대표뭉치 설정 (batch에 추가) + let newMainRef = db.collection("KeyringBundle").document(newMainDocId) + batch.updateData(["isMain": true], forDocument: newMainRef) + + // 3. Batch commit (원자적 업데이트) do { - try await db.collection("KeyringBundle").document(documentId).updateData([ - "selectedBackground": backgroundId - ]) + try await batch.commit() + + // 성공 시 로컬 상태 업데이트 + if let previousMain = previousMainBundle, + let previousDocId = previousMain.documentId, + previousDocId != newMainDocId, + let index = bundleViewModel.bundles.firstIndex(where: { $0.documentId == previousDocId }) { + bundleViewModel.bundles[index].isMain = false + } + if let index = bundleViewModel.bundles.firstIndex(where: { $0.documentId == newMainDocId }) { + bundleViewModel.bundles[index].isMain = true + } } catch { - print("[HomeView] 뭉치 배경 업데이트 실패: \(error.localizedDescription)") + // 실패 시 로컬 상태는 변경하지 않음 (데이터 일관성 유지) } } @@ -196,13 +239,13 @@ class HomeViewModel { /// 모든 키링 준비 완료되면 0.5초 대기 후 로딩을 삭제함 func handleAllKeyringsReady() { - // 물리 엔진 안정화를 위한 딜레이만 적용 (0.5초) - Task { - try? await Task.sleep(for: .seconds(0.5)) // 0.5초 + Task { [weak self] in + try? await Task.sleep(for: .seconds(0.5)) - await MainActor.run { + await MainActor.run { [weak self] in + guard let self else { return } withAnimation(.easeOut(duration: 0.3)) { - isSceneReady = true + self.isSceneReady = true } } } @@ -216,4 +259,40 @@ class HomeViewModel { await loadMainBundle(collectionViewModel: collectionViewModel, bundleViewModel: bundleViewModel, onBackgroundLoaded: onBackgroundLoaded) } + // MARK: - Bundle Switching + + /// 다른 뭉치로 전환 + /// - Parameters: + /// - bundle: 전환할 뭉치 + /// - collectionViewModel: CollectionViewModel + /// - bundleViewModel: BundleViewModel + @MainActor + func switchBundle(to bundle: KeyringBundle, collectionViewModel: CollectionViewModel, bundleViewModel: BundleViewModel) async { + // 1. 씬 준비 상태 초기화 (로딩 효과 표시) + withAnimation(.easeIn(duration: 0.2)) { + isSceneReady = false + } + + // 2. 대표뭉치 설정 (isMain 업데이트) + await updateMainBundle(newMainBundle: bundle, bundleViewModel: bundleViewModel) + + // 3. 모든 데이터 먼저 준비 (UI 업데이트 전) + let resolvedBackground = bundleViewModel.resolveBackground(from: bundle.selectedBackground) + let resolvedCarabiner = bundleViewModel.resolveCarabiner(from: bundle.selectedCarabiner) + + guard let carabiner = resolvedCarabiner else { return } + + // 4. 키링 데이터 생성 (새 카라비너 기준) + let newKeyringDataList = await createKeyringDataList(bundle: bundle, carabiner: carabiner) + + // 5. 모든 상태를 한 번에 업데이트 (SwiftUI re-render 최소화) + bundleViewModel.selectedBundle = bundle + bundleViewModel.selectedBackground = resolvedBackground ?? bundleViewModel.backgrounds.first + bundleViewModel.selectedCarabiner = carabiner + keyringDataList = newKeyringDataList + + // 데이터 로드 완료 표시 + isDataLoaded = true + } + } diff --git a/Keychy/Keychy/Presentation/Home/Views/Main/HomeView.swift b/Keychy/Keychy/Presentation/Home/Views/Main/HomeView.swift index 344367990..bb3f36f3c 100644 --- a/Keychy/Keychy/Presentation/Home/Views/Main/HomeView.swift +++ b/Keychy/Keychy/Presentation/Home/Views/Main/HomeView.swift @@ -27,6 +27,9 @@ struct HomeView: View { @State private var viewModel = HomeViewModel() @State private var isTabBarVisible = true + /// 뭉치 변경 팝업 표시 여부 + @State private var showBundleSwitchPopup = false + // MARK: - Body var body: some View { ZStack(alignment: .top) { @@ -59,8 +62,8 @@ struct HomeView: View { ) .ignoresSafeArea() /// 씬 재생성 조건을 위한 ID 설정 - /// 배경, 카라비너, 키링 구성이 변경되면 씬을 완전히 재생성 - .id("\(background.id ?? "")_\(carabiner.id ?? "")_\(viewModel.keyringDataList.map(\.index).sorted())") + /// 뭉치, 배경, 카라비너, 키링 구성이 변경되면 씬을 완전히 재생성 + .id("\(bundle.documentId ?? "")_\(background.id ?? "")_\(carabiner.id ?? "")_\(viewModel.keyringDataList.map(\.bodyImageURL).joined(separator: ","))") } else { // 데이터 로딩 중 Color.clear.ignoresSafeArea() @@ -76,6 +79,11 @@ struct HomeView: View { LoadingAlert(type: .longWithKeychy, message: "키링 뭉치를 불러오고 있어요") } } + + // 뭉치 변경 팝업 오버레이 + if showBundleSwitchPopup { + bundleSwitchPopupOverlay + } } .frame(maxWidth: .infinity, maxHeight: .infinity) .toolbar(isTabBarVisible ? .visible : .hidden, for: .tabBar) @@ -99,11 +107,8 @@ struct HomeView: View { await viewModel.loadMainBundle(collectionViewModel: collectionViewModel, bundleViewModel: bundleViewModel, onBackgroundLoaded: onBackgroundLoaded) } .onChange(of: viewModel.keyringDataList) { _, _ in - // 키링 데이터가 변경되면 씬 준비 상태 초기화 + // 키링 데이터가 변경되면 씬 준비 상태 초기화 (씬이 자동으로 재생성됨) viewModel.handleKeyringDataChange() - Task { - await viewModel.loadMainBundle(collectionViewModel: collectionViewModel, bundleViewModel: bundleViewModel, onBackgroundLoaded: onBackgroundLoaded) - } } .onChange(of: NetworkManager.shared.isConnected) { oldValue, newValue in // 네트워크가 복구되고, 에러 상태였다면 자동 재시도 @@ -137,37 +142,115 @@ extension HomeView { /// 상단 네비게이션 버튼들 private var navigationButtons: some View { - HStack(spacing: 10) { - Spacer() - - // 알림 및 마이페이지 버튼 그룹 - GlassEffectContainer { - HStack { - Button { - router.push(.alarmView) - } label: { - Image(userManager.hasUnreadNotifications ? "alarmSent" : "alarm") - } - .frame(width: 44, height: 44) - .glassEffectUnion(id: "mapOptions", namespace: unionNamespace) - .buttonStyle(.glass) - - Button { - router.push(.myPageView) - } label: { - Image(.myPageIcon) - .resizable() - .scaledToFit() - .frame(width: 30, height: 30) - + VStack(alignment: .trailing, spacing: 12) { + HStack(spacing: 10) { + // 뭉치 변경 버튼 (좌측) - 뭉치가 2개 이상일 때만 표시 + bundleSwitchButton + + Spacer() + + // 알림 및 마이페이지 버튼 그룹 + GlassEffectContainer { + HStack { + Button { + router.push(.alarmView) + } label: { + Image(userManager.hasUnreadNotifications ? "alarmSent" : "alarm") + } + .frame(width: 44, height: 44) + .glassEffectUnion(id: "mapOptions", namespace: unionNamespace) + .buttonStyle(.glass) + + Button { + router.push(.myPageView) + } label: { + Image(.myPageIcon) + .resizable() + .scaledToFit() + .frame(width: 30, height: 30) + + } + .frame(width: 44, height: 44) + .glassEffectUnion(id: "mapOptions", namespace: unionNamespace) + .buttonStyle(.glass) } - .frame(width: 44, height: 44) - .glassEffectUnion(id: "mapOptions", namespace: unionNamespace) - .buttonStyle(.glass) } } } .padding(.horizontal, 20) } + + /// 뭉치 변경 버튼 + @ViewBuilder + private var bundleSwitchButton: some View { + let hasMutipleBundles = bundleViewModel.bundles.count >= 2 + let bundleName = bundleViewModel.selectedBundle?.name ?? "뭉치" + + BundleSwitchButton( + bundleName: bundleName, + isExpanded: showBundleSwitchPopup, + isEnabled: hasMutipleBundles, + onTap: { + withAnimation(.spring(response: 0.35, dampingFraction: 0.8)) { + showBundleSwitchPopup.toggle() + } + } + ) + } + + /// 뭉치 변경 팝업 오버레이 + private var bundleSwitchPopupOverlay: some View { + ZStack(alignment: .topLeading) { + // 배경 탭하면 닫기 + Color.clear + .contentShape(Rectangle()) + .onTapGesture { + withAnimation(.spring(response: 0.35, dampingFraction: 0.8)) { + showBundleSwitchPopup = false + } + } + + // 팝업 (버튼 아래에 위치) + BundleSwitchPopup( + bundles: bundleViewModel.sortedBundles, + currentBundle: bundleViewModel.selectedBundle, + onSelect: { selectedBundle in + handleBundleSelection(selectedBundle) + } + ) + .padding(.top, getSafeAreaTop() + 50) + .padding(.leading, 20) + .transition(.scale(scale: 0.9, anchor: .topLeading).combined(with: .opacity)) + } + .ignoresSafeArea() + } + + /// 뭉치 선택 처리 + private func handleBundleSelection(_ bundle: KeyringBundle) { + // 같은 뭉치 선택 시 팝업만 닫기 + guard bundle.documentId != bundleViewModel.selectedBundle?.documentId else { + withAnimation(.spring(response: 0.35, dampingFraction: 0.8)) { + showBundleSwitchPopup = false + } + return + } + + // 팝업 닫기 + withAnimation(.spring(response: 0.35, dampingFraction: 0.8)) { + showBundleSwitchPopup = false + } + + // 로딩 시작 + viewModel.isSceneReady = false + + // 선택된 뭉치로 변경 후 로드 + Task { + await viewModel.switchBundle( + to: bundle, + collectionViewModel: collectionViewModel, + bundleViewModel: bundleViewModel + ) + } + } } diff --git a/Keychy/Keychy/Presentation/Tab/ViewModels/MainTabViewModel.swift b/Keychy/Keychy/Presentation/Tab/ViewModels/MainTabViewModel.swift index 4dd070ed2..b8aaf9bef 100644 --- a/Keychy/Keychy/Presentation/Tab/ViewModels/MainTabViewModel.swift +++ b/Keychy/Keychy/Presentation/Tab/ViewModels/MainTabViewModel.swift @@ -16,7 +16,6 @@ class MainTabViewModel { case home = 0 case workshop = 1 case collection = 2 - case festival = 3 } /// 화면 전환 시 딜레이 시간 @@ -36,7 +35,6 @@ class MainTabViewModel { var homeRouter = NavigationRouter() var collectionRouter = NavigationRouter() var workshopRouter = NavigationRouter() - var festivalRouter = NavigationRouter() // Sheets var showReceiveSheet = false @@ -50,7 +48,6 @@ class MainTabViewModel { // ViewModels let collectionViewModel = CollectionViewModel() - let festivalViewModel = Showcase25BoardViewModel() // Managers let userManager = UserManager.shared @@ -77,43 +74,7 @@ class MainTabViewModel { } } - // MARK: - Public Methods - /// Festival 탭에서 Workshop 탭으로 전환하고 특정 라우트로 이동 - /// - Parameter route: 이동할 WorkshopRoute - func handleSwitchToWorkshop(_ route: WorkshopRoute) { - festivalViewModel.isFromFestivalTab = true - - selectedTab = TabIndex.workshop.rawValue - Task { @MainActor in - try? await Task.sleep(for: .seconds(Delay.tabSwitchAnimation)) - workshopRouter.push(route) - } - } - - /// Festival 탭에서 KeyringMaker로 전환하고 특정 라우트로 이동 - /// - Parameter route: 이동할 WorkshopRoute (키링 제작 라우트) - func handleSwitchToKeyringMaker(_ route: WorkshopRoute) { - setupFestivalReturnCallback() - - selectedTab = TabIndex.workshop.rawValue - Task { @MainActor in - try? await Task.sleep(for: .seconds(Delay.tabSwitchAnimation)) - workshopRouter.push(route) - } - } - // MARK: - Private Methods - /// Festival → KeyringMaker 이동 시 완료 후 복귀 콜백 설정 - private func setupFestivalReturnCallback() { - festivalViewModel.isFromFestivalTab = true - festivalViewModel.onKeyringCompleteFromFestival = { [weak self] (router: NavigationRouter) in - guard let self = self else { return } - router.reset() - self.selectedTab = TabIndex.festival.rawValue - self.festivalViewModel.isFromFestivalTab = false - } - } - /// 대기 중인 딥링크가 있는지 확인하고 처리 private func checkPendingDeepLink() { if let (postOfficeId, type) = deepLinkManager.consumePendingDeepLink() { diff --git a/Keychy/Keychy/Presentation/Tab/Views/FestivalTab.swift b/Keychy/Keychy/Presentation/Tab/Views/FestivalTab.swift deleted file mode 100644 index 202e52d1a..000000000 --- a/Keychy/Keychy/Presentation/Tab/Views/FestivalTab.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// FestivalTab.swift -// KeytschPrototype -// -// Created by 길지훈 on 10/16/25. -// - -import SwiftUI - -struct FestivalTab: View { - @Bindable var router: NavigationRouter - @Bindable var showcaseVM: Showcase25BoardViewModel - var onSwitchToKeyringMaker: ((WorkshopRoute) -> Void)? = nil - var onSwitchToWorkshop: ((WorkshopRoute) -> Void)? = nil - - var body: some View { - NavigationStack(path: $router.path) { - FestivalView(router: router) - .navigationDestination(for: FestivalRoute.self) { route in - switch route { - case .showcase25BoardView: - Showcase25BoardView( - festivalRouter: router, - viewModel: showcaseVM, - onNavigateToKeyringMaker: { route in - onSwitchToKeyringMaker?(route) - }, - onNavigateToWorkshop: { route in - onSwitchToWorkshop?(route) - } - ) - - case .festivalView: - FestivalView(router: router) - case .festivalKeyringDetailView(let keyring): - FestivalKeyringDetailView( - festivalRouter: router, - viewModel: showcaseVM, - keyring: keyring - ) - case .coinCharge: - CoinChargeView(router: router) - } - } - } - } -} diff --git a/Keychy/Keychy/Presentation/Tab/Views/HomeTab.swift b/Keychy/Keychy/Presentation/Tab/Views/HomeTab.swift index 1b6869e76..1b11cdcdf 100644 --- a/Keychy/Keychy/Presentation/Tab/Views/HomeTab.swift +++ b/Keychy/Keychy/Presentation/Tab/Views/HomeTab.swift @@ -13,6 +13,7 @@ struct HomeTab: View { @State private var collectionViewModel = CollectionViewModel() @State private var bundleViewModel = BundleViewModel() @Bindable private var introViewModel = IntroViewModel() + @State private var festivalViewModel = Showcase25BoardViewModel() /// 배경 로드 완료 콜백 var onBackgroundLoaded: (() -> Void)? = nil @@ -59,6 +60,14 @@ struct HomeTab: View { IntroView(viewModel: introViewModel) case .termsAndPolicy: TermsView(router: router) + + // Festival + case .festivalView: + FestivalView(router: router) + case .showcase25BoardView: + Showcase25BoardView(router: router, viewModel: festivalViewModel) + case .festivalKeyringDetailView(let keyring): + FestivalKeyringDetailView(router: router, viewModel: festivalViewModel, keyring: keyring) } } } diff --git a/Keychy/Keychy/Presentation/Tab/Views/MainTabView.swift b/Keychy/Keychy/Presentation/Tab/Views/MainTabView.swift index cf7f89c9b..a9d61c57c 100644 --- a/Keychy/Keychy/Presentation/Tab/Views/MainTabView.swift +++ b/Keychy/Keychy/Presentation/Tab/Views/MainTabView.swift @@ -9,7 +9,7 @@ import SwiftUI /// 앱의 메인 탭 화면 /// -/// 4개의 메인 탭(홈, 공방, 보관함, 페스티벌)을 관리하고 +/// 3개의 메인 탭(홈, 공방, 보관함)을 관리하고 /// 딥링크 처리, 스플래시 화면, 배지 카운트 동기화 등을 담당 struct MainTabView: View { @State private var viewModel = MainTabViewModel() @@ -50,7 +50,6 @@ extension MainTabView { homeTab workshopTab collectionTab - festivalTab } .tint(.main500) .tabBarMinimizeBehavior(.onScrollDown) @@ -97,16 +96,12 @@ extension MainTabView { } private var workshopTab: some View { - WorkshopTab( - router: viewModel.workshopRouter, - festivalRouter: viewModel.festivalRouter, - festivalVM: viewModel.festivalViewModel - ) - .modifier(TabItemModifier( - image: .workshop, - title: "공방", - tag: MainTabViewModel.TabIndex.workshop.rawValue - )) + WorkshopTab(router: viewModel.workshopRouter) + .modifier(TabItemModifier( + image: .workshop, + title: "공방", + tag: MainTabViewModel.TabIndex.workshop.rawValue + )) } private var collectionTab: some View { @@ -121,23 +116,6 @@ extension MainTabView { )) } - private var festivalTab: some View { - FestivalTab( - router: viewModel.festivalRouter, - showcaseVM: viewModel.festivalViewModel, - onSwitchToKeyringMaker: { route in - viewModel.handleSwitchToKeyringMaker(route) - }, - onSwitchToWorkshop: { route in - viewModel.handleSwitchToWorkshop(route) - } - ) - .modifier(TabItemModifier( - image: .festival, - title: "페스티벌", - tag: MainTabViewModel.TabIndex.festival.rawValue - )) - } } // MARK: - Sheet Contents diff --git a/Keychy/Keychy/Presentation/Tab/Views/WorkshopTab.swift b/Keychy/Keychy/Presentation/Tab/Views/WorkshopTab.swift index 0a4b37f18..ca8a6f9a0 100644 --- a/Keychy/Keychy/Presentation/Tab/Views/WorkshopTab.swift +++ b/Keychy/Keychy/Presentation/Tab/Views/WorkshopTab.swift @@ -9,8 +9,6 @@ import SwiftUI struct WorkshopTab: View { @Bindable var router: NavigationRouter - @Bindable var festivalRouter: NavigationRouter - @Bindable var festivalVM: Showcase25BoardViewModel @State private var acrylicPhotoVM: AcrylicPhotoVM? @State private var neonSignVM: NeonSignVM? @@ -67,17 +65,6 @@ struct WorkshopTab: View { case .coinCharge: CoinChargeView(router: router) - // MARK: - 쇼케이스용 페스티벌 임시 라우트 - case .showcase25BoardView: - Showcase25BoardView(festivalRouter: festivalRouter, viewModel: festivalVM) - - case .festivalKeyringDetailView(let keyring): - FestivalKeyringDetailView( - festivalRouter: festivalRouter, - viewModel: festivalVM, - keyring: keyring - ) - // MARK: - AcrylicPhoto case .acrylicPhotoPreview: AcrylicPhotoPreView(router: router, viewModel: getAcrylicPhotoVM()) @@ -101,10 +88,7 @@ struct WorkshopTab: View { KeyringCompleteView( router: router, viewModel: getAcrylicPhotoVM(), - navigationTitle: "키링이 완성되었어요!", - onCloseFromFestival: festivalVM.isFromFestivalTab ? { router in - festivalVM.onKeyringCompleteFromFestival?(router) - } : nil + navigationTitle: "키링이 완성되었어요!" ) // MARK: - NeonSign @@ -126,10 +110,7 @@ struct WorkshopTab: View { KeyringCompleteView( router: router, viewModel: getNeonSignVM(), - navigationTitle: "키링이 완성되었어요!", - onCloseFromFestival: festivalVM.isFromFestivalTab ? { router in - festivalVM.onKeyringCompleteFromFestival?(router) - } : nil + navigationTitle: "키링이 완성되었어요!" ) // MARK: - Polaroid @@ -151,10 +132,7 @@ struct WorkshopTab: View { KeyringCompleteView( router: router, viewModel: getPolaroidVM(), - navigationTitle: "키링이 완성되었어요!", - onCloseFromFestival: festivalVM.isFromFestivalTab ? { router in - festivalVM.onKeyringCompleteFromFestival?(router) - } : nil + navigationTitle: "키링이 완성되었어요!" ) // MARK: - ClearSketch @@ -180,10 +158,7 @@ struct WorkshopTab: View { KeyringCompleteView( router: router, viewModel: getClearSketchVM(), - navigationTitle: "키링이 완성되었어요!", - onCloseFromFestival: festivalVM.isFromFestivalTab ? { router in - festivalVM.onKeyringCompleteFromFestival?(router) - } : nil + navigationTitle: "키링이 완성되었어요!" ) // MARK: - Pixel @@ -207,10 +182,7 @@ struct WorkshopTab: View { KeyringCompleteView( router: router, viewModel: getPixelKeyringVM(), - navigationTitle: "키링이 완성되었어요!", - onCloseFromFestival: festivalVM.isFromFestivalTab ? { router in - festivalVM.onKeyringCompleteFromFestival?(router) - } : nil + navigationTitle: "키링이 완성되었어요!" ) // MARK: - SpeechBubble @@ -232,10 +204,7 @@ struct WorkshopTab: View { KeyringCompleteView( router: router, viewModel: getSpeechBubbleVM(), - navigationTitle: "키링이 완성되었어요!", - onCloseFromFestival: festivalVM.isFromFestivalTab ? { router in - festivalVM.onKeyringCompleteFromFestival?(router) - } : nil + navigationTitle: "키링이 완성되었어요!" ) // MARK: - 선물 포장 완료