diff --git a/Keychy/Keychy.xcodeproj/project.pbxproj b/Keychy/Keychy.xcodeproj/project.pbxproj index 56bf3993c..6053b661d 100644 --- a/Keychy/Keychy.xcodeproj/project.pbxproj +++ b/Keychy/Keychy.xcodeproj/project.pbxproj @@ -83,6 +83,8 @@ 38D17A512EBBF88C00F52A88 /* CollectionViewModel+Edit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D17A502EBBF88C00F52A88 /* CollectionViewModel+Edit.swift */; }; 38DD90622ED594C00042EB45 /* FestivalKeyringContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38DD90612ED594C00042EB45 /* FestivalKeyringContextMenu.swift */; }; 38DD909C2EF1679D0042EB45 /* Color+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38DD909B2EF1679D0042EB45 /* Color+Extension.swift */; }; + 38DE8C432F38ECEF00C87924 /* BundleViewModel+Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38DE8C422F38ECEF00C87924 /* BundleViewModel+Filter.swift */; }; + 38DE8C452F38FAA700C87924 /* BundleViewModel+Sort.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38DE8C442F38FAA700C87924 /* BundleViewModel+Sort.swift */; }; 38F832CB2EC9067300D3A248 /* WidgetOnboardingStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F832CA2EC9067300D3A248 /* WidgetOnboardingStepView.swift */; }; 38F832CD2EC90DEF00D3A248 /* WidgetOnboardingStepView+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F832CC2EC90DEF00D3A248 /* WidgetOnboardingStepView+Helpers.swift */; }; 38F832CF2EC914C900D3A248 /* InvenExpandPopup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F832CE2EC914C900D3A248 /* InvenExpandPopup.swift */; }; @@ -359,9 +361,6 @@ AA2146B72F15E5B60048D40E /* BundleEditView+SelectSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA2146B62F15E5B60048D40E /* BundleEditView+SelectSheet.swift */; }; AA2146BB2F161D0C0048D40E /* BundleEditView+Initialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA2146BA2F161D0C0048D40E /* BundleEditView+Initialization.swift */; }; AA3908F82EC8BF0400D87EEC /* BundleDetailView+SaveImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3908F72EC8BF0400D87EEC /* BundleDetailView+SaveImage.swift */; }; - BC4CMPLT2F3B123400000001 /* BundleCompleteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC1CMPLT2F3B123400000001 /* BundleCompleteView.swift */; }; - BC5CMPLT2F3B123400000002 /* BundleCompleteView+VideoGen.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC2CMPLT2F3B123400000002 /* BundleCompleteView+VideoGen.swift */; }; - BC6CMPLT2F3B123400000003 /* BundleCompleteView+SaveImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC3CMPLT2F3B123400000003 /* BundleCompleteView+SaveImage.swift */; }; AA3909462EC9F29500D87EEC /* UIApplication+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3909452EC9F29500D87EEC /* UIApplication+Extension.swift */; }; AA39098E2ECA061700D87EEC /* GridItemSpacing.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA39098D2ECA061700D87EEC /* GridItemSpacing.swift */; }; AA390CE52ECC60A700D87EEC /* BundleRoute.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA390CE42ECC60A700D87EEC /* BundleRoute.swift */; }; @@ -396,6 +395,9 @@ BC0002102F35F00200000004 /* KeyringEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC00020E2F35F00200000002 /* KeyringEmptyStateView.swift */; }; BC0002132F35F00200000006 /* KeyringSelectionContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0002112F35F00200000005 /* KeyringSelectionContent.swift */; }; BC0002152F35F00200000008 /* BundleKeyringCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0002142F35F00200000007 /* BundleKeyringCellView.swift */; }; + BC4CMPLT2F3B123400000001 /* BundleCompleteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC1CMPLT2F3B123400000001 /* BundleCompleteView.swift */; }; + BC5CMPLT2F3B123400000002 /* BundleCompleteView+VideoGen.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC2CMPLT2F3B123400000002 /* BundleCompleteView+VideoGen.swift */; }; + BC6CMPLT2F3B123400000003 /* BundleCompleteView+SaveImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC3CMPLT2F3B123400000003 /* BundleCompleteView+SaveImage.swift */; }; C645AE9F2EB1055C004BFE69 /* CategoryTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = C645AE9E2EB1055C004BFE69 /* CategoryTabBar.swift */; }; C645AEA32EB1B8FC004BFE69 /* DataInitializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C645AEA22EB1B8FC004BFE69 /* DataInitializer.swift */; }; C665DDE82EAEFA8700CE4495 /* CoinChargeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C665DDE72EAEFA8700CE4495 /* CoinChargeView.swift */; }; @@ -548,6 +550,8 @@ 38D17A502EBBF88C00F52A88 /* CollectionViewModel+Edit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CollectionViewModel+Edit.swift"; sourceTree = ""; }; 38DD90612ED594C00042EB45 /* FestivalKeyringContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FestivalKeyringContextMenu.swift; sourceTree = ""; }; 38DD909B2EF1679D0042EB45 /* Color+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Extension.swift"; sourceTree = ""; }; + 38DE8C422F38ECEF00C87924 /* BundleViewModel+Filter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleViewModel+Filter.swift"; sourceTree = ""; }; + 38DE8C442F38FAA700C87924 /* BundleViewModel+Sort.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleViewModel+Sort.swift"; sourceTree = ""; }; 38F832CA2EC9067300D3A248 /* WidgetOnboardingStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetOnboardingStepView.swift; sourceTree = ""; }; 38F832CC2EC90DEF00D3A248 /* WidgetOnboardingStepView+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WidgetOnboardingStepView+Helpers.swift"; sourceTree = ""; }; 38F832CE2EC914C900D3A248 /* InvenExpandPopup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvenExpandPopup.swift; sourceTree = ""; }; @@ -822,9 +826,6 @@ AA2146B62F15E5B60048D40E /* BundleEditView+SelectSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleEditView+SelectSheet.swift"; sourceTree = ""; }; AA2146BA2F161D0C0048D40E /* BundleEditView+Initialization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleEditView+Initialization.swift"; sourceTree = ""; }; AA3908F72EC8BF0400D87EEC /* BundleDetailView+SaveImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleDetailView+SaveImage.swift"; sourceTree = ""; }; - BC1CMPLT2F3B123400000001 /* BundleCompleteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleCompleteView.swift; sourceTree = ""; }; - BC2CMPLT2F3B123400000002 /* BundleCompleteView+VideoGen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleCompleteView+VideoGen.swift"; sourceTree = ""; }; - BC3CMPLT2F3B123400000003 /* BundleCompleteView+SaveImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleCompleteView+SaveImage.swift"; sourceTree = ""; }; AA3909452EC9F29500D87EEC /* UIApplication+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Extension.swift"; sourceTree = ""; }; AA39098D2ECA061700D87EEC /* GridItemSpacing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridItemSpacing.swift; sourceTree = ""; }; AA390CE42ECC60A700D87EEC /* BundleRoute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleRoute.swift; sourceTree = ""; }; @@ -859,6 +860,9 @@ BC00020E2F35F00200000002 /* KeyringEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyringEmptyStateView.swift; sourceTree = ""; }; BC0002112F35F00200000005 /* KeyringSelectionContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyringSelectionContent.swift; sourceTree = ""; }; BC0002142F35F00200000007 /* BundleKeyringCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleKeyringCellView.swift; sourceTree = ""; }; + BC1CMPLT2F3B123400000001 /* BundleCompleteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleCompleteView.swift; sourceTree = ""; }; + BC2CMPLT2F3B123400000002 /* BundleCompleteView+VideoGen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleCompleteView+VideoGen.swift"; sourceTree = ""; }; + BC3CMPLT2F3B123400000003 /* BundleCompleteView+SaveImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleCompleteView+SaveImage.swift"; sourceTree = ""; }; C645AE9E2EB1055C004BFE69 /* CategoryTabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryTabBar.swift; sourceTree = ""; }; C645AEA22EB1B8FC004BFE69 /* DataInitializer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataInitializer.swift; sourceTree = ""; }; C665DDE72EAEFA8700CE4495 /* CoinChargeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinChargeView.swift; sourceTree = ""; }; @@ -2172,6 +2176,8 @@ 61SOUIL06G2NM0OODQ5MLB0A /* BundleViewModel+Fetch.swift */, AA69DD232F14C56F00C0A41C /* BundleViewModel+CRUD.swift */, AA69DD252F14C60000C0A41C /* BundleViewModel+Edit.swift */, + 38DE8C442F38FAA700C87924 /* BundleViewModel+Sort.swift */, + 38DE8C422F38ECEF00C87924 /* BundleViewModel+Filter.swift */, QM5GFF2WHZX9U24OOWPTMM4S /* BundleViewModel+Purchase.swift */, 40QZ1H4Y8EH2YZZUOT7WN7MX /* BundleViewModel+Helpers.swift */, DTQCH5OWZZJ8N03KVZERHJKR /* BundleViewModel+Cache.swift */, @@ -2227,6 +2233,16 @@ path = Edit; sourceTree = ""; }; + BC0CMPLT2F3B123400000000 /* Complete */ = { + isa = PBXGroup; + children = ( + BC1CMPLT2F3B123400000001 /* BundleCompleteView.swift */, + BC2CMPLT2F3B123400000002 /* BundleCompleteView+VideoGen.swift */, + BC3CMPLT2F3B123400000003 /* BundleCompleteView+SaveImage.swift */, + ); + path = Complete; + sourceTree = ""; + }; C665DDE92EAEFAA800CE4495 /* Coin */ = { isa = PBXGroup; children = ( @@ -2321,16 +2337,6 @@ path = Detail; sourceTree = ""; }; - BC0CMPLT2F3B123400000000 /* Complete */ = { - isa = PBXGroup; - children = ( - BC1CMPLT2F3B123400000001 /* BundleCompleteView.swift */, - BC2CMPLT2F3B123400000002 /* BundleCompleteView+VideoGen.swift */, - BC3CMPLT2F3B123400000003 /* BundleCompleteView+SaveImage.swift */, - ); - path = Complete; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -2670,6 +2676,7 @@ 4C4733BF2F1FA388005D2376 /* PolaroidVM.swift in Sources */, 4C4733C02F1FA388005D2376 /* ClearSketchPreview.swift in Sources */, 4C4733C12F1FA388005D2376 /* ClearSketchVM+Drawing.swift in Sources */, + 38DE8C452F38FAA700C87924 /* BundleViewModel+Sort.swift in Sources */, 4C4733C22F1FA388005D2376 /* PixelVM+Effect.swift in Sources */, 4C4733C32F1FA388005D2376 /* CropModels.swift in Sources */, 4C4733C42F1FA388005D2376 /* SpeechBubbleVM+Effect.swift in Sources */, @@ -2857,6 +2864,7 @@ BC00010B2F35F0010000000B /* BundleCreateView+Capture.swift in Sources */, BC00010C2F35F0010000000C /* BundleEditView+Capture.swift in Sources */, 38C3C2922EC1F787003C5DE1 /* CollectionKeyringDetailView+Lifecycle.swift in Sources */, + 38DE8C432F38ECEF00C87924 /* BundleViewModel+Filter.swift in Sources */, 4C8426642ED375840050B6FE /* ColorPalette.swift in Sources */, C6C402A32EB40ACA006B58DF /* AlarmView.swift in Sources */, C6B56F0B2EBC43AC0049F969 /* StoreProduct.swift in Sources */, diff --git a/Keychy/Keychy/Core/Navigation/Routes/BundleRoute.swift b/Keychy/Keychy/Core/Navigation/Routes/BundleRoute.swift index 72577575a..5e10ba0a8 100644 --- a/Keychy/Keychy/Core/Navigation/Routes/BundleRoute.swift +++ b/Keychy/Keychy/Core/Navigation/Routes/BundleRoute.swift @@ -17,4 +17,5 @@ protocol BundleRoute: Hashable { static var bundleEditView: Self { get } static var bundleCompleteView: Self { get } static var coinCharge: Self { get } + static var widgetSettingView: Self { get } } diff --git a/Keychy/Keychy/Core/Navigation/Routes/HomeRoute.swift b/Keychy/Keychy/Core/Navigation/Routes/HomeRoute.swift index d0b33faed..047ca0497 100644 --- a/Keychy/Keychy/Core/Navigation/Routes/HomeRoute.swift +++ b/Keychy/Keychy/Core/Navigation/Routes/HomeRoute.swift @@ -15,6 +15,9 @@ enum HomeRoute: Hashable, BundleRoute { case bundleNameEditView case bundleEditView case bundleCompleteView + + // Widget + case widgetSettingView // Home case coinCharge diff --git a/Keychy/Keychy/Core/Navigation/Routes/WorkshopRoute.swift b/Keychy/Keychy/Core/Navigation/Routes/WorkshopRoute.swift index 869c7f76d..39b187de4 100644 --- a/Keychy/Keychy/Core/Navigation/Routes/WorkshopRoute.swift +++ b/Keychy/Keychy/Core/Navigation/Routes/WorkshopRoute.swift @@ -67,6 +67,9 @@ enum WorkshopRoute: Hashable, BundleRoute { // MARK: - 선물 포장 완료 case packageComplete(keyringDocumentId: String, postOfficeId: String, templateId: String, shareLink: String) + // MARK: - 위젯 가이딩 + case widgetSettingView + /// template.id 문자열을 WorkshopRoute로 변환 static func from(string: String) -> WorkshopRoute? { switch string { diff --git a/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Filter.swift b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Filter.swift new file mode 100644 index 000000000..aeba69409 --- /dev/null +++ b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Filter.swift @@ -0,0 +1,26 @@ +// +// BundleViewModel+Filter.swift +// Keychy +// +// Created by Jini on 2/9/26. +// + +import SwiftUI + +extension BundleViewModel { + // MARK: - 검색 키워드 필터링 + /// 검색어로 뭉치 필터링 (이름 기준) + func getFilteredBundles(searchText: String = "") -> [KeyringBundle] { + var result = bundles + + // 검색 필터 + if !searchText.isEmpty { + result = result.filter { bundle in + bundle.name.localizedCaseInsensitiveContains(searchText) + } + } + + // 정렬 적용 + return sortBundles(result) + } +} diff --git a/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Sort.swift b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Sort.swift new file mode 100644 index 000000000..a0488bc58 --- /dev/null +++ b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Sort.swift @@ -0,0 +1,43 @@ +// +// BundleViewModel+Sort.swift +// Keychy +// +// Created by Jini on 2/9/26. +// + +import SwiftUI + +extension BundleViewModel { + // MARK: - 정렬 방식 + + /// 정렬 기준 변경 및 즉시 적용 + func updateSortOrder(_ newSort: String) { + selectedSort = newSort + } + + /// 뭉치 배열을 정렬 (메인 뭉치는 항상 최상단 유지) + func sortBundles(_ bundles: [KeyringBundle]) -> [KeyringBundle] { + // 메인 뭉치와 일반 뭉치 분리 + let mainBundles = bundles.filter { $0.isMain } + let normalBundles = bundles.filter { !$0.isMain } + + // 일반 뭉치를 선택된 정렬 기준으로 정렬 + let sortedNormalBundles: [KeyringBundle] + + switch selectedSort { + case "최신순": + sortedNormalBundles = normalBundles.sorted { $0.createdAt > $1.createdAt } + case "오래된순": + sortedNormalBundles = normalBundles.sorted { $0.createdAt < $1.createdAt } + case "이름순": + sortedNormalBundles = normalBundles.sorted { + $0.name.localizedStandardCompare($1.name) == .orderedAscending + } + default: + sortedNormalBundles = normalBundles.sorted { $0.createdAt > $1.createdAt } + } + + // 메인 뭉치(최신순 정렬) + 정렬된 일반 뭉치 + return mainBundles.sorted { $0.createdAt > $1.createdAt } + sortedNormalBundles + } +} diff --git a/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel.swift b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel.swift index 9e568b453..f9c57f962 100644 --- a/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel.swift +++ b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel.swift @@ -75,6 +75,10 @@ class BundleViewModel { var isLoading = false var isPurchasing = false + // MARK: - 정렬 상태 + + var selectedSort: String = "최신순" // 기본값 + // MARK: - 시트 필터/정렬 상태 var sheetSortOrder: String = "최신순" @@ -150,12 +154,7 @@ class BundleViewModel { // MARK: - 정렬된 뭉치 var sortedBundles: [KeyringBundle] { - bundles.sorted { a, b in - if a.isMain != b.isMain { - return a.isMain - } - return a.createdAt > b.createdAt - } + sortBundles(bundles) } // MARK: - 구성 ID 저장소 (편집 → 상세 화면 전환용) diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Detail/BundleDetailView+Menu.swift b/Keychy/Keychy/Presentation/Bundle/Views/Detail/BundleDetailView+Menu.swift index 0d1335ff7..fd7fae83a 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Detail/BundleDetailView+Menu.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Detail/BundleDetailView+Menu.swift @@ -67,6 +67,13 @@ extension BundleDetailView { uiState.showDeleteAlert = true } }, + onWidget: { + uiState.showMenu = false + + withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { + router.push(.widgetSettingView) + } + }, isMain: bundle.isMain ) .zIndex(50) diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Detail/BundleDetailView+VideoGen.swift b/Keychy/Keychy/Presentation/Bundle/Views/Detail/BundleDetailView+VideoGen.swift index 1a1c4d205..531a68141 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Detail/BundleDetailView+VideoGen.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Detail/BundleDetailView+VideoGen.swift @@ -86,4 +86,54 @@ extension BundleDetailView { return nil } } + + /// 공유용 영상 생성 + @MainActor + func generateVideoForShare() async { + guard !uiState.isGeneratingVideo else { return } + + // 이미 캐시된 영상이 있으면 바로 공유 + if cachedVideoURL != nil { + showShareSheet = true + return + } + + uiState.isGeneratingVideo = true + + guard let bundle = bundleVM.selectedBundle, + let background = bundleVM.selectedBackground, + let carabiner = bundleVM.selectedCarabiner else { + uiState.isGeneratingVideo = false + return + } + + do { + // 배경 이미지 로드 + let backgroundImage = await loadImage(from: background.backgroundImage) + + // 영상 생성 (기존 generateVideo 함수 사용) + let videoURL = try await videoGenerator.generateVideo( + keyringDataList: keyringDataList, + backgroundImage: backgroundImage, + backgroundImageURL: background.backgroundImage, + carabinerBackImageURL: carabiner.backImageURL, + carabinerFrontImageURL: carabiner.frontImageURL, + carabinerX: carabiner.carabinerX, + carabinerY: carabiner.carabinerY, + carabinerWidth: carabiner.carabinerWidth, + carabinerType: carabiner.type, + bundleScale: 2.5 + ) + + cachedVideoURL = videoURL + uiState.isGeneratingVideo = false + + // 영상 생성 완료 후 공유 시트 표시 + showShareSheet = true + + } catch { + print("[BundleDetail] 영상 생성 실패: \(error.localizedDescription)") + uiState.isGeneratingVideo = false + } + } } diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Detail/BundleDetailView.swift b/Keychy/Keychy/Presentation/Bundle/Views/Detail/BundleDetailView.swift index 3b7768d5c..a9d2b55d0 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Detail/BundleDetailView.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Detail/BundleDetailView.swift @@ -53,6 +53,10 @@ struct BundleDetailView: View { /// 영상 생성기 @State var videoGenerator = BundleVideoGenerator() + // 영상 공유 관련 + @State var cachedVideoURL: URL? + @State var showShareSheet: Bool = false + // MARK: - Body var body: some View { GeometryReader { geometry in @@ -110,23 +114,14 @@ struct BundleDetailView: View { VStack { Spacer() + bottomSection } - - // 영상 저장 버튼 - 이미지 저장 버튼 바로 위 - VStack { - Spacer() - HStack { - Spacer() - downloadVideoButton - } - .padding(.trailing, 16) - .padding(.bottom, 36 + 48 + 12) // bottomSection padding + button height + spacing - } } menuOverlay customnavigationBar + .adaptiveTopPadding() } .blur(radius: shouldShowAlertOverlay ? 15 : 0) .ignoresSafeArea() @@ -137,6 +132,13 @@ struct BundleDetailView: View { .ignoresSafeArea() .navigationBarBackButtonHidden(true) .withToast(position: .default) + .sheet(isPresented: $showShareSheet) { + if let url = cachedVideoURL { + ShareSheet(items: [url]) + .presentationDetents([.fraction(0.65)]) + .presentationDragIndicator(.visible) + } + } .onPreferenceChange(MenuButtonPreferenceKey.self) { frame in if frame != .zero { menuPosition = frame @@ -150,6 +152,7 @@ struct BundleDetailView: View { } .onDisappear { uiState.resetOverlays() + cleanupCachedVideo() // 진행 중인 작업들 취소 readyDelayTask?.cancel() @@ -174,6 +177,14 @@ struct BundleDetailView: View { } } } + + // MARK: - 캐시된 영상 정리 + private func cleanupCachedVideo() { + if let url = cachedVideoURL { + try? FileManager.default.removeItem(at: url) + cachedVideoURL = nil + } + } } // MARK: - Data Loading @@ -323,25 +334,50 @@ extension BundleDetailView { // MARK: - 커스텀 네비게이션 바 extension BundleDetailView { + private var safeAreaTop: CGFloat { + guard let window = UIApplication.shared.connectedScenes + .compactMap({ $0 as? UIWindowScene }) + .first?.windows + .first(where: { $0.isKeyWindow }) else { + return 0 + } + return window.safeAreaInsets.top + } + private var customnavigationBar: some View { - CustomNavigationBar { - BackToolbarButton { - bundleVM.lastKeyringsIdForDetail = "" - bundleVM.lastCarabinerIdForDetail = "" - bundleVM.lastBackgroundIdForDetail = "" - router.pop() - } - } center: { + ZStack { if let bundle = bundleVM.selectedBundle { Text("\(bundle.name)") + .typography(.notosans17M) + .foregroundStyle(.gray600) } - } trailing: { - MenuToolbarButton { - withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { - uiState.showMenu.toggle() + + HStack { + BackToolbarButton { + bundleVM.lastKeyringsIdForDetail = "" + bundleVM.lastCarabinerIdForDetail = "" + bundleVM.lastBackgroundIdForDetail = "" + router.pop() + } + + Spacer() + + HStack(spacing: 10) { + // 이미지 다운 버튼 + downloadImageButton + + // 메뉴 버튼 + MenuToolbarButton { + withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { + uiState.showMenu.toggle() + } + } } } + .padding(.horizontal, 16) } + .frame(height: 44) + .padding(.top, safeAreaTop) } } @@ -371,7 +407,7 @@ extension BundleDetailView { Spacer() - downloadImageButton + shareButton } } .padding(EdgeInsets(top: 4, leading: 16, bottom: 36, trailing: 16)) @@ -409,6 +445,25 @@ extension BundleDetailView { } } + /// 공유 버튼 + private var shareButton: some View { + Button(action: { + if cachedVideoURL != nil { + showShareSheet = true + return + } + Task { + await generateVideoForShare() + } + }) { + Image(.share) + } + .disabled(uiState.isGeneratingVideo || uiState.isCapturing) + .frame(width: 48, height: 48) + .glassEffect(in: .circle) + .opacity((uiState.isGeneratingVideo || uiState.isCapturing) ? 0.5 : 1) + } + /// 영상 다운로드 버튼 private var downloadVideoButton: some View { Button(action: { @@ -435,7 +490,7 @@ extension BundleDetailView { Image(.imageDownload) } .disabled(uiState.isCapturing || uiState.isGeneratingVideo) - .frame(width: 48, height: 48) + .frame(width: 44, height: 44) .glassEffect(in: .circle) .opacity((uiState.isCapturing || uiState.isGeneratingVideo) ? 0.5 : 1) } diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Detail/BundleMenu.swift b/Keychy/Keychy/Presentation/Bundle/Views/Detail/BundleMenu.swift index 9c98506a1..bc89b7113 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Detail/BundleMenu.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Detail/BundleMenu.swift @@ -12,12 +12,11 @@ struct BundleMenu: View { let onNameEdit: () -> Void let onEdit: () -> Void let onDelete: () -> Void + let onWidget: () -> Void let isMain: Bool private let menuWidth: CGFloat = 185 - private var menuHeight: CGFloat { - isMain ? 120 : 175 // 메인 뭉치면 삭제 버튼 없음 - } + private let menuHeight: CGFloat = 218 @State private var isAppearing = false @@ -62,23 +61,55 @@ struct BundleMenu: View { .frame(maxWidth: .infinity, alignment: .leading) // 삭제 버튼 - if !isMain { - Button(action: onDelete) { - HStack(spacing: 8) { - Image(.trash) - - Text("삭제") - .typography(.suit16M) - .foregroundColor(.pink) - - Spacer() - } - .padding(.vertical, 10) - .padding(.horizontal, 10) - .contentShape(Rectangle()) + HStack(spacing: 8) { + Image(.trash) + .renderingMode(.template) + .resizable() + .frame(width: 24, height: 24) + .foregroundColor(isMain ? .gray300 : .pink) + + Text("삭제") + .typography(.suit16M) + .foregroundColor(isMain ? .gray300 : .pink) + + Spacer() + } + .padding(.vertical, 10) + .padding(.horizontal, 10) + .contentShape(Rectangle()) + .frame(maxWidth: .infinity, alignment: .leading) + .onTapGesture { + if !isMain { + onDelete() } - .frame(maxWidth: .infinity, alignment: .leading) } + + // 구분선 + Rectangle() + .fill(Color.gray100) + .padding(.horizontal, 10) + .frame(height: 1) + + // 위젯 버튼 + Button(action: onWidget) { + HStack(spacing: 8) { + Image(.widget) + .renderingMode(.template) + .resizable() + .frame(width: 24, height: 24) + .foregroundColor(.gray600) + + Text("위젯 설정") + .typography(.suit16M) + .foregroundColor(.gray600) + + Spacer() + } + .padding(.vertical, 10) + .padding(.horizontal, 10) + .contentShape(Rectangle()) + } + .frame(maxWidth: .infinity, alignment: .leading) } .padding(.horizontal, 10) .padding(.vertical, 20) diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleGridItem.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleGridItem.swift index 351d2cc8c..ec381a71f 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleGridItem.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleGridItem.swift @@ -12,6 +12,7 @@ import FirebaseFirestore struct BundleGridItem: View { let bundle: KeyringBundle + var searchKeyword: String = "" // 검색 키워드 (기본값 빈 문자열) @State private var cachedImage: Image? @State private var isCapturing: Bool = false @@ -46,16 +47,60 @@ struct BundleGridItem: View { .padding(10) } } - HStack { + // 번들 이름 (검색 키워드 하이라이트 적용) + bundleNameView + } //: VSTACK + .onAppear { + loadBundleImage() + } + } + + // MARK: - Bundle Name View + private var bundleNameView: some View { + HStack { + if !searchKeyword.isEmpty { + // 검색 모드: 하이라이트 적용 + Text(highlightedText(text: bundle.name, keyword: searchKeyword)) + } else { + // 일반 모드 Text(bundle.name) .typography(.notosans14M) .foregroundStyle(.black100) } - } //: VSTACK - .onAppear { - loadBundleImage() } } + + // MARK: - 검색 키워드 Highlighted Text + private func highlightedText(text: String, keyword: String) -> AttributedString { + var attributedString = AttributedString(text) + + guard !keyword.isEmpty else { + attributedString.font = .notosans14M + return attributedString + } + + attributedString.font = .notosans14M + attributedString.foregroundColor = .black100 + + let lowerText = text.lowercased() + let lowerKeyword = keyword.lowercased() + + var searchRange = lowerText.startIndex.., + viewModel: CollectionViewModel, + keyring: Keyring, + isSearchMode: Bool = false + ) { + self.router = router + self.viewModel = viewModel + self._keyring = State(initialValue: keyring) + self.isSearchMode = isSearchMode + } var body: some View { GeometryReader { geometry in @@ -151,7 +165,10 @@ struct CollectionKeyringDetailView: View { .onPreferenceChange(MenuButtonPreferenceKey.self) { frame in menuPosition = frame } - + .task { + // 뷰가 다시 나타날 때 키링 데이터 새로고침 + await refreshKeyringData() + } } private var shouldApplyBlur: Bool { @@ -176,6 +193,30 @@ struct CollectionKeyringDetailView: View { } } + /// 키링 데이터 새로고침 (편집 후 돌아왔을 때) + private func refreshKeyringData() async { + guard let documentId = keyring.documentId else { return } + + // ViewModel에서 최신 키링 데이터 찾기 + if let updatedKeyring = viewModel.keyring.first(where: { $0.documentId == documentId }) { + await MainActor.run { + self.keyring = updatedKeyring + } + } else { + // 로컬에 없으면 Firebase에서 직접 가져오기 + await withCheckedContinuation { continuation in + viewModel.fetchKeyringById(keyringId: documentId) { fetchedKeyring in + if let fetchedKeyring = fetchedKeyring { + Task { @MainActor in + self.keyring = fetchedKeyring + } + } + continuation.resume() + } + } + } + } + /// 씬 스케일 (시트 최대화 시 작게, 최소화 시 크게) private var sceneScale: CGFloat { isSheetPresented == false ? 1.1 : 0.8 diff --git a/Keychy/Keychy/Presentation/Collection/Views/Detail/KeyringEditView.swift b/Keychy/Keychy/Presentation/Collection/Views/Detail/KeyringEditView.swift index adff7a9f1..a87498e41 100644 --- a/Keychy/Keychy/Presentation/Collection/Views/Detail/KeyringEditView.swift +++ b/Keychy/Keychy/Presentation/Collection/Views/Detail/KeyringEditView.swift @@ -159,11 +159,7 @@ extension KeyringEditView { tags: editedTags ) { success in if success { - router.reset() - TabBarManager.show() - - //TODO: 수정완료 후 pop (데이터 새로고침 로직 추가 예정) - //router.pop() + router.pop() } } } label: { diff --git a/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView+NormalMode.swift b/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView+NormalMode.swift index 7d68c8cea..69d77aba5 100644 --- a/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView+NormalMode.swift +++ b/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView+NormalMode.swift @@ -55,7 +55,6 @@ extension CollectionView { VStack { headerSection .padding(.horizontal, Spacing.margin) - .padding(.top, 2) if collectionViewModel.collectionToggle { tagSection @@ -78,7 +77,6 @@ extension CollectionView { VStack(spacing: 0) { headerSection .padding(.horizontal, Spacing.margin) - .padding(.top, 2) .padding(.bottom, 10) tagSection @@ -110,7 +108,6 @@ extension CollectionView { VStack(spacing: 0) { headerSection .padding(.horizontal, Spacing.margin) - .padding(.top, 2) .padding(.bottom, 10) collectionHeader @@ -309,6 +306,7 @@ extension CollectionView { } } .padding(.bottom, 90) + .padding(.horizontal, Spacing.gap) } private var bundleEmptyView: some View { @@ -382,7 +380,7 @@ extension CollectionView { showSortSheet = true }) { HStack(spacing: 2) { - Text(collectionViewModel.selectedSort) + Text(currentSortText) .typography(.suit14SB18) .foregroundColor(.gray500) @@ -401,6 +399,13 @@ extension CollectionView { .buttonStyle(PlainButtonStyle()) } + // 현재 탭에 따른 정렬 텍스트 + private var currentSortText: String { + collectionViewModel.collectionToggle + ? collectionViewModel.selectedSort + : bundleViewModel.selectedSort + } + var emptyView: some View { VStack { Spacer() diff --git a/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView+SearchMode.swift b/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView+SearchMode.swift index 0fdf6112c..a6b8c8a79 100644 --- a/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView+SearchMode.swift +++ b/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView+SearchMode.swift @@ -14,78 +14,23 @@ extension CollectionView { Group { if collectionViewModel.hasNetworkError { // 네트워크 에러: 오버레이 형태 - ZStack(alignment: .top) { - Color.white - .ignoresSafeArea() - - NoInternetView(topPadding: getSafeAreaTop() + 90, onRetry: { - Task { - guard let uid = UserDefaults.standard.string(forKey: "userUID") else { - print("UID를 찾을 수 없습니다") - return - } - await collectionViewModel.retryFetchData(userId: uid) - } - }) - .ignoresSafeArea() - - VStack(spacing: 10) { - VStack(spacing: 10) { - Spacer() - .frame(height: 60) - - HStack { - Spacer() - - Text("\(filteredKeyrings.count)개 발견됨") - .typography(.suit14M) - .foregroundColor(.gray500) - .padding(.top, 22) - .padding(.trailing, 22) - } - - HStack { - Text("키링") - .typography(.suit16B) - .foregroundColor(.gray500) - .padding(.leading, 20) - - Spacer() - } - .opacity(!filteredKeyrings.isEmpty ? 1 : 0) - } - .background(Color.white) - - Spacer() - } - } + networkErrorView } else { // 정상 상태: 기존 VStack 형태 VStack(spacing: 10) { - Spacer() - .frame(height: 60) - - HStack { - Spacer() - - Text("\(filteredKeyrings.count)개 발견됨") - .typography(.suit14M) - .foregroundColor(.gray500) - .padding(.top, 22) - .padding(.trailing, 22) - } - - HStack { - Text("키링") - .typography(.suit16B) - .foregroundColor(.gray500) - .padding(.leading, 20) - - Spacer() - } - .opacity(!filteredKeyrings.isEmpty ? 1 : 0) - - searchCollectionSection + // 헤더 + searchHeaderSection + + // 세그먼트 컨트롤 + searchSegmentControl + .padding(.horizontal, Spacing.margin) + + // 결과 카운트 + searchResultCount + + // 컨텐츠 + searchContentSection + .padding(.top, -4) } .contentShape(Rectangle()) .onTapGesture { @@ -95,10 +40,88 @@ extension CollectionView { } } } - } - var searchCollectionSection: some View { + // 검색 헤더 + private var searchHeaderSection: some View { + HStack { + Text("검색") + .typography(.nanum24EB) + .foregroundColor(.black100) + + Spacer() + } + .padding(.vertical, Spacing.sm) + .padding(.top, 60) + .padding(.leading, 16) + } + + // 세그먼트 컨트롤 + private var searchSegmentControl: some View { + HStack(spacing: 0) { + segmentButton(title: "키링", segment: .keyring) + segmentButton(title: "뭉치", segment: .bundle) + } + .frame(height: 46) + .background(Color.gray50) + .cornerRadius(100) + } + + private func segmentButton(title: String, segment: SearchSegment) -> some View { + Button { + withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { + searchSegment = segment + } + } label: { + Text(title) + .typography(.suit16M) + .foregroundColor(.black100) + .frame(maxWidth: .infinity) + .frame(height: 38) + .background( + searchSegment == segment ? + .white100 : .clear + ) + .cornerRadius(20) + } + .buttonStyle(.plain) + .padding(4) + } + + // 결과 카운트 + private var searchResultCount: some View { + HStack { + Text("\(currentSearchResultCount)개 발견됨") + .typography(.suit14M) + .foregroundColor(.gray500) + .padding(.top, 10) + .padding(.leading, 22) + + Spacer() + } + } + + private var currentSearchResultCount: Int { + switch searchSegment { + case .keyring: + return filteredKeyrings.count + case .bundle: + return filteredBundles.count + } + } + + var searchContentSection: some View { + Group { + if searchSegment == .keyring { + keyringSearchResults + } else { + bundleSearchResults + } + } + } + + // 키링 검색 결과 + private var keyringSearchResults: some View { ScrollView { VStack(spacing: 0) { if filteredKeyrings.isEmpty { @@ -110,20 +133,107 @@ extension CollectionView { .padding(.horizontal, Spacing.xs) } .scrollIndicators(.hidden) - .simultaneousGesture( - DragGesture().onChanged { _ in - if showSearchBar { - isSearchFieldFocused = false + .simultaneousGesture(scrollGesture) + } + + // 뭉치 검색 결과 + private var bundleSearchResults: some View { + ScrollView { + VStack(spacing: 0) { + if filteredBundles.isEmpty { + searchEmptyView + } else { + bundleSearchGrid + } + } + .padding(.horizontal, Spacing.xs) + } + .scrollIndicators(.hidden) + .simultaneousGesture(scrollGesture) + } + + private var bundleSearchGrid: some View { + LazyVGrid(columns: columns, spacing: 11) { + ForEach(filteredBundles, id: \.documentId) { bundle in + Button { + guard NetworkManager.shared.isConnected else { + ToastManager.shared.show() + return + } + + // 키보드 내리기 + if isSearchFieldFocused { + isSearchFieldFocused = false + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + bundleViewModel.selectedBundle = bundle + bundleViewModel.selectedBackground = bundleViewModel.resolveBackground(from: bundle.selectedBackground) + bundleViewModel.selectedCarabiner = bundleViewModel.resolveCarabiner(from: bundle.selectedCarabiner) + router.push(.bundleDetailView) + } + } label: { + BundleGridItem( + bundle: bundle, + searchKeyword: searchText + ) + + } + .buttonStyle(.plain) + } + } + .padding(.horizontal, Spacing.gap) + .padding(.vertical, 4) + .padding(.bottom, 90) + } + + // 스크롤 제스처 + private var scrollGesture: some Gesture { + DragGesture().onChanged { _ in + if showSearchBar { + isSearchFieldFocused = false + + if !isSearching { + withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { + showSearchBar = false + } + } + } + } + } - if !isSearching { - withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { - showSearchBar = false - } + private var networkErrorView: some View { + ZStack(alignment: .top) { + Color.white + .ignoresSafeArea() + + NoInternetView(topPadding: getSafeAreaTop() + 90, onRetry: { + Task { + guard let uid = UserDefaults.standard.string(forKey: "userUID") else { + print("UID를 찾을 수 없습니다") + return } + await collectionViewModel.retryFetchData(userId: uid) } + }) + .ignoresSafeArea() + + VStack(spacing: 10) { + VStack(spacing: 10) { + searchHeaderSection + + searchSegmentControl + .padding(.horizontal, Spacing.margin) + + searchResultCount + + Spacer() + } + .background(Color.white) } - ) + } } + var searchEmptyView: some View { VStack { @@ -222,3 +332,10 @@ extension CollectionView { return attributedString } } + +extension UISegmentedControl { + override open func didMoveToSuperview() { + super.didMoveToSuperview() + self.setContentHuggingPriority(.defaultLow, for: .vertical) + } +} diff --git a/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView.swift b/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView.swift index 65d8b5366..ed7a29d02 100644 --- a/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView.swift +++ b/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView.swift @@ -8,6 +8,11 @@ import SwiftUI import SpriteKit +enum SearchSegment { + case keyring + case bundle +} + struct CollectionView: View { @Bindable var router: NavigationRouter @State var collectionViewModel: CollectionViewModel @@ -32,6 +37,7 @@ struct CollectionView: View { @State var newCategoryName: String = "" @State var showingMenuFor: String? @State var menuPosition: CGRect = .zero + @State var searchSegment: SearchSegment = .keyring // 디버그용 @State var showCachedImagesDebug: Bool = false @@ -59,6 +65,11 @@ struct CollectionView: View { ) } + // 필터링된 뭉치 (검색) + var filteredBundles: [KeyringBundle] { + bundleViewModel.getFilteredBundles(searchText: isSearching ? searchText : "") + } + let columns: [GridItem] = [ GridItem(.flexible(), spacing: Spacing.gap), GridItem(.flexible(), spacing: Spacing.gap) @@ -294,7 +305,12 @@ struct CollectionView: View { title: sort, isSelected: collectionViewModel.selectedSort == sort ) { - collectionViewModel.updateSortOrder(sort) + // 현재 탭에 따라 정렬 적용 + if collectionViewModel.collectionToggle { + collectionViewModel.updateSortOrder(sort) + } else { + bundleViewModel.updateSortOrder(sort) + } showSortSheet = false } } diff --git a/Keychy/Keychy/Presentation/Collection/Views/Widget/WidgetSettingView.swift b/Keychy/Keychy/Presentation/Collection/Views/Widget/WidgetSettingView.swift index 362534b90..36f85cf49 100644 --- a/Keychy/Keychy/Presentation/Collection/Views/Widget/WidgetSettingView.swift +++ b/Keychy/Keychy/Presentation/Collection/Views/Widget/WidgetSettingView.swift @@ -7,8 +7,8 @@ import SwiftUI -struct WidgetSettingView: View { - @Bindable var router: NavigationRouter +struct WidgetSettingView: View { + @Bindable var router: NavigationRouter private let steps = WidgetOnboardingStep.steps diff --git a/Keychy/Keychy/Presentation/Tab/Views/CollectionTab.swift b/Keychy/Keychy/Presentation/Tab/Views/CollectionTab.swift index 30c00b2ca..f5617d5df 100644 --- a/Keychy/Keychy/Presentation/Tab/Views/CollectionTab.swift +++ b/Keychy/Keychy/Presentation/Tab/Views/CollectionTab.swift @@ -20,7 +20,7 @@ struct CollectionTab: View { switch route { case .collectionKeyringDetailView(let keyring, let isSearchMode): - CollectionKeyringDetailView(router: router, viewModel: collectionViewModel, isSearchMode: isSearchMode, keyring: keyring) + CollectionKeyringDetailView(router: router, viewModel: collectionViewModel, keyring: keyring, isSearchMode: isSearchMode) case .collectionKeyringPackageView(let keyring, let isSearchMode): CollectionKeyringPackageView(router: router, viewModel: collectionViewModel, isSearchMode: isSearchMode, keyring: keyring) case .keyringEditView(let keyring): diff --git a/Keychy/Keychy/Presentation/Tab/Views/HomeTab.swift b/Keychy/Keychy/Presentation/Tab/Views/HomeTab.swift index 2e98ba8d6..7ba8247d0 100644 --- a/Keychy/Keychy/Presentation/Tab/Views/HomeTab.swift +++ b/Keychy/Keychy/Presentation/Tab/Views/HomeTab.swift @@ -63,6 +63,8 @@ struct HomeTab: View { IntroView(viewModel: introViewModel) case .termsAndPolicy: TermsView(router: router) + case .widgetSettingView: + WidgetSettingView(router: router) // Festival case .festivalView: diff --git a/Keychy/Keychy/Presentation/Tab/Views/WorkshopTab.swift b/Keychy/Keychy/Presentation/Tab/Views/WorkshopTab.swift index 4872b23ff..6b9129800 100644 --- a/Keychy/Keychy/Presentation/Tab/Views/WorkshopTab.swift +++ b/Keychy/Keychy/Presentation/Tab/Views/WorkshopTab.swift @@ -62,6 +62,10 @@ struct WorkshopTab: View { // MARK: - 재화 구매뷰 case .coinCharge: CoinChargeView(router: router) + + // MARK: - 위젯 가이딩 + case .widgetSettingView: + WidgetSettingView(router: router) // MARK: - AcrylicPhoto case .acrylicPhotoPreview: