diff --git a/Keychy/Keychy.xcodeproj/project.pbxproj b/Keychy/Keychy.xcodeproj/project.pbxproj index 46bb0339b..93e26e870 100644 --- a/Keychy/Keychy.xcodeproj/project.pbxproj +++ b/Keychy/Keychy.xcodeproj/project.pbxproj @@ -7,7 +7,7 @@ objects = { /* Begin PBXBuildFile section */ - AA0000022F17000100000001 /* BundleSwitchPopup.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0000012F17000100000001 /* BundleSwitchPopup.swift */; }; + 324NSC72E34Q6L9EW2AV1KL9 /* BundleViewModel+Fetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61SOUIL06G2NM0OODQ5MLB0A /* BundleViewModel+Fetch.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 */; }; @@ -86,6 +86,7 @@ 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 */; }; + 40WF8CXMLHGD9B5S521VX89Y /* BundleViewModel+Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = DTQCH5OWZZJ8N03KVZERHJKR /* BundleViewModel+Cache.swift */; }; 4C004F912F164D5500D9063E /* TabBarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C004F902F164D5500D9063E /* TabBarManager.swift */; }; 4C004FA12F177C4600D9063E /* BundleVideoGenerator+Rendering.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C004F982F177C4600D9063E /* BundleVideoGenerator+Rendering.swift */; }; 4C004FA22F177C4600D9063E /* KeyringVideoGenerator+Setup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C004F9E2F177C4600D9063E /* KeyringVideoGenerator+Setup.swift */; }; @@ -311,7 +312,7 @@ 4CEC61F12EAE08C40099ECEE /* KeychyApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEC61EE2EAE08C40099ECEE /* KeychyApp.swift */; }; 4CEC621A2EAE08DA0099ECEE /* KeyringScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEC61F92EAE08DA0099ECEE /* KeyringScene.swift */; }; 4CEC621B2EAE08DA0099ECEE /* KeyringScene+Touch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEC61FC2EAE08DA0099ECEE /* KeyringScene+Touch.swift */; }; - 4CEC621C2EAE08DA0099ECEE /* BackgroundSelectableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEC61F62EAE08DA0099ECEE /* BackgroundSelectableCell.swift */; }; + 4CEC621C2EAE08DA0099ECEE /* BackgroundCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEC61F62EAE08DA0099ECEE /* BackgroundCell.swift */; }; 4CEC621D2EAE08DA0099ECEE /* HomeRoute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEC62072EAE08DA0099ECEE /* HomeRoute.swift */; }; 4CEC621E2EAE08DA0099ECEE /* KeyringChainComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEC61FF2EAE08DA0099ECEE /* KeyringChainComponent.swift */; }; 4CEC621F2EAE08DA0099ECEE /* KeyringBodyComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEC62002EAE08DA0099ECEE /* KeyringBodyComponent.swift */; }; @@ -330,7 +331,7 @@ 4CEC622F2EAE08DA0099ECEE /* NavigationRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEC620C2EAE08DA0099ECEE /* NavigationRouter.swift */; }; 4CEC62302EAE08DA0099ECEE /* KeyringScene+Effects.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEC61FA2EAE08DA0099ECEE /* KeyringScene+Effects.swift */; }; 4CEC62312EAE08DA0099ECEE /* KeyringScene+Swipe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEC61FD2EAE08DA0099ECEE /* KeyringScene+Swipe.swift */; }; - 4CEC62332EAE08DA0099ECEE /* KeyringBundleItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEC61F52EAE08DA0099ECEE /* KeyringBundleItem.swift */; }; + 4CEC62332EAE08DA0099ECEE /* BundleGridItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEC61F52EAE08DA0099ECEE /* BundleGridItem.swift */; }; 4CEC62342EAE08DA0099ECEE /* Spacing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEC62142EAE08DA0099ECEE /* Spacing.swift */; }; 4CEC626C2EAE08DF0099ECEE /* FestivalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEC62442EAE08DF0099ECEE /* FestivalView.swift */; }; 4CEC62752EAE08DF0099ECEE /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEC624B2EAE08DF0099ECEE /* HomeView.swift */; }; @@ -344,12 +345,16 @@ 4CF2A96A2F0B94EA00BA9FDA /* View+PullToRefresh.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF2A9692F0B94EA00BA9FDA /* View+PullToRefresh.swift */; }; 4CF2A96D2F0F969300BA9FDA /* UpdateAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF2A96C2F0F969300BA9FDA /* UpdateAlert.swift */; }; 4CF2A9702F0FB2E100BA9FDA /* AppUpdateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF2A96E2F0FB2E100BA9FDA /* AppUpdateManager.swift */; }; + 4F350E459E3B47DA93E2FFF4 /* BundleSheetToggleButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = F82FD6112F9442AAAD58DB97 /* BundleSheetToggleButtons.swift */; }; + 8LLA4688ZY3G5NGU9R4ET5A6 /* BundleViewModel+Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = U828QC8U2RENQFVCV7ANT8I1 /* BundleViewModel+Types.swift */; }; + 917971D262384EC5AF4D5965 /* BundlePurchaseCartItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E036016318043AC801A19DA /* BundlePurchaseCartItem.swift */; }; + AA0000022F17000100000001 /* BundleSwitchPopup.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0000012F17000100000001 /* BundleSwitchPopup.swift */; }; AA0219DE2EB1C041006EF269 /* BundleNameInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0219DD2EB1C041006EF269 /* BundleNameInputView.swift */; }; AA0A54B72EC053E4007B5413 /* CarabinerType.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0A54B62EC053E4007B5413 /* CarabinerType.swift */; }; AA0A54B92EC05C41007B5413 /* BundleRingComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0A54B82EC05C41007B5413 /* BundleRingComponent.swift */; }; AA2146AF2F15D0160048D40E /* BundleEditView+Purchase.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA2146AE2F15D0160048D40E /* BundleEditView+Purchase.swift */; }; AA2146B12F15D43C0048D40E /* BundleEditView+Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA2146B02F15D43C0048D40E /* BundleEditView+Alert.swift */; }; - AA2146B52F15D8490048D40E /* KeyringSelectableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA2146B42F15D8490048D40E /* KeyringSelectableCell.swift */; }; + AA2146B52F15D8490048D40E /* KeyringCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA2146B42F15D8490048D40E /* KeyringCell.swift */; }; AA2146B72F15E5B60048D40E /* BundleEditView+SelectSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA2146B62F15E5B60048D40E /* BundleEditView+SelectSheet.swift */; }; AA2146B92F160E2B0048D40E /* BundleEditView+RestoreSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA2146B82F160E2B0048D40E /* BundleEditView+RestoreSelection.swift */; }; AA2146BB2F161D0C0048D40E /* BundleEditView+Initialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA2146BA2F161D0C0048D40E /* BundleEditView+Initialization.swift */; }; @@ -360,26 +365,21 @@ AA4B07322EB26CD2005F9227 /* BundleAddKeyringView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA4B07312EB26CD2005F9227 /* BundleAddKeyringView.swift */; }; AA6298522EC233D2001576C0 /* BundleEditView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA6298512EC233D2001576C0 /* BundleEditView.swift */; }; AA6298542EC39065001576C0 /* BundleCreateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA6298532EC39065001576C0 /* BundleCreateView.swift */; }; - AA6298562EC3AD16001576C0 /* BundleItemCustomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA6298552EC3AD16001576C0 /* BundleItemCustomSheet.swift */; }; - AA6298582EC457DF001576C0 /* CarabinerChangePopup.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA6298572EC457DF001576C0 /* CarabinerChangePopup.swift */; }; - AA69DD222F14C41300C0A41C /* BundleViewModel+LoadData.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA69DD212F14C41300C0A41C /* BundleViewModel+LoadData.swift */; }; + AA6298562EC3AD16001576C0 /* DraggableSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA6298552EC3AD16001576C0 /* DraggableSheet.swift */; }; + AA6298582EC457DF001576C0 /* CarabinerPopup.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA6298572EC457DF001576C0 /* CarabinerPopup.swift */; }; AA69DD242F14C56F00C0A41C /* BundleViewModel+CRUD.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA69DD232F14C56F00C0A41C /* BundleViewModel+CRUD.swift */; }; AA69DD262F14C60000C0A41C /* BundleViewModel+Edit.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA69DD252F14C60000C0A41C /* BundleViewModel+Edit.swift */; }; - AA69DD282F14C64B00C0A41C /* BundleViewModel+Sort.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA69DD272F14C64B00C0A41C /* BundleViewModel+Sort.swift */; }; - AA69DD2C2F14C80900C0A41C /* BundleViewModel+CaptureScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA69DD2B2F14C80900C0A41C /* BundleViewModel+CaptureScene.swift */; }; - AA69DD2E2F14C87900C0A41C /* BundleViewModel+Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA69DD2D2F14C87900C0A41C /* BundleViewModel+Image.swift */; }; AA8C9B8C2F0F349500A352D2 /* BundleDetailView+Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA8C9B8B2F0F349500A352D2 /* BundleDetailView+Alert.swift */; }; AA8C9B8E2F0F3E5E00A352D2 /* BundleDetailView+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA8C9B8D2F0F3E5E00A352D2 /* BundleDetailView+Menu.swift */; }; AA8C9B982F10E6D500A352D2 /* BundleViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA8C9B972F10E6D500A352D2 /* BundleViewModel.swift */; }; - AA8C9B9A2F14C35200A352D2 /* BundleViewModel+ReloadDecision.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA8C9B992F14C35200A352D2 /* BundleViewModel+ReloadDecision.swift */; }; - AA9115082EB126A60026E9BC /* CarabinerSelectableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA9115072EB126A60026E9BC /* CarabinerSelectableCell.swift */; }; - AA91150A2EB1B7930026E9BC /* CarabinerAddKeyringButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA9115092EB1B7930026E9BC /* CarabinerAddKeyringButton.swift */; }; + AA9115082EB126A60026E9BC /* CarabinerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA9115072EB126A60026E9BC /* CarabinerCell.swift */; }; + AA91150A2EB1B7930026E9BC /* AddKeyringButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA9115092EB1B7930026E9BC /* AddKeyringButton.swift */; }; AA9123AF2ED4BC490070A9F9 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA9123AE2ED4BC490070A9F9 /* LocationManager.swift */; }; AA9B2E892EB001AA0004D31C /* Background.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA9B2E882EB001AA0004D31C /* Background.swift */; }; AA9B2E8B2EB001B70004D31C /* Carabiner.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA9B2E8A2EB001B70004D31C /* Carabiner.swift */; }; AA9B2E912EB081750004D31C /* ItemDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA9B2E902EB081750004D31C /* ItemDetailView.swift */; }; - AAA4467C2EC64C9900080AB1 /* SelectBackgroundSheetContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA4467B2EC64C9900080AB1 /* SelectBackgroundSheetContent.swift */; }; - AAA446822EC6519700080AB1 /* SelectCarabinerSheetContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA446812EC6519700080AB1 /* SelectCarabinerSheetContent.swift */; }; + AAA4467C2EC64C9900080AB1 /* SelectBackgroundSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA4467B2EC64C9900080AB1 /* SelectBackgroundSheet.swift */; }; + AAA446822EC6519700080AB1 /* SelectCarabinerSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA446812EC6519700080AB1 /* SelectCarabinerSheet.swift */; }; AABA4DAC2ED2D4C700A7D062 /* cardPagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AABA4DAB2ED2D4C700A7D062 /* cardPagerView.swift */; }; AABA4DBD2ED2D6CA00A7D062 /* festivalCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = AABA4DBC2ED2D6CA00A7D062 /* festivalCard.swift */; }; AAEB46AD2EC1C893002B13E5 /* BundleMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAEB46AC2EC1C893002B13E5 /* BundleMenu.swift */; }; @@ -426,6 +426,9 @@ C6C4028F2EB27458006B58DF /* Particle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6C4028E2EB27458006B58DF /* Particle.swift */; }; C6C402A32EB40ACA006B58DF /* AlarmView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6C402A22EB40ACA006B58DF /* AlarmView.swift */; }; C6EE7AD72EB445F6002B5669 /* MyPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6EE7AD62EB445F6002B5669 /* MyPageView.swift */; }; + STYVKQEFFZGIGOFGB2L823IG /* BundleViewModel+Views.swift in Sources */ = {isa = PBXBuildFile; fileRef = OA1ZL7ZLBDTEHRTX3IOLKBSC /* BundleViewModel+Views.swift */; }; + WM2NO6DHYPIHK7087JF53TFU /* BundleViewModel+Purchase.swift in Sources */ = {isa = PBXBuildFile; fileRef = QM5GFF2WHZX9U24OOWPTMM4S /* BundleViewModel+Purchase.swift */; }; + X4JAMTO3227H7FD5X157LHSW /* BundleViewModel+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40QZ1H4Y8EH2YZZUOT7WN7MX /* BundleViewModel+Helpers.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -463,7 +466,6 @@ /* 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 = ""; }; @@ -537,6 +539,7 @@ 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 = ""; }; + 40QZ1H4Y8EH2YZZUOT7WN7MX /* BundleViewModel+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleViewModel+Helpers.swift"; sourceTree = ""; }; 4C004F902F164D5500D9063E /* TabBarManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarManager.swift; sourceTree = ""; }; 4C004F962F177C4600D9063E /* BundleVideoGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleVideoGenerator.swift; sourceTree = ""; }; 4C004F972F177C4600D9063E /* BundleVideoGenerator+Metal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleVideoGenerator+Metal.swift"; sourceTree = ""; }; @@ -761,8 +764,8 @@ 4CEC61EE2EAE08C40099ECEE /* KeychyApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychyApp.swift; sourceTree = ""; }; 4CEC61F32EAE08DA0099ECEE /* KeyringCellScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyringCellScene.swift; sourceTree = ""; }; 4CEC61F42EAE08DA0099ECEE /* KeyringCellScene+Setup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KeyringCellScene+Setup.swift"; sourceTree = ""; }; - 4CEC61F52EAE08DA0099ECEE /* KeyringBundleItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyringBundleItem.swift; sourceTree = ""; }; - 4CEC61F62EAE08DA0099ECEE /* BackgroundSelectableCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundSelectableCell.swift; sourceTree = ""; }; + 4CEC61F52EAE08DA0099ECEE /* BundleGridItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleGridItem.swift; sourceTree = ""; }; + 4CEC61F62EAE08DA0099ECEE /* BackgroundCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundCell.swift; sourceTree = ""; }; 4CEC61F92EAE08DA0099ECEE /* KeyringScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyringScene.swift; sourceTree = ""; }; 4CEC61FA2EAE08DA0099ECEE /* KeyringScene+Effects.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KeyringScene+Effects.swift"; sourceTree = ""; }; 4CEC61FB2EAE08DA0099ECEE /* KeyringScene+Setup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KeyringScene+Setup.swift"; sourceTree = ""; }; @@ -794,12 +797,15 @@ 4CF2A9692F0B94EA00BA9FDA /* View+PullToRefresh.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+PullToRefresh.swift"; sourceTree = ""; }; 4CF2A96C2F0F969300BA9FDA /* UpdateAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateAlert.swift; sourceTree = ""; }; 4CF2A96E2F0FB2E100BA9FDA /* AppUpdateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUpdateManager.swift; sourceTree = ""; }; + 5E036016318043AC801A19DA /* BundlePurchaseCartItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundlePurchaseCartItem.swift; sourceTree = ""; }; + 61SOUIL06G2NM0OODQ5MLB0A /* BundleViewModel+Fetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleViewModel+Fetch.swift"; sourceTree = ""; }; + AA0000012F17000100000001 /* BundleSwitchPopup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleSwitchPopup.swift; sourceTree = ""; }; AA0219DD2EB1C041006EF269 /* BundleNameInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleNameInputView.swift; sourceTree = ""; }; AA0A54B62EC053E4007B5413 /* CarabinerType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarabinerType.swift; sourceTree = ""; }; AA0A54B82EC05C41007B5413 /* BundleRingComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleRingComponent.swift; sourceTree = ""; }; AA2146AE2F15D0160048D40E /* BundleEditView+Purchase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleEditView+Purchase.swift"; sourceTree = ""; }; AA2146B02F15D43C0048D40E /* BundleEditView+Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleEditView+Alert.swift"; sourceTree = ""; }; - AA2146B42F15D8490048D40E /* KeyringSelectableCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyringSelectableCell.swift; sourceTree = ""; }; + AA2146B42F15D8490048D40E /* KeyringCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyringCell.swift; sourceTree = ""; }; AA2146B62F15E5B60048D40E /* BundleEditView+SelectSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleEditView+SelectSheet.swift"; sourceTree = ""; }; AA2146B82F160E2B0048D40E /* BundleEditView+RestoreSelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleEditView+RestoreSelection.swift"; sourceTree = ""; }; AA2146BA2F161D0C0048D40E /* BundleEditView+Initialization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleEditView+Initialization.swift"; sourceTree = ""; }; @@ -810,26 +816,21 @@ AA4B07312EB26CD2005F9227 /* BundleAddKeyringView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleAddKeyringView.swift; sourceTree = ""; }; AA6298512EC233D2001576C0 /* BundleEditView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleEditView.swift; sourceTree = ""; }; AA6298532EC39065001576C0 /* BundleCreateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleCreateView.swift; sourceTree = ""; }; - AA6298552EC3AD16001576C0 /* BundleItemCustomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleItemCustomSheet.swift; sourceTree = ""; }; - AA6298572EC457DF001576C0 /* CarabinerChangePopup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarabinerChangePopup.swift; sourceTree = ""; }; - AA69DD212F14C41300C0A41C /* BundleViewModel+LoadData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleViewModel+LoadData.swift"; sourceTree = ""; }; + AA6298552EC3AD16001576C0 /* DraggableSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggableSheet.swift; sourceTree = ""; }; + AA6298572EC457DF001576C0 /* CarabinerPopup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarabinerPopup.swift; sourceTree = ""; }; AA69DD232F14C56F00C0A41C /* BundleViewModel+CRUD.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleViewModel+CRUD.swift"; sourceTree = ""; }; AA69DD252F14C60000C0A41C /* BundleViewModel+Edit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleViewModel+Edit.swift"; sourceTree = ""; }; - AA69DD272F14C64B00C0A41C /* BundleViewModel+Sort.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleViewModel+Sort.swift"; sourceTree = ""; }; - AA69DD2B2F14C80900C0A41C /* BundleViewModel+CaptureScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleViewModel+CaptureScene.swift"; sourceTree = ""; }; - AA69DD2D2F14C87900C0A41C /* BundleViewModel+Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleViewModel+Image.swift"; sourceTree = ""; }; AA8C9B8B2F0F349500A352D2 /* BundleDetailView+Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleDetailView+Alert.swift"; sourceTree = ""; }; AA8C9B8D2F0F3E5E00A352D2 /* BundleDetailView+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleDetailView+Menu.swift"; sourceTree = ""; }; AA8C9B972F10E6D500A352D2 /* BundleViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleViewModel.swift; sourceTree = ""; }; - AA8C9B992F14C35200A352D2 /* BundleViewModel+ReloadDecision.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleViewModel+ReloadDecision.swift"; sourceTree = ""; }; - AA9115072EB126A60026E9BC /* CarabinerSelectableCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarabinerSelectableCell.swift; sourceTree = ""; }; - AA9115092EB1B7930026E9BC /* CarabinerAddKeyringButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarabinerAddKeyringButton.swift; sourceTree = ""; }; + AA9115072EB126A60026E9BC /* CarabinerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarabinerCell.swift; sourceTree = ""; }; + AA9115092EB1B7930026E9BC /* AddKeyringButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddKeyringButton.swift; sourceTree = ""; }; AA9123AE2ED4BC490070A9F9 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = ""; }; AA9B2E882EB001AA0004D31C /* Background.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Background.swift; sourceTree = ""; }; AA9B2E8A2EB001B70004D31C /* Carabiner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Carabiner.swift; sourceTree = ""; }; AA9B2E902EB081750004D31C /* ItemDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemDetailView.swift; sourceTree = ""; }; - AAA4467B2EC64C9900080AB1 /* SelectBackgroundSheetContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectBackgroundSheetContent.swift; sourceTree = ""; }; - AAA446812EC6519700080AB1 /* SelectCarabinerSheetContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectCarabinerSheetContent.swift; sourceTree = ""; }; + AAA4467B2EC64C9900080AB1 /* SelectBackgroundSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectBackgroundSheet.swift; sourceTree = ""; }; + AAA446812EC6519700080AB1 /* SelectCarabinerSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectCarabinerSheet.swift; sourceTree = ""; }; AABA4DAB2ED2D4C700A7D062 /* cardPagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = cardPagerView.swift; sourceTree = ""; }; AABA4DBC2ED2D6CA00A7D062 /* festivalCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = festivalCard.swift; sourceTree = ""; }; AAEB46AC2EC1C893002B13E5 /* BundleMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleMenu.swift; sourceTree = ""; }; @@ -875,6 +876,11 @@ C6C4028E2EB27458006B58DF /* Particle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Particle.swift; sourceTree = ""; }; C6C402A22EB40ACA006B58DF /* AlarmView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmView.swift; sourceTree = ""; }; C6EE7AD62EB445F6002B5669 /* MyPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageView.swift; sourceTree = ""; }; + DTQCH5OWZZJ8N03KVZERHJKR /* BundleViewModel+Cache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleViewModel+Cache.swift"; sourceTree = ""; }; + F82FD6112F9442AAAD58DB97 /* BundleSheetToggleButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleSheetToggleButtons.swift; sourceTree = ""; }; + OA1ZL7ZLBDTEHRTX3IOLKBSC /* BundleViewModel+Views.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleViewModel+Views.swift"; sourceTree = ""; }; + QM5GFF2WHZX9U24OOWPTMM4S /* BundleViewModel+Purchase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleViewModel+Purchase.swift"; sourceTree = ""; }; + U828QC8U2RENQFVCV7ANT8I1 /* BundleViewModel+Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleViewModel+Types.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -1779,20 +1785,22 @@ path = App; sourceTree = ""; }; - 4CEC61F72EAE08DA0099ECEE /* Component */ = { + 4CEC61F72EAE08DA0099ECEE /* Shared */ = { isa = PBXGroup; children = ( - 4CEC61F52EAE08DA0099ECEE /* KeyringBundleItem.swift */, - 4CEC61F62EAE08DA0099ECEE /* BackgroundSelectableCell.swift */, - AA9115072EB126A60026E9BC /* CarabinerSelectableCell.swift */, - AA2146B42F15D8490048D40E /* KeyringSelectableCell.swift */, - AAA4467B2EC64C9900080AB1 /* SelectBackgroundSheetContent.swift */, - AAA446812EC6519700080AB1 /* SelectCarabinerSheetContent.swift */, - AA9115092EB1B7930026E9BC /* CarabinerAddKeyringButton.swift */, - AA6298572EC457DF001576C0 /* CarabinerChangePopup.swift */, - AA6298552EC3AD16001576C0 /* BundleItemCustomSheet.swift */, + 4CEC61F52EAE08DA0099ECEE /* BundleGridItem.swift */, + 4CEC61F62EAE08DA0099ECEE /* BackgroundCell.swift */, + AA9115072EB126A60026E9BC /* CarabinerCell.swift */, + AA2146B42F15D8490048D40E /* KeyringCell.swift */, + AAA4467B2EC64C9900080AB1 /* SelectBackgroundSheet.swift */, + AAA446812EC6519700080AB1 /* SelectCarabinerSheet.swift */, + AA9115092EB1B7930026E9BC /* AddKeyringButton.swift */, + AA6298572EC457DF001576C0 /* CarabinerPopup.swift */, + F82FD6112F9442AAAD58DB97 /* BundleSheetToggleButtons.swift */, + 5E036016318043AC801A19DA /* BundlePurchaseCartItem.swift */, + AA6298552EC3AD16001576C0 /* DraggableSheet.swift */, ); - path = Component; + path = Shared; sourceTree = ""; }; 4CEC61F82EAE08DA0099ECEE /* View */ = { @@ -2104,6 +2112,16 @@ path = Update; sourceTree = ""; }; + A44AB20AA6F24D959D7EAC79 /* Create */ = { + isa = PBXGroup; + children = ( + AA6298532EC39065001576C0 /* BundleCreateView.swift */, + AA0219DD2EB1C041006EF269 /* BundleNameInputView.swift */, + AA4B07312EB26CD2005F9227 /* BundleAddKeyringView.swift */, + ); + path = Create; + sourceTree = ""; + }; AA8C9B932F10E67200A352D2 /* Bundle */ = { isa = PBXGroup; children = ( @@ -2117,13 +2135,14 @@ isa = PBXGroup; children = ( AA8C9B972F10E6D500A352D2 /* BundleViewModel.swift */, - AA8C9B992F14C35200A352D2 /* BundleViewModel+ReloadDecision.swift */, - AA69DD212F14C41300C0A41C /* BundleViewModel+LoadData.swift */, + U828QC8U2RENQFVCV7ANT8I1 /* BundleViewModel+Types.swift */, + 61SOUIL06G2NM0OODQ5MLB0A /* BundleViewModel+Fetch.swift */, AA69DD232F14C56F00C0A41C /* BundleViewModel+CRUD.swift */, - AA69DD2B2F14C80900C0A41C /* BundleViewModel+CaptureScene.swift */, - AA69DD272F14C64B00C0A41C /* BundleViewModel+Sort.swift */, - AA69DD2D2F14C87900C0A41C /* BundleViewModel+Image.swift */, AA69DD252F14C60000C0A41C /* BundleViewModel+Edit.swift */, + QM5GFF2WHZX9U24OOWPTMM4S /* BundleViewModel+Purchase.swift */, + 40QZ1H4Y8EH2YZZUOT7WN7MX /* BundleViewModel+Helpers.swift */, + DTQCH5OWZZJ8N03KVZERHJKR /* BundleViewModel+Cache.swift */, + OA1ZL7ZLBDTEHRTX3IOLKBSC /* BundleViewModel+Views.swift */, ); path = ViewModels; sourceTree = ""; @@ -2131,24 +2150,10 @@ AA8C9B962F10E6A500A352D2 /* Views */ = { isa = PBXGroup; children = ( - 4CEC61F72EAE08DA0099ECEE /* Component */, - 4CEC629B2EAE09990099ECEE /* BundleInventoryView.swift */, - 4CEC629A2EAE09990099ECEE /* BundleDetailView.swift */, - AA8C9B8D2F0F3E5E00A352D2 /* BundleDetailView+Menu.swift */, - AA8C9B8B2F0F349500A352D2 /* BundleDetailView+Alert.swift */, - AA3908F72EC8BF0400D87EEC /* BundleDetailView+SaveImage.swift */, - 4C004FAC2F177F2600D9063E /* BundleDetailView+VideoGen.swift */, - AA6298532EC39065001576C0 /* BundleCreateView.swift */, - AA4B07312EB26CD2005F9227 /* BundleAddKeyringView.swift */, - AA0219DD2EB1C041006EF269 /* BundleNameInputView.swift */, - AAEB46AC2EC1C893002B13E5 /* BundleMenu.swift */, - AAEB46AE2EC1D648002B13E5 /* BundleNameEditView.swift */, - AA6298512EC233D2001576C0 /* BundleEditView.swift */, - AA2146BA2F161D0C0048D40E /* BundleEditView+Initialization.swift */, - AA2146B82F160E2B0048D40E /* BundleEditView+RestoreSelection.swift */, - AA2146B02F15D43C0048D40E /* BundleEditView+Alert.swift */, - AA2146B62F15E5B60048D40E /* BundleEditView+SelectSheet.swift */, - AA2146AE2F15D0160048D40E /* BundleEditView+Purchase.swift */, + D6A1E83B3F994B7DA46CDC7B /* Detail */, + A44AB20AA6F24D959D7EAC79 /* Create */, + AD292C97DAFD4C18A064840D /* Edit */, + 4CEC61F72EAE08DA0099ECEE /* Shared */, ); path = Views; sourceTree = ""; @@ -2174,6 +2179,20 @@ path = KeyringBundle; sourceTree = ""; }; + AD292C97DAFD4C18A064840D /* Edit */ = { + isa = PBXGroup; + children = ( + AA6298512EC233D2001576C0 /* BundleEditView.swift */, + AA2146BA2F161D0C0048D40E /* BundleEditView+Initialization.swift */, + AA2146B82F160E2B0048D40E /* BundleEditView+RestoreSelection.swift */, + AA2146B02F15D43C0048D40E /* BundleEditView+Alert.swift */, + AA2146B62F15E5B60048D40E /* BundleEditView+SelectSheet.swift */, + AA2146AE2F15D0160048D40E /* BundleEditView+Purchase.swift */, + AAEB46AE2EC1D648002B13E5 /* BundleNameEditView.swift */, + ); + path = Edit; + sourceTree = ""; + }; C665DDE92EAEFAA800CE4495 /* Coin */ = { isa = PBXGroup; children = ( @@ -2254,6 +2273,20 @@ path = Showcase25Board; sourceTree = ""; }; + D6A1E83B3F994B7DA46CDC7B /* Detail */ = { + isa = PBXGroup; + children = ( + 4CEC629B2EAE09990099ECEE /* BundleInventoryView.swift */, + 4CEC629A2EAE09990099ECEE /* BundleDetailView.swift */, + AA8C9B8D2F0F3E5E00A352D2 /* BundleDetailView+Menu.swift */, + AA8C9B8B2F0F349500A352D2 /* BundleDetailView+Alert.swift */, + AA3908F72EC8BF0400D87EEC /* BundleDetailView+SaveImage.swift */, + 4C004FAC2F177F2600D9063E /* BundleDetailView+VideoGen.swift */, + AAEB46AC2EC1C893002B13E5 /* BundleMenu.swift */, + ); + path = Detail; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -2427,7 +2460,6 @@ 4CEBB14E2EFAA52F00CF53E2 /* DeepLinkManager.swift in Sources */, 4CEC61E62EAE08C00099ECEE /* Keyring.swift in Sources */, 4CEC61F12EAE08C40099ECEE /* KeychyApp.swift in Sources */, - AA69DD222F14C41300C0A41C /* BundleViewModel+LoadData.swift in Sources */, 4CEBB1482EFA93D000CF53E2 /* RootView.swift in Sources */, 4CEBB1492EFA93D000CF53E2 /* AppDelegate.swift in Sources */, 4C6622152EAE70BF001760B5 /* Gradient+Keychy.swift in Sources */, @@ -2437,14 +2469,15 @@ 4CEBB16F2EFCFC5600CF53E2 /* NoInternetView.swift in Sources */, 4C004FAB2F177C7200D9063E /* KeyringVideoGenerator+Metal.swift in Sources */, 4CEBB1702EFCFC5600CF53E2 /* NoInternetToast.swift in Sources */, - AA8C9B9A2F14C35200A352D2 /* BundleViewModel+ReloadDecision.swift in Sources */, 4CEC61E82EAE08C00099ECEE /* ChainType.swift in Sources */, 4C77753E2EB1343600981C3E /* IntroViewModel.swift in Sources */, 4C77753F2EB1343600981C3E /* IntroViewModel+Login.swift in Sources */, 4C7775402EB1343600981C3E /* IntroViewModel+Signup.swift in Sources */, - AAA446822EC6519700080AB1 /* SelectCarabinerSheetContent.swift in Sources */, + AAA446822EC6519700080AB1 /* SelectCarabinerSheet.swift in Sources */, 4C7775412EB1343600981C3E /* IntroViewModel+NicknameSetup.swift in Sources */, - AA6298562EC3AD16001576C0 /* BundleItemCustomSheet.swift in Sources */, + 4F350E459E3B47DA93E2FFF4 /* BundleSheetToggleButtons.swift in Sources */, + 917971D262384EC5AF4D5965 /* BundlePurchaseCartItem.swift in Sources */, + AA6298562EC3AD16001576C0 /* DraggableSheet.swift in Sources */, 4CEC61E92EAE08C00099ECEE /* KeyringTemplate.swift in Sources */, 4CA9C6FC2ECBC20100CA546B /* NotificationGiftView.swift in Sources */, 4CEC61EA2EAE08C00099ECEE /* RingType.swift in Sources */, @@ -2461,7 +2494,7 @@ 4CEC621A2EAE08DA0099ECEE /* KeyringScene.swift in Sources */, 4CEC621B2EAE08DA0099ECEE /* KeyringScene+Touch.swift in Sources */, 38C3C2882EC1D761003C5DE1 /* CollectionKeyringDetailView+Menu.swift in Sources */, - 4CEC621C2EAE08DA0099ECEE /* BackgroundSelectableCell.swift in Sources */, + 4CEC621C2EAE08DA0099ECEE /* BackgroundCell.swift in Sources */, 3828F5472EC4CCDC00F1B040 /* CollectionView+NormalMode.swift in Sources */, 4CEBB1612EFAD3F800CF53E2 /* MainTabView.swift in Sources */, 386102482F1104EB0045C529 /* KeyringCollectView+Alerts.swift in Sources */, @@ -2497,8 +2530,8 @@ AA2146BB2F161D0C0048D40E /* BundleEditView+Initialization.swift in Sources */, 3828F5492EC4CCE400F1B040 /* CollectionView+SearchMode.swift in Sources */, AA9B2E8B2EB001B70004D31C /* Carabiner.swift in Sources */, - AA6298582EC457DF001576C0 /* CarabinerChangePopup.swift in Sources */, - AAA4467C2EC64C9900080AB1 /* SelectBackgroundSheetContent.swift in Sources */, + AA6298582EC457DF001576C0 /* CarabinerPopup.swift in Sources */, + AAA4467C2EC64C9900080AB1 /* SelectBackgroundSheet.swift in Sources */, 38C3C2842EC0D081003C5DE1 /* LinkCopiedPopup.swift in Sources */, C6C35F3E2ED2AB71009642F4 /* ZoomableScrollView.swift in Sources */, AA69DD262F14C60000C0A41C /* BundleViewModel+Edit.swift in Sources */, @@ -2528,18 +2561,16 @@ 4CEBB15F2EFAD3D600CF53E2 /* MainTabViewModel.swift in Sources */, 4C65303B2EBA5FA0000F8154 /* CheckmarkAlert.swift in Sources */, 4C4733F12F22563D005D2376 /* WorkshopView+TopSection.swift in Sources */, - AA69DD2C2F14C80900C0A41C /* BundleViewModel+CaptureScene.swift in Sources */, 4C004FB12F187CFC00D9063E /* CollectionKeyringDetailView+VideoGen.swift in Sources */, 4C004FBB2F19F1FE00D9063E /* RootViewModel+ReviewCheck.swift in Sources */, C6B56F112EBD96110049F969 /* CoinChargeView+Purchase.swift in Sources */, 382800D12EC05D4E005F1332 /* PostOffice.swift in Sources */, - AA69DD2E2F14C87900C0A41C /* BundleViewModel+Image.swift in Sources */, C6C361E62ED4B289009642F4 /* ShowcaseConfirmPopup.swift in Sources */, AA9123AF2ED4BC490070A9F9 /* LocationManager.swift in Sources */, 4CEC626C2EAE08DF0099ECEE /* FestivalView.swift in Sources */, 4C7775382EB0EFD800981C3E /* ItemDetailMakingBtn.swift in Sources */, C6830F042EB8A4000059379A /* WorkshopDataManager.swift in Sources */, - AA2146B52F15D8490048D40E /* KeyringSelectableCell.swift in Sources */, + AA2146B52F15D8490048D40E /* KeyringCell.swift in Sources */, C6EE7AD72EB445F6002B5669 /* MyPageView.swift in Sources */, AA8C9B8E2F0F3E5E00A352D2 /* BundleDetailView+Menu.swift in Sources */, 389080632ED47F2500D7A49F /* VotePopup.swift in Sources */, @@ -2674,7 +2705,6 @@ 4CF2A9702F0FB2E100BA9FDA /* AppUpdateManager.swift in Sources */, AA9B2E912EB081750004D31C /* ItemDetailView.swift in Sources */, C665DDE82EAEFA8700CE4495 /* CoinChargeView.swift in Sources */, - AA69DD282F14C64B00C0A41C /* BundleViewModel+Sort.swift in Sources */, AAEB46AD2EC1C893002B13E5 /* BundleMenu.swift in Sources */, 4CF2A96D2F0F969300BA9FDA /* UpdateAlert.swift in Sources */, 4C7775322EB0EEF100981C3E /* ItemDetailImage.swift in Sources */, @@ -2705,7 +2735,7 @@ 38A22A9D2EC27AC400B4C7C5 /* PackagedKeyringView+SaveImage.swift in Sources */, 3861024A2F1129FA0045C529 /* KeyringCacheManager.swift in Sources */, AABA4DAC2ED2D4C700A7D062 /* cardPagerView.swift in Sources */, - AA9115082EB126A60026E9BC /* CarabinerSelectableCell.swift in Sources */, + AA9115082EB126A60026E9BC /* CarabinerCell.swift in Sources */, 38283A832EBF554E00BE45A5 /* KeyringReceiveView.swift in Sources */, 4CEC627F2EAE08DF0099ECEE /* CollectionView.swift in Sources */, AA0A54B92EC05C41007B5413 /* BundleRingComponent.swift in Sources */, @@ -2715,7 +2745,7 @@ C6C361E42ED4AF48009642F4 /* Showcase25BoardView+Grid.swift in Sources */, 386102442F0F7C980045C529 /* KeyringCollectViewModel.swift in Sources */, 4C65306E2EBCF157000F8154 /* TermsView.swift in Sources */, - AA91150A2EB1B7930026E9BC /* CarabinerAddKeyringButton.swift in Sources */, + AA91150A2EB1B7930026E9BC /* AddKeyringButton.swift in Sources */, 4CEC622C2EAE08DA0099ECEE /* KeyringSceneView.swift in Sources */, C645AEA32EB1B8FC004BFE69 /* DataInitializer.swift in Sources */, 4CA9C6F72ECBA45200CA546B /* KeychyNotification.swift in Sources */, @@ -2745,8 +2775,14 @@ 4CEC62312EAE08DA0099ECEE /* KeyringScene+Swipe.swift in Sources */, 4C004F912F164D5500D9063E /* TabBarManager.swift in Sources */, C665DDF02EAF08D000CE4495 /* PurchaseManager.swift in Sources */, - 4CEC62332EAE08DA0099ECEE /* KeyringBundleItem.swift in Sources */, + 4CEC62332EAE08DA0099ECEE /* BundleGridItem.swift in Sources */, AA69DD242F14C56F00C0A41C /* BundleViewModel+CRUD.swift in Sources */, + 8LLA4688ZY3G5NGU9R4ET5A6 /* BundleViewModel+Types.swift in Sources */, + 324NSC72E34Q6L9EW2AV1KL9 /* BundleViewModel+Fetch.swift in Sources */, + WM2NO6DHYPIHK7087JF53TFU /* BundleViewModel+Purchase.swift in Sources */, + X4JAMTO3227H7FD5X157LHSW /* BundleViewModel+Helpers.swift in Sources */, + STYVKQEFFZGIGOFGB2L823IG /* BundleViewModel+Views.swift in Sources */, + 40WF8CXMLHGD9B5S521VX89Y /* BundleViewModel+Cache.swift in Sources */, AA3908F82EC8BF0400D87EEC /* BundleDetailView+SaveImage.swift in Sources */, 4C4733EB2F22553F005D2376 /* WorkshopView+StickyHeader.swift in Sources */, 4C4733F72F225A2C005D2376 /* WorkshopItemDetailView.swift in Sources */, diff --git a/Keychy/Keychy/Core/Components/View/Popup/BundleSwitchPopup.swift b/Keychy/Keychy/Core/Components/View/Popup/BundleSwitchPopup.swift index c7f1fec40..5a1950af1 100644 --- a/Keychy/Keychy/Core/Components/View/Popup/BundleSwitchPopup.swift +++ b/Keychy/Keychy/Core/Components/View/Popup/BundleSwitchPopup.swift @@ -2,7 +2,7 @@ // BundleSwitchPopup.swift // Keychy // -// Created by Claude on 2/3/26. +// Created by 길지훈 on 2/3/26. // import SwiftUI diff --git a/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+CRUD.swift b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+CRUD.swift index a2ae6de7b..da425d1e3 100644 --- a/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+CRUD.swift +++ b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+CRUD.swift @@ -5,6 +5,15 @@ // Created by 김서현 on 1/12/26. // +// MARK: - BundleViewModel+CRUD +// +// Firebase 데이터 쓰기 +// - createBundle: 뭉치 생성 +// - createKeyringDataList: 키링 → Scene 데이터 변환 +// - updateBundleMainStatus: 대표 뭉치 설정 +// - updateBundleName: 이름 변경 +// - incrementUseCount: 사용 횟수 증가 + import FirebaseFirestore extension BundleViewModel { @@ -240,4 +249,40 @@ extension BundleViewModel { } } } + + // MARK: - 사용 횟수 증가 + + /// 배경, 카라비너의 사용 횟수를 증가시키는 메서드 + func incrementUseCount( + carabinerId: String?, + backgroundId: String? + ) { + if let carabinerId = carabinerId, !carabinerId.isEmpty { + db.collection("Carabiner") + .document(carabinerId) + .updateData([ + "useCount": FieldValue.increment(Int64(1)) + ]) { error in + if let error = error { + print("[useCount] Carabiner 증가 실패: \(error)") + } else { + print("[useCount] Carabiner 증가 성공: \(carabinerId)") + } + } + } + + if let backgroundId = backgroundId, !backgroundId.isEmpty { + db.collection("Background") + .document(backgroundId) + .updateData([ + "useCount": FieldValue.increment(Int64(1)) + ]) { error in + if let error = error { + print("[useCount] Background 증가 실패: \(error)") + } else { + print("[useCount] Background 증가 성공: \(backgroundId)") + } + } + } + } } diff --git a/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Cache.swift b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Cache.swift new file mode 100644 index 000000000..d2863822d --- /dev/null +++ b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Cache.swift @@ -0,0 +1,95 @@ +// +// BundleViewModel+Cache.swift +// Keychy +// +// Created by 길지훈 on 2/5/26. +// + +// MARK: - BundleViewModel+Cache +// +// 캐시 및 씬 리로드 최적화 +// - loadBundleImageFromCache: 캐시에서 이미지 로드 +// - saveBundleImageToCache: 이미지 캐시 저장 +// - shouldSkipReloadForReturnedConfig: 리로드 스킵 판단 +// - updateLastConfigIds: 구성 ID 업데이트 + +import Foundation + +extension BundleViewModel { + + // MARK: - 이미지 캐시 + + /// 캐시에서 번들 이미지를 로드하여 bundleCapturedImage에 설정 + @discardableResult + func loadBundleImageFromCache(bundle: KeyringBundle) -> Bool { + guard let documentId = bundle.documentId else { + print("[BundleViewModel] 번들 documentId가 없습니다.") + return false + } + + if let imageData = BundleImageCache.shared.load(for: documentId) { + self.bundleCapturedImage = imageData + print("[BundleViewModel] 캐시에서 번들 이미지 로드 성공: \(documentId)") + return true + } else { + print("[BundleViewModel] 캐시에 번들 이미지가 없습니다: \(documentId)") + return false + } + } + + /// 뷰모델에 저장된 뭉치 이미지를 BundleImageCache에 저장 + func saveBundleImageToCache( + bundleId: String, + bundleName: String, + widgetImageData: Data? = nil, + createdAt: Date = Date() + ) { + guard let imageData = bundleCapturedImage else { + return + } + BundleImageCache.shared.syncBundle( + id: bundleId, + name: bundleName, + fullImageData: imageData, + widgetImageData: widgetImageData, + createdAt: createdAt + ) + } + + // MARK: - 씬 리로드 최적화 + + /// 이전 화면에서 전달된 구성과 동일하면 씬 리로드 스킵 + /// + /// - Returns: true면 동일 구성 → 리로드 스킵, false면 정상 로드 + /// + /// 편집 화면에서 돌아올 때 변경사항이 없으면 씬을 다시 그리지 않음 + func shouldSkipReloadForReturnedConfig() -> Bool { + guard let returnBGId = returnBackgroundId, + let returnCBId = returnCarabinerId, + let returnKRId = returnKeyringsId else { + return false + } + + let same = (returnBGId == lastBackgroundIdForDetail) && + (returnCBId == lastCarabinerIdForDetail) && + (returnKRId == lastKeyringsIdForDetail) + + if same { + returnBackgroundId = nil + returnCarabinerId = nil + returnKeyringsId = nil + } + return same + } + + /// BundleDetailView가 뭉치 로드를 마친 후 현재 구성 ID 저장 + func updateLastConfigIds( + background: Background?, + carabiner: Carabiner?, + keyringDataList: [MultiKeyringScene.KeyringData] + ) { + lastBackgroundIdForDetail = makeBackgroundId(background) + lastCarabinerIdForDetail = makeCarabinerId(carabiner) + lastKeyringsIdForDetail = makeKeyringsId(keyringDataList) + } +} diff --git a/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+CaptureScene.swift b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+CaptureScene.swift deleted file mode 100644 index ec6b3c9dd..000000000 --- a/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+CaptureScene.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// BundleViewModel+CaptureScene.swift -// Keychy -// -// Created by 김서현 on 1/12/26. -// - -import SwiftUI - -extension BundleViewModel { - /// 캡쳐한 씬을 보여주는 메서드 - /// - // BundleNameInputView, BundleNameEditView에서 사용하는 미리보기 씬 - @ViewBuilder - func bundleCaptureSceneView() -> some View { - let widthSize = screenWidth - 176 - let heightSize = widthSize * 7/5 - - Group { - if let imageData = bundleCapturedImage, - let uiImage = UIImage(data: imageData) { - // 캡처된 이미지 표시 - Image(uiImage: uiImage) - .resizable() - .scaledToFill() - .offset(y: 30) - .clipped() - } else { - // 이미지가 없으면 기본 메시지 표시 - VStack { - Image(systemName: "photo") - .font(.system(size: 50)) - .foregroundColor(.gray) - Text("이미지를 불러오는 중...") - .font(.caption) - .foregroundColor(.gray) - } - } - } - .frame(width: widthSize, height: heightSize) - .clipShape(RoundedRectangle(cornerRadius: 15)) - .clipped() - } -} diff --git a/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Edit.swift b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Edit.swift index a2bfa80ad..96d3d4427 100644 --- a/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Edit.swift +++ b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Edit.swift @@ -5,10 +5,23 @@ // Created by 김서현 on 1/12/26. // +// MARK: - BundleViewModel+Edit +// +// 뭉치 편집 로직 +// - createKeyringDataListFromSelected: 키링 → Scene 데이터 +// - convertBundleToSelectedKeyrings: 뭉치 → 편집용 변환 +// - convertSelectedKeyringsToBundleFormat: 편집용 → 뭉치 변환 +// - refreshEditData: 편집 화면 새로고침 +// - saveBundleChanges: Firebase 저장 +// - sortedKeyringsForSelection: 키링 정렬 + import SwiftUI import FirebaseFirestore extension BundleViewModel { + + // MARK: - 키링 데이터 변환 + /// 선택된 키링들로부터 키링 데이터 리스트 생성 (편집용) func createKeyringDataListFromSelected( selectedKeyrings: [Int: Keyring], @@ -16,23 +29,19 @@ extension BundleViewModel { carabiner: Carabiner ) -> [MultiKeyringScene.KeyringData] { var dataList: [MultiKeyringScene.KeyringData] = [] - - // 추가된 순서대로 처리 + for index in keyringOrder { guard let keyring = selectedKeyrings[index] else { continue } let soundId = keyring.soundId - - // 커스텀 사운드 URL 처리 + let customSoundURL: URL? = { if soundId.hasPrefix("https://") || soundId.hasPrefix("http://") { return URL(string: soundId) } return nil }() - + let particleId = keyring.particleId - - // 절대 좌표 사용 (이미 절대 좌표로 저장됨) let position = CGPoint( x: carabiner.keyringXPosition[index], y: carabiner.keyringYPosition[index] @@ -59,57 +68,53 @@ extension BundleViewModel { func convertBundleToSelectedKeyrings(bundle: KeyringBundle) async -> ([Int: Keyring], [Int]) { var selectedKeyrings: [Int: Keyring] = [:] var keyringOrder: [Int] = [] - + for (index, keyringId) in bundle.keyrings.enumerated() { guard keyringId != "none", !keyringId.isEmpty else { continue } - - // 사용자의 키링 목록에서 해당 키링 찾기 (documentId로 비교) + if let keyring = self.keyring.first(where: { $0.documentId == keyringId }) { selectedKeyrings[index] = keyring keyringOrder.append(index) } } - + return (selectedKeyrings, keyringOrder) } - + /// selectedKeyrings를 뭉치 형태의 키링 배열로 변환 func convertSelectedKeyringsToBundleFormat( selectedKeyrings: [Int: Keyring], maxKeyringCount: Int ) -> [String] { var keyrings = Array(repeating: "none", count: maxKeyringCount) - + for (index, keyring) in selectedKeyrings { if index < maxKeyringCount { keyrings[index] = keyring.documentId ?? "none" } } - + return keyrings } - - // 뭉치 편집뷰에서 화면이 다시 나타날 때 데이터 새로고침 (구매 상태 업데이트) 메서드 + + // MARK: - 데이터 새로고침 + + /// 편집 화면 데이터 새로고침 (구매 상태 업데이트) func refreshEditData() async { - // 현재 선택된 아이템의 ID 저장 let currentBackgroundId = newSelectedBackground?.background.id let currentCarabinerId = newSelectedCarabiner?.carabiner.id - - // 배경 데이터 새로고침 + await withCheckedContinuation { continuation in fetchAllBackgrounds { _ in - // 이전에 선택했던 배경을 다시 찾아서 선택 (구매 상태가 업데이트됨) if let bgId = currentBackgroundId { self.newSelectedBackground = self.backgroundViewData.first { $0.background.id == bgId } } continuation.resume() } } - - // 카라비너 데이터 새로고침 + await withCheckedContinuation { continuation in fetchAllCarabiners { _ in - // 이전에 선택했던 카라비너를 다시 찾아서 선택 (구매 상태가 업데이트됨) if let cbId = currentCarabinerId { self.newSelectedCarabiner = self.carabinerViewData.first { $0.carabiner.id == cbId } } @@ -117,9 +122,10 @@ extension BundleViewModel { } } } - - // 뭉치 변경사항을 Firebase에 저장 - + + // MARK: - Firebase 저장 + + /// 뭉치 변경사항을 Firebase에 저장 func saveBundleChanges() async { guard let bundle = selectedBundle, let documentId = bundle.documentId, @@ -127,30 +133,26 @@ extension BundleViewModel { let carabiner = newSelectedCarabiner else { return } - - // ID 안전성 체크 + guard let backgroundId = background.background.id, let carabinerId = carabiner.carabiner.id else { return } - - // 변경사항 체크: 원본 번들과 현재 선택된 항목 비교 + let isBackgroundChanged = bundle.selectedBackground != backgroundId let isCarabinerChanged = bundle.selectedCarabiner != carabinerId - - // 키링 변경사항 체크 + let currentKeyrings = convertSelectedKeyringsToBundleFormat( selectedKeyrings: selectedKeyrings, maxKeyringCount: carabiner.carabiner.maxKeyringCount ).map { $0.isEmpty ? "none" : $0 } - + let isKeyringsChanged = bundle.keyrings != currentKeyrings - - // 변경사항이 전혀 없으면 저장하지 않고 즉시 리턴 + if !isBackgroundChanged && !isCarabinerChanged && !isKeyringsChanged { return } - + do { let db = FirebaseFirestore.Firestore.firestore() let updateData: [String: Any] = [ @@ -159,26 +161,23 @@ extension BundleViewModel { "selectedCarabiner": carabinerId ] try await db.collection("KeyringBundle").document(documentId).updateData(updateData) - - // 로컬 상태도 업데이트 + await MainActor.run { if let index = bundles.firstIndex(where: { $0.documentId == documentId }) { bundles[index].keyrings = currentKeyrings bundles[index].selectedBackground = backgroundId bundles[index].selectedCarabiner = carabinerId } - - // selectedBundle도 업데이트 + if selectedBundle?.documentId == documentId { selectedBundle?.keyrings = currentKeyrings selectedBundle?.selectedBackground = backgroundId selectedBundle?.selectedCarabiner = carabinerId } - - // 캐시 삭제, BundleInventoryView로 접근했을 때 썸네일 업데이트 하도록 함 + BundleImageCache.shared.delete(for: documentId) } - + } catch { print("❌ Firebase 업데이트 실패: \(error.localizedDescription)") if let firestoreError = error as NSError? { @@ -188,4 +187,51 @@ extension BundleViewModel { } } } + + // MARK: - 키링 정렬 (선택 시트용) + + /// 키링 선택 시트용 정렬된 키링 리스트 + /// - 1순위: 현재 위치에 선택된 키링 + /// - 2순위: 일반 키링들 (선택되지 않고, published/packaged 아님) + /// - 3순위: 다른 위치에 장착된 키링들 + /// - 4순위: published 또는 packaged 상태의 키링들 (맨 뒤) + func sortedKeyringsForSelection(selectedKeyrings: [Int: Keyring], selectedPosition: Int) -> [Keyring] { + let selectedKeyring = selectedKeyrings[selectedPosition] + + return keyring.sorted { keyring1, keyring2 in + let isKeyring1SelectedHere = keyring1.id == selectedKeyring?.id + let isKeyring2SelectedHere = keyring2.id == selectedKeyring?.id + + let isKeyring1SelectedElsewhere = selectedKeyrings.values.contains { $0.id == keyring1.id } && !isKeyring1SelectedHere + let isKeyring2SelectedElsewhere = selectedKeyrings.values.contains { $0.id == keyring2.id } && !isKeyring2SelectedHere + + let isKeyring1Unavailable = keyring1.status == .published || keyring1.status == .packaged + let isKeyring2Unavailable = keyring2.status == .published || keyring2.status == .packaged + + if isKeyring1SelectedHere != isKeyring2SelectedHere { + return isKeyring1SelectedHere + } + + let isKeyring1Normal = !isKeyring1SelectedElsewhere && !isKeyring1Unavailable + let isKeyring2Normal = !isKeyring2SelectedElsewhere && !isKeyring2Unavailable + + if isKeyring1Normal != isKeyring2Normal { + return isKeyring1Normal + } + + if isKeyring1SelectedElsewhere != isKeyring2SelectedElsewhere { + return isKeyring1SelectedElsewhere + } + + if isKeyring1Unavailable != isKeyring2Unavailable { + return isKeyring2Unavailable + } + + guard let index1 = keyring.firstIndex(of: keyring1), + let index2 = keyring.firstIndex(of: keyring2) else { + return false + } + return index1 < index2 + } + } } diff --git a/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+LoadData.swift b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Fetch.swift similarity index 77% rename from Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+LoadData.swift rename to Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Fetch.swift index de4ddd740..717292457 100644 --- a/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+LoadData.swift +++ b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Fetch.swift @@ -1,17 +1,27 @@ // -// BundleViewModel+LoadData.swift +// BundleViewModel+Fetch.swift // Keychy // // Created by 김서현 on 1/12/26. // +// MARK: - BundleViewModel+Fetch +// +// Firebase 데이터 읽기 +// - fetchAllBundles: 사용자 뭉치 로드 +// - fetchAllBackgrounds: 배경 로드 +// - fetchAllCarabiners: 카라비너 로드 +// - fetchKeyringInfo: 키링 정보 로드 + import FirebaseFirestore extension BundleViewModel { - //MARK: - Firebase에서 사용자의 모든 뭉치 로드 + + // MARK: - 사용자의 모든 뭉치 로드 + func fetchAllBundles(uid: String, completion: @escaping (Bool) -> Void) { isLoading = true - + db.collection("KeyringBundle") .whereField("userId", isEqualTo: uid) .getDocuments { [weak self] snapshot, error in @@ -19,46 +29,44 @@ extension BundleViewModel { completion(false) return } - + defer { self.isLoading = false } - + if let error = error { print("뭉치 로드 에러: \(error.localizedDescription)") completion(false) return } - + guard let documents = snapshot?.documents else { print("뭉치 문서가 없습니다.") self.bundles = [] completion(true) return } - + let loadedBundles: [KeyringBundle] = documents.compactMap { doc in KeyringBundle(documentId: doc.documentID, data: doc.data()) } - - // 뷰모델 번들에 저장 (정렬은 sortedBundles에서 처리) + self.bundles = loadedBundles completion(true) } } - - // MARK: - 전체 배경 로드 + 소유 여부 주석 (dataManager 활용) + + // MARK: - 전체 배경 로드 + 소유 여부 표시 + func fetchAllBackgrounds(completion: @escaping (Bool) -> Void) { isLoading = true Task { - // dataManager를 통해 캐싱된 데이터 활용 await dataManager.fetchBackgroundsIfNeeded() - - // dataManager에서 이미 로드된 데이터 가져오기 - let items = backgrounds // dataManager.backgrounds + + let items = backgrounds let ownedIds = UserManager.shared.currentUser?.backgrounds ?? [] let decorated = items.map { bg in BackgroundViewData(background: bg, isOwned: ownedIds.contains(bg.id ?? "")) } - + await MainActor.run { self.backgroundViewData = decorated self.isLoading = false @@ -66,32 +74,31 @@ extension BundleViewModel { } } } - - // MARK: - 전체 카라비너 로드 + 소유 여부 주석 (dataManager 활용) + + // MARK: - 전체 카라비너 로드 + 소유 여부 표시 + func fetchAllCarabiners(completion: @escaping (Bool) -> Void) { isLoading = true Task { - // dataManager를 통해 캐싱된 데이터 활용 await dataManager.fetchCarabinersIfNeeded() - - // dataManager에서 이미 로드된 데이터 가져오기 - let items = carabiners // dataManager.carabiners + + let items = carabiners let ownedIds = UserManager.shared.currentUser?.carabiners ?? [] let decorated = items.map { cb in CarabinerViewData(carabiner: cb, isOwned: ownedIds.contains(cb.id ?? "")) } - + await MainActor.run { self.carabinerViewData = decorated self.isLoading = false completion(true) } } - // 카라비너는 기본 카라비너 자동 선택 됨 selectedCarabiner = carabiners.first } - - /// Firestore에서 키링 정보를 가져옴 + + // MARK: - 단일 키링 정보 로드 + func fetchKeyringInfo(keyringId: String) async -> KeyringInfo? { do { let document = try await db.collection("Keyring").document(keyringId).getDocument() diff --git a/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Helpers.swift b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Helpers.swift new file mode 100644 index 000000000..5f69cdcf2 --- /dev/null +++ b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Helpers.swift @@ -0,0 +1,111 @@ +// +// BundleViewModel+Helpers.swift +// Keychy +// +// Created by 길지훈 on 2/5/26. +// + +// MARK: - BundleViewModel+Helpers +// +// 유틸리티 메서드 +// - resolveBackground/Carabiner: ID → 모델 변환 +// - makeBackgroundId/CarabinerId/KeyringsId: 구성 ID 생성 +// - addBackgroundToUser/addCarabinerToUser: 사용자 아이템 추가 + +import FirebaseFirestore + +extension BundleViewModel { + + // MARK: - ID → Model 변환 + + func resolveBackground(from id: String) -> Background? { + backgrounds.first { $0.id == id } + } + + func resolveCarabiner(from id: String) -> Carabiner? { + carabiners.first { $0.id == id } + } + + // MARK: - 구성 ID 생성 (씬 리로드 판단용) + + func makeBackgroundId(_ bg: Background?) -> String { + guard let bg else { return "" } + return bg.id ?? "" + } + + func makeCarabinerId(_ cb: Carabiner?) -> String { + guard let cb else { return "" } + return "\(cb.id ?? "")|\(cb.carabinerX)|\(cb.carabinerY)|\(cb.carabinerWidth)" + } + + func makeKeyringsId(_ list: [MultiKeyringScene.KeyringData]) -> String { + list + .sorted(by: { $0.index < $1.index }) + .map { item in + "\(item.index)|\(item.bodyImageURL)|\((item.templateId ?? ""))|\(item.soundId)|\(item.particleId)|\((item.hookOffsetY ?? 0))|\(item.chainLength)" + } + .joined(separator: ";") + } + + // MARK: - 사용자 아이템 추가 + + /// User의 backgrounds 배열에 새 배경 추가 + func addBackgroundToUser(backgroundName: String, userManager: UserManager) async -> Bool { + guard let userId = userManager.currentUser?.id else { + print("사용자 ID를 가져올 수 없습니다") + return false + } + + let db = FirebaseFirestore.Firestore.firestore() + let userRef = db.collection("User").document(userId) + + do { + try await userRef.updateData([ + "backgrounds": FirebaseFirestore.FieldValue.arrayUnion([backgroundName]) + ]) + + await withCheckedContinuation { (continuation: CheckedContinuation) in + userManager.loadUserInfo(uid: userId) { _ in + continuation.resume() + } + } + + print("User backgrounds 업데이트 완료: \(backgroundName)") + return true + + } catch { + print("User backgrounds 업데이트 에러: \(error.localizedDescription)") + return false + } + } + + /// User의 카라비너에 새 카라비너 추가 + func addCarabinerToUser(carabinerName: String, userManager: UserManager) async -> Bool { + guard let userId = userManager.currentUser?.id else { + print("사용자 ID를 가져올 수 없습니다") + return false + } + + let db = FirebaseFirestore.Firestore.firestore() + let userRef = db.collection("User").document(userId) + + do { + try await userRef.updateData([ + "carabiners": FirebaseFirestore.FieldValue.arrayUnion([carabinerName]) + ]) + + await withCheckedContinuation { (continuation: CheckedContinuation) in + userManager.loadUserInfo(uid: userId) { _ in + continuation.resume() + } + } + + print("User carabiners 업데이트 완료: \(carabinerName)") + return true + + } catch { + print("User carabiners 업데이트 에러: \(error.localizedDescription)") + return false + } + } +} diff --git a/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Purchase.swift b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Purchase.swift new file mode 100644 index 000000000..e85dfd5ab --- /dev/null +++ b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Purchase.swift @@ -0,0 +1,85 @@ +// +// BundleViewModel+Purchase.swift +// Keychy +// +// Created by 길지훈 on 2/5/26. +// + +// MARK: - BundleViewModel+Purchase +// +// 구매 관련 로직 +// - payableItemsCount: 구매 가능 아이템 수 +// - totalCartPrice: 총 가격 +// - hasUnpurchasedItems: 미구매 아이템 존재 여부 +// - purchaseSelectedItems: 구매 처리 + +import Foundation + +extension BundleViewModel { + + // MARK: - Computed Properties + + /// 구매 가능한 아이템 수 (미소유 + 유료) + var payableItemsCount: Int { + let backgroundCount = (newSelectedBackground != nil && !newSelectedBackground!.isOwned && newSelectedBackground!.background.price > 0) ? 1 : 0 + let carabinerCount = (newSelectedCarabiner != nil && !newSelectedCarabiner!.isOwned && newSelectedCarabiner!.carabiner.price > 0) ? 1 : 0 + return backgroundCount + carabinerCount + } + + /// 총 구매 가격 + var totalCartPrice: Int { + let backgroundPrice = (newSelectedBackground != nil && !newSelectedBackground!.isOwned && newSelectedBackground!.background.price > 0) ? newSelectedBackground!.background.price : 0 + let carabinerPrice = (newSelectedCarabiner != nil && !newSelectedCarabiner!.isOwned && newSelectedCarabiner!.carabiner.price > 0) ? newSelectedCarabiner!.carabiner.price : 0 + return backgroundPrice + carabinerPrice + } + + /// 구매하지 않은 유료 아이템이 있는지 확인 + var hasUnpurchasedItems: Bool { + let hasUnpurchasedBackground = newSelectedBackground != nil && !newSelectedBackground!.isOwned && newSelectedBackground!.background.price > 0 + let hasUnpurchasedCarabiner = newSelectedCarabiner != nil && !newSelectedCarabiner!.isOwned && newSelectedCarabiner!.carabiner.price > 0 + return hasUnpurchasedBackground || hasUnpurchasedCarabiner + } + + // MARK: - 구매 처리 + + /// 선택된 아이템들 구매 처리 + @MainActor + func purchaseSelectedItems() async -> PurchaseResult { + isPurchasing = true + + // 선택된 배경이 유료인 경우 구매 + if let bg = newSelectedBackground, !bg.isOwned && bg.background.price > 0 { + let result = await ItemPurchaseManager.shared.purchaseWorkshopItem(bg.background, userManager: UserManager.shared) + + switch result { + case .success: + break + case .insufficientCoins: + isPurchasing = false + return .insufficientCoins + case .failed(let message): + isPurchasing = false + return .failed(message) + } + } + + // 선택된 카라비너가 유료인 경우 구매 + if let cb = newSelectedCarabiner, !cb.isOwned && cb.carabiner.price > 0 { + let result = await ItemPurchaseManager.shared.purchaseWorkshopItem(cb.carabiner, userManager: UserManager.shared) + + switch result { + case .success: + break + case .insufficientCoins: + isPurchasing = false + return .insufficientCoins + case .failed(let message): + isPurchasing = false + return .failed(message) + } + } + + isPurchasing = false + return .success + } +} diff --git a/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+ReloadDecision.swift b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+ReloadDecision.swift deleted file mode 100644 index 3bf736d45..000000000 --- a/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+ReloadDecision.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// BundleViewModel+ReloadDecision.swift -// Keychy -// -// Created by 김서현 on 1/12/26. -// - - - -extension BundleViewModel { - // MARK: - 씬 리로드 스킵 판단/최신 구성 저장 로직 - /// 이전 화면에서 전달된 구성 id(return~)가 있고, Detail이 마지막으로 로드한 구성(last~)과 모두 동일하면 true - /// - true: 동일 구성 -> Detail은 씬 재구성/false 스킵 (View에서 필요 시 isSceneReady를 즉시 true로 복구) - /// - false: 동일하지 않음 -> 정상 로드 진행 - /// 호출 후 return~Id는 비워짐 - /// return ... Id : 이전 뭉치 상세 화면에서 전달 받은 구성(배경, 카라비너, 키링)들의 id - func shouldSkipReloadForReturnedConfig() -> Bool { - guard let returnBGId = returnBackgroundId, - let returnCBId = returnCarabinerId, - let returnKRId = returnKeyringsId else { - return false - } - - // same: 배경, 카라비너, 키링의 id가 변경 된 것이 없으면 true, 변경된 것이 있으면 false를 반환 - let same = (returnBGId == lastBackgroundIdForDetail) && - (returnCBId == lastCarabinerIdForDetail) && - (returnKRId == lastKeyringsIdForDetail) - // 한 번 사용 후 비움 - if same { - returnBackgroundId = nil - returnCarabinerId = nil - returnKeyringsId = nil - } - return same - } - - /// BundleDetailView가 뭉치 로드를 마친 후 현재 구성 id를 last~ForDetail로 저장 - func updateLastConfigIds( - background: Background?, - carabiner: Carabiner?, - keyringDataList: [MultiKeyringScene.KeyringData] - ) { - lastBackgroundIdForDetail = makeBackgroundId(background) - lastCarabinerIdForDetail = makeCarabinerId(carabiner) - lastKeyringsIdForDetail = makeKeyringsId(keyringDataList) - } -} diff --git a/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Sort.swift b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Sort.swift deleted file mode 100644 index 71fc6a42a..000000000 --- a/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Sort.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// BundleViewModel+Sort.swift -// Keychy -// -// Created by 김서현 on 1/12/26. -// - -import Foundation - -extension BundleViewModel { - // MARK: - 키링 선택 시트 키링 정렬 - /// 키링 선택 시트용 정렬된 키링 리스트 - /// 1순위: 현재 위치에 선택된 키링 - /// 2순위: 일반 키링들 (선택되지 않고, published/packaged 아님) - /// 3순위: 다른 위치에 장착된 키링들 - /// 4순위: published 또는 packaged 상태의 키링들 (맨 뒤) - func sortedKeyringsForSelection(selectedKeyrings: [Int: Keyring], selectedPosition: Int) -> [Keyring] { - let selectedKeyring = selectedKeyrings[selectedPosition] - - return keyring.sorted { keyring1, keyring2 in - let isKeyring1SelectedHere = keyring1.id == selectedKeyring?.id - let isKeyring2SelectedHere = keyring2.id == selectedKeyring?.id - - let isKeyring1SelectedElsewhere = selectedKeyrings.values.contains { $0.id == keyring1.id } && !isKeyring1SelectedHere - let isKeyring2SelectedElsewhere = selectedKeyrings.values.contains { $0.id == keyring2.id } && !isKeyring2SelectedHere - - let isKeyring1Unavailable = keyring1.status == .published || keyring1.status == .packaged - let isKeyring2Unavailable = keyring2.status == .published || keyring2.status == .packaged - - // 1순위: 현재 위치에 선택된 키링 - 맨 앞 - if isKeyring1SelectedHere != isKeyring2SelectedHere { - return isKeyring1SelectedHere - } - - // 2순위: 일반 키링 vs 나머지 (elsewhere or unavailable) - let isKeyring1Normal = !isKeyring1SelectedElsewhere && !isKeyring1Unavailable - let isKeyring2Normal = !isKeyring2SelectedElsewhere && !isKeyring2Unavailable - - if isKeyring1Normal != isKeyring2Normal { - return isKeyring1Normal - } - - // 3순위: 다른 위치 장착 vs unavailable (다른 위치 장착이 먼저) - if isKeyring1SelectedElsewhere != isKeyring2SelectedElsewhere { - return isKeyring1SelectedElsewhere // elsewhere를 앞으로 - } - - // 4순위: unavailable 키링들 (맨 뒤) - if isKeyring1Unavailable != isKeyring2Unavailable { - return isKeyring2Unavailable // unavailable을 맨 뒤로 - } - - // 같은 그룹 내에서는 원래 순서 유지 (viewModel.keyringSorting 결과) - guard let index1 = keyring.firstIndex(of: keyring1), - let index2 = keyring.firstIndex(of: keyring2) else { - return false - } - return index1 < index2 - } - } -} diff --git a/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Types.swift b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Types.swift new file mode 100644 index 000000000..6916ef9b1 --- /dev/null +++ b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Types.swift @@ -0,0 +1,40 @@ +// +// BundleViewModel+Types.swift +// Keychy +// +// Created by 김서현 on 1/9/26. +// + +// MARK: - BundleViewModel+Types +// +// 화면 표시용 데이터 구조체 +// - BackgroundViewData: 배경 + 소유 여부 +// - CarabinerViewData: 카라비너 + 소유 여부 +// - KeyringInfo: Firebase 키링 정보 + +import Foundation + +/// 배경 화면 표시용 데이터 (소유 여부 포함) +struct BackgroundViewData: Identifiable, Equatable, Hashable { + var id: String { background.id ?? UUID().uuidString } + let background: Background + let isOwned: Bool +} + +/// 카라비너 화면 표시용 데이터 (소유 여부 포함) +struct CarabinerViewData: Identifiable, Equatable, Hashable { + var id: String { carabiner.id ?? UUID().uuidString } + let carabiner: Carabiner + let isOwned: Bool +} + +/// Firestore에서 가져온 키링 정보를 담는 구조체 +struct KeyringInfo { + let id: String + let bodyImage: String + let selectedTemplate: String? + let soundId: String + let particleId: String + let hookOffsetY: CGFloat? + let chainLength: Int +} diff --git a/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Image.swift b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Views.swift similarity index 51% rename from Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Image.swift rename to Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Views.swift index 805c07eb8..02b0c84ad 100644 --- a/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Image.swift +++ b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Views.swift @@ -1,15 +1,57 @@ // -// BundleViewModel+Image.swift +// BundleViewModel+Views.swift // Keychy // -// Created by 김서현 on 1/12/26. +// Created by 길지훈 on 2/5/26. // -import NukeUI +// MARK: - BundleViewModel+Views +// +// SwiftUI ViewBuilder 메서드 +// - bundleCaptureSceneView: 캡쳐 미리보기 +// - backCarabinerImage: 뒷 카라비너 이미지 +// - frontCarabinerImage: 앞 카라비너 이미지 +// - backgroundImage: 배경 이미지 + import SwiftUI +import NukeUI -// MARK: - Nuke를 통해 캐싱된 이미지, 또는 URL을 통해 이미지를 로드하는 메서드들 extension BundleViewModel { + + // MARK: - 뭉치 캡쳐 미리보기 + + /// BundleNameInputView, BundleNameEditView에서 사용하는 미리보기 씬 + @ViewBuilder + func bundleCaptureSceneView() -> some View { + let widthSize = screenWidth - 176 + let heightSize = widthSize * 7/5 + + Group { + if let imageData = bundleCapturedImage, + let uiImage = UIImage(data: imageData) { + Image(uiImage: uiImage) + .resizable() + .scaledToFill() + .offset(y: 30) + .clipped() + } else { + VStack { + Image(systemName: "photo") + .font(.system(size: 50)) + .foregroundColor(.gray) + Text("이미지를 불러오는 중...") + .font(.caption) + .foregroundColor(.gray) + } + } + } + .frame(width: widthSize, height: heightSize) + .clipShape(RoundedRectangle(cornerRadius: 15)) + .clipped() + } + + // MARK: - 카라비너 이미지 + /// 뒷 카라비너 이미지 (또는 단일 카라비너 이미지) func backCarabinerImage(carabiner: Carabiner) -> some View { LazyImage(url: URL(string: carabiner.backImageURL)) { state in @@ -24,7 +66,7 @@ extension BundleViewModel { } } } - + /// 앞 카라비너 이미지 (햄버거 타입만) func frontCarabinerImage(carabiner: Carabiner) -> some View { Group { @@ -45,7 +87,9 @@ extension BundleViewModel { } } } - + + // MARK: - 배경 이미지 + /// 배경 이미지 뷰 var backgroundImage: some View { Group { @@ -67,49 +111,4 @@ extension BundleViewModel { } } } - - /// 캐시에서 번들 이미지를 로드하여 bundleCapturedImage에 설정 - /// - Parameter bundle: 로드할 번들 - /// - Returns: 로드 성공 여부 - @discardableResult - func loadBundleImageFromCache(bundle: KeyringBundle) -> Bool { - guard let documentId = bundle.documentId else { - print("[CollectionViewModel] 번들 documentId가 없습니다.") - return false - } - - // BundleImageCache에서 이미지 로드 - if let imageData = BundleImageCache.shared.load(for: documentId) { - self.bundleCapturedImage = imageData - print("[CollectionViewModel] 캐시에서 번들 이미지 로드 성공: \(documentId)") - return true - } else { - print("[CollectionViewModel] 캐시에 번들 이미지가 없습니다: \(documentId)") - return false - } - } - - /// 뷰모델에 저장된 뭉치 이미지를 BundleImageCache에 저장 - /// - Parameters: - /// - bundleId: 뭉치 ID - /// - bundleName: 뭉치 이름 - /// - widgetImageData: 위젯용 이미지 (배경 없음, optional) - /// - createdAt: 뭉치 생성일 (새 뭉치는 Date(), 기존 뭉치는 bundle.createdAt) - func saveBundleImageToCache( - bundleId: String, - bundleName: String, - widgetImageData: Data? = nil, - createdAt: Date = Date() - ) { - guard let imageData = bundleCapturedImage else { - return - } - BundleImageCache.shared.syncBundle( - id: bundleId, - name: bundleName, - fullImageData: imageData, - widgetImageData: widgetImageData, - createdAt: createdAt - ) - } } diff --git a/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel.swift b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel.swift index b42d3e1b8..fc135d945 100644 --- a/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel.swift +++ b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel.swift @@ -5,84 +5,89 @@ // Created by 김서현 on 1/9/26. // +// MARK: - BundleViewModel (메인) +// +// 뭉치 관련 상태 관리. 프로퍼티만 포함. +// +// Extension 파일: +// +Types - 데이터 구조체 +// +Fetch - Firebase 읽기 +// +CRUD - Firebase 쓰기 +// +Edit - 편집 로직 +// +Purchase - 구매 로직 +// +Helpers - 유틸리티 +// +Cache - 캐시/최적화 +// +Views - UI 컴포넌트 + import SwiftUI import FirebaseFirestore import FirebaseStorage -import NukeUI - -// MARK: - 화면 표시용 구조체 -struct BackgroundViewData: Identifiable, Equatable, Hashable { - var id: String { background.id ?? UUID().uuidString } - let background: Background - let isOwned: Bool -} - -struct CarabinerViewData: Identifiable, Equatable, Hashable { - var id: String { carabiner.id ?? UUID().uuidString } - let carabiner: Carabiner - let isOwned: Bool -} - -/// Firestore에서 가져온 키링 정보를 담는 구조체 -struct KeyringInfo { - let id: String - let bodyImage: String - let selectedTemplate: String? - let soundId: String - let particleId: String - let hookOffsetY: CGFloat? - let chainLength: Int -} @Observable class BundleViewModel { + + // MARK: - Firebase + var db: Firestore { Firestore.firestore() } - - // MARK: - Shared Data + + // MARK: - Shared Data Manager + let dataManager = WorkshopDataManager.shared - // 배경 및 카라비너 데이터 (WorkshopDataManager에서 가져옴) + // MARK: - 배경/카라비너 원본 데이터 + var backgrounds: [Background] { dataManager.backgrounds } var selectedBackground: Background? var carabiners: [Carabiner] { dataManager.carabiners } var selectedCarabiner: Carabiner? - // 공방에서 "뭉치에 사용하기"로 진입 시 미리 선택할 아이템 ID + // MARK: - 공방에서 미리 선택된 아이템 ID + var preSelectedBackgroundId: String? var preSelectedCarabinerId: String? - - // 뭉치 이름 최대 글자 수 + + // MARK: - 뭉치 설정 + var maxBundleNameCount: Int = 9 - - // 뭉치 생성 시 선택 된 키링들을 저장 + + // MARK: - 뭉치 생성 시 선택된 키링 + var selectedKeyringsForBundle: [Int: Keyring] = [:] - - // 뭉치 캡쳐 이미지 (png 데이터) + + // MARK: - 뭉치 캡쳐 이미지 + var bundleCapturedImage: Data? - - // 현재 선택 된 뭉치 - 뭉치 상세뷰 접근 시 데이터 할당 됨 + + // MARK: - 현재 선택된 뭉치 + var selectedBundle: KeyringBundle? - - // MARK: - 사용자의 뭉치, 키링 정보를 저장하는 프로퍼티 + + // MARK: - 사용자 데이터 + var bundles: [KeyringBundle] = [] var keyring: [Keyring] = [] - + + // MARK: - 로딩 상태 + var isLoading = false - - // MARK: - 뭉치 편집뷰 용 데이터 + var isPurchasing = false + + // MARK: - 편집 화면용 데이터 + var newSelectedBackground: BackgroundViewData? var newSelectedCarabiner: CarabinerViewData? var selectedKeyringPosition: Int = 0 var keyringOrder: [Int] = [] var selectedKeyrings: [Int: Keyring] = [:] - + // MARK: - 화면 표시용 배열 + var _backgroundViewData: [BackgroundViewData] = [] var _carabinerViewData: [CarabinerViewData] = [] - + var backgroundViewData: [BackgroundViewData] { get { _backgroundViewData } set { _backgroundViewData = newValue } @@ -91,8 +96,9 @@ class BundleViewModel { get { _carabinerViewData } set { _carabinerViewData = newValue } } - - // 정렬된 뭉치 (메인 뭉치 우선 정렬) + + // MARK: - 정렬된 뭉치 + var sortedBundles: [KeyringBundle] { bundles.sorted { a, b in if a.isMain != b.isMain { @@ -101,33 +107,13 @@ class BundleViewModel { return a.createdAt > b.createdAt } } - - // MARK: - 구성 id 생성 헬퍼 (BundleDetailView의 로직과 동일한 규칙) - func makeBackgroundId(_ bg: Background?) -> String { - guard let bg else { return "" } - return bg.id ?? "" - } - - func makeCarabinerId(_ cb: Carabiner?) -> String { - guard let cb else { return "" } - return "\(cb.id ?? "")|\(cb.carabinerX)|\(cb.carabinerY)|\(cb.carabinerWidth)" - } - - func makeKeyringsId(_ list: [MultiKeyringScene.KeyringData]) -> String { - list - .sorted(by: { $0.index < $1.index }) - .map { item in - "\(item.index)|\(item.bodyImageURL)|\((item.templateId ?? ""))|\(item.soundId)|\(item.particleId)|\((item.hookOffsetY ?? 0))|\(item.chainLength)" - } - .joined(separator: ";") - } - - // MARK: - 이전 화면에서 전달된 구성 id 저장소 - // 이전 화면에서 pop 직전에 넘겨받은 구성 id를 임시 저장하는 내부 저장소 + + // MARK: - 구성 ID 저장소 (편집 → 상세 화면 전환용) + var _returnBackgroundId: String? var _returnCarabinerId: String? var _returnKeyringsId: String? - + var returnBackgroundId: String? { get { _returnBackgroundId } set { _returnBackgroundId = newValue } @@ -140,9 +126,9 @@ class BundleViewModel { get { _returnKeyringsId } set { _returnKeyringsId = newValue } } - - // MARK: - Detail에서 마지막으로 로드한 구성 id (Detail <-> ViewModel 공유) - // BundleDetailView가 마지막으로 로드 완료한 구성 id를 저장, 다음 진입 때 return~id와 비교해 동일 구성 여부 판단에 사용 + + // MARK: - 마지막 로드 구성 ID (씬 리로드 최적화용) + var _lastBackgroundIdForDetail: String? var _lastCarabinerIdForDetail: String? var _lastKeyringsIdForDetail: String? @@ -159,112 +145,4 @@ class BundleViewModel { get { _lastKeyringsIdForDetail ?? "" } set { _lastKeyringsIdForDetail = newValue } } - - /// 배경, 카라비너의 사용 횟수를 증가시키는 메서드 - func incrementUseCount( - carabinerId: String?, - backgroundId: String? - ) { - if let carabinerId = carabinerId, !carabinerId.isEmpty { - db.collection("Carabiner") - .document(carabinerId) - .updateData([ - "useCount": FieldValue.increment(Int64(1)) - ]) { error in - if let error = error { - print("[useCount] Carabiner 증가 실패: \(error)") - } else { - print("[useCount] Carabiner 증가 성공: \(carabinerId)") - } - } - } - - if let backgroundId = backgroundId, !backgroundId.isEmpty { - db.collection("Background") - .document(backgroundId) - .updateData([ - "useCount": FieldValue.increment(Int64(1)) - ]) { error in - if let error = error { - print("[useCount] Background 증가 실패: \(error)") - } else { - print("[useCount] Background 증가 성공: \(backgroundId)") - } - } - } - } - - /// Resolve Helpers (id -> Model) - func resolveCarabiner(from id: String) -> Carabiner? { - carabiners.first { $0.id == id } - } - - func resolveBackground(from id: String) -> Background? { - backgrounds.first { $0.id == id } - } - - // MARK: - User Background Management - /// User의 backgrounds 배열에 새 배경 추가 - func addBackgroundToUser(backgroundName: String, userManager: UserManager) async -> Bool { - guard let userId = userManager.currentUser?.id else { - print("사용자 ID를 가져올 수 없습니다") - return false - } - - let db = FirebaseFirestore.Firestore.firestore() - let userRef = db.collection("User").document(userId) - - do { - // Firebase 업데이트 (ItemPurchaseManager와 동일한 방식) - try await userRef.updateData([ - "backgrounds": FirebaseFirestore.FieldValue.arrayUnion([backgroundName]) - ]) - - // UserManager 데이터 갱신 - await withCheckedContinuation { (continuation: CheckedContinuation) in - userManager.loadUserInfo(uid: userId) { _ in - continuation.resume() - } - } - - print("User backgrounds 업데이트 완료: \(backgroundName)") - return true - - } catch { - print("User backgrounds 업데이트 에러: \(error.localizedDescription)") - return false - } - } - - ///User의 카라비너에 새 카라비너 추가 - func addCarabinerToUser(carabinerName: String, userManager: UserManager) async -> Bool { - guard let userId = userManager.currentUser?.id else { - print("사용자 ID를 가져올 수 없습니다") - return false - } - - let db = FirebaseFirestore.Firestore.firestore() - let userRef = db.collection("User").document(userId) - - do { - // Firebase 업데이트 (ItemPurchaseManager와 동일한 방식) - try await userRef.updateData([ - "carabiners": FirebaseFirestore.FieldValue.arrayUnion([carabinerName]) - ]) - - // UserManager 데이터 갱신 - await withCheckedContinuation { (continuation: CheckedContinuation) in - userManager.loadUserInfo(uid: userId) { _ in - continuation.resume() - } - } - - print("User carabiners 업데이트 완료: \(carabinerName)") - return true - - } catch { - print("User carabiners 업데이트 에러: \(error.localizedDescription)") - return false - } - } } diff --git a/Keychy/Keychy/Presentation/Bundle/Views/BundleEditView+Purchase.swift b/Keychy/Keychy/Presentation/Bundle/Views/BundleEditView+Purchase.swift deleted file mode 100644 index 9b193117d..000000000 --- a/Keychy/Keychy/Presentation/Bundle/Views/BundleEditView+Purchase.swift +++ /dev/null @@ -1,203 +0,0 @@ -// -// BundleEditView+Purchase.swift -// Keychy -// -// Created by 김서현 on 1/13/26. -// -// 뭉치 편집뷰의 구매 관련 로직을 모아놓은 파일입니다. -import SwiftUI - -extension BundleEditView { - var purchaseSheetView: some View { - VStack(spacing: 12) { - // 상단 섹션 - 닫기 버튼, 타이틀 - HStack { - Button { - showPurchaseSheet = false - } label: { - Image(systemName: "xmark") - .font(.system(size: 20)) - .foregroundStyle(.gray600) - } - Spacer() - Text("구매하기") - .typography(.suit17B) - .foregroundStyle(.gray600) - Spacer() - } - .padding(EdgeInsets(top: 30, leading: 20, bottom: 10, trailing: 20)) - - // 구매할 아이템 목록 - ScrollView { - VStack(spacing: 20) { - if let bg = bundleVM.newSelectedBackground, !bg.isOwned && bg.background.price > 0 { - cartItemRow(name: bg.background.backgroundName, type: "배경", price: bg.background.price) - } - if let cb = bundleVM.newSelectedCarabiner, !cb.isOwned && cb.carabiner.price > 0 { - cartItemRow(name: cb.carabiner.carabinerName, type: "카라비너", price: cb.carabiner.price) - } - } - .padding(.horizontal, 20) - } - - // 내 보유 재화와 총 가격 - HStack(spacing: 6) { - Text("내 보유 : ") - .typography(.suit15M25) - .foregroundStyle(.black100) - .padding(.vertical, 4.5) - Text("\(UserManager.shared.currentUser?.coin ?? 0)") - .typography(.nanum16EB) - .foregroundStyle(.main500) - } - purchaseButton - .padding(.horizontal, 33.2) - .adaptiveBottomPadding() - } - .background(.white100) - .presentationDetents([.fraction(0.43)]) - } - - private func cartItemRow(name: String, type: String, price: Int) -> some View { - HStack(spacing: 6) { - Image(.selectedIcon) - - Text(name) - .typography(.suit16B) - .foregroundStyle(.black100) - .padding(.trailing, 7) - - Text(type) - .typography(.suit13M) - .foregroundStyle(.gray400) - - Spacer() - - Text("\(price)") - .typography(.nanum16EB) - .foregroundStyle(.main500) - } - .padding(.vertical, 15) - .padding(.horizontal, 16) - .background( - RoundedRectangle(cornerRadius: 12) - .fill(.gray50) - ) - } - - // 구매 버튼, PurchaseManager로 통합한 후에 공통 컴포넌트로 빼야 할 듯. - private var purchaseButton: some View { - Button { - Task { - await purchaseItems() - } - } label: { - HStack(spacing: 5) { - if isPurchasing { - LoadingAlert(type: .short40, message: nil) - } else { - Image(.myCoinMini) - } - - Text("\(totalCartPrice)") - .typography(.nanum18EB) - .padding(.top, 16) - .padding(.bottom, 12) - - Text("(\(payableItemsCount)개)") - .typography(.suit17SB) - } - .foregroundStyle(.white100) - .frame(maxWidth: .infinity) - .background(isPurchasing ? .gray400 : .black80) - .clipShape(RoundedRectangle(cornerRadius: 100)) - } - .disabled(isPurchasing) - } - - var payableItemsCount: Int { - let backgroundCount = (bundleVM.newSelectedBackground != nil && !bundleVM.newSelectedBackground!.isOwned && bundleVM.newSelectedBackground!.background.price > 0) ? 1 : 0 - let carabinerCount = (bundleVM.newSelectedCarabiner != nil && !bundleVM.newSelectedCarabiner!.isOwned && bundleVM.newSelectedCarabiner!.carabiner.price > 0) ? 1 : 0 - return backgroundCount + carabinerCount - } - - var totalCartPrice: Int { - let backgroundPrice = (bundleVM.newSelectedBackground != nil && !bundleVM.newSelectedBackground!.isOwned && bundleVM.newSelectedBackground!.background.price > 0) ? bundleVM.newSelectedBackground!.background.price : 0 - let carabinerPrice = (bundleVM.newSelectedCarabiner != nil && !bundleVM.newSelectedCarabiner!.isOwned && bundleVM.newSelectedCarabiner!.carabiner.price > 0) ? bundleVM.newSelectedCarabiner!.carabiner.price : 0 - return backgroundPrice + carabinerPrice - } - - // MARK: - 구매 처리 - private func purchaseItems() async { - isPurchasing = true - - var allSuccess = true - - // 선택된 배경이 유료인 경우 구매 - if let bg = bundleVM.newSelectedBackground, !bg.isOwned && bg.background.price > 0 { - let result = await ItemPurchaseManager.shared.purchaseWorkshopItem(bg.background, userManager: UserManager.shared) - - switch result { - case .success: - break - case .insufficientCoins, .failed(_): - allSuccess = false - } - } - - // 선택된 카라비너가 유료이고 이전 구매가 성공한 경우에만 구매 - if allSuccess, let cb = bundleVM.newSelectedCarabiner, !cb.isOwned && cb.carabiner.price > 0 { - let result = await ItemPurchaseManager.shared.purchaseWorkshopItem(cb.carabiner, userManager: UserManager.shared) - - switch result { - case .success: - break - case .insufficientCoins, .failed(_): - allSuccess = false - } - } - - if allSuccess { - // 모든 구매 성공 - alert만 표시 - await MainActor.run { - isPurchasing = false - showPurchaseSheet = false - showPurchaseSuccessAlert = true - purchasesSuccessScale = 0.3 - withAnimation(.spring(response: 0.6, dampingFraction: 0.5)) { - purchasesSuccessScale = 1.0 - } - } - - await bundleVM.refreshEditData() - - // 1초 후 알럿 자동 닫기 및 저장 후 화면 이동 - try? await Task.sleep(nanoseconds: 1_000_000_000) // 1초 대기 - - await bundleVM.saveBundleChanges() - await MainActor.run { - showPurchaseSuccessAlert = false - purchasesSuccessScale = 0.3 - } - - } else { - // 구매 실패 - await MainActor.run { - isPurchasing = false - // 시트 먼저 닫기 - showPurchaseSheet = false - } - - // 시트 닫히는 애니메이션 대기 - try? await Task.sleep(nanoseconds: 300_000_000) - - await MainActor.run { - showPurchaseFailAlert = true - purchaseFailScale = 0.3 - withAnimation(.spring(response: 0.6, dampingFraction: 0.5)) { - purchaseFailScale = 1.0 - } - } - } - } -} diff --git a/Keychy/Keychy/Presentation/Bundle/Views/BundleAddKeyringView.swift b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleAddKeyringView.swift similarity index 99% rename from Keychy/Keychy/Presentation/Bundle/Views/BundleAddKeyringView.swift rename to Keychy/Keychy/Presentation/Bundle/Views/Create/BundleAddKeyringView.swift index 7c5791bd1..cfa09c705 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/BundleAddKeyringView.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleAddKeyringView.swift @@ -147,7 +147,7 @@ extension BundleAddKeyringView { let viewX = dx + carabiner.keyringXPosition[index] * scale let viewY = dy + carabiner.keyringYPosition[index] * scale - CarabinerAddKeyringButton( + AddKeyringButton( isSelected: selectedPosition == index, action: { selectedPosition = index diff --git a/Keychy/Keychy/Presentation/Bundle/Views/BundleCreateView.swift b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift similarity index 62% rename from Keychy/Keychy/Presentation/Bundle/Views/BundleCreateView.swift rename to Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift index 0d95a51cd..831ddaf91 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/BundleCreateView.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift @@ -15,15 +15,8 @@ struct BundleCreateView: View { //MARK: - 프로퍼티들 @Bindable var router: NavigationRouter @State var collectionVM: CollectionViewModel - @State var bundleVM: BundleViewModel - - /// 선택한 카테고리 : "Background" 또는 "Carabiner" - @State private var selectedCategory: String = "" - - // 선택한 배경과 카라비너 - @State private var selectedBackground: BackgroundViewData? - @State private var selectedCarabiner: CarabinerViewData? - + @Bindable var bundleVM: BundleViewModel + // 시트 활성화 상태 @State private var showBackgroundSheet: Bool = false @State private var showCarabinerSheet: Bool = false @@ -34,10 +27,7 @@ struct BundleCreateView: View { // 구매 시트 @State var showPurchaseSheet = false - - // 구매 처리 상태 - @State private var isPurchasing = false - + // 구매 Alert 애니메이션 @State var showPurchaseSuccessAlert = false @State var purchasesSuccessScale: CGFloat = 0.3 @@ -55,8 +45,8 @@ struct BundleCreateView: View { //MARK: 메인 뷰 var body: some View { ZStack(alignment: .bottom) { - if let bg = selectedBackground, - let cb = selectedCarabiner { + if let bg = bundleVM.newSelectedBackground, + let cb = bundleVM.newSelectedCarabiner { // 배경과 카라비너만 보여줌 MultiKeyringSceneView( keyringDataList: [], @@ -130,17 +120,6 @@ struct BundleCreateView: View { showBackgroundSheet = false } } - // 선택한 배경과 카라비너를 ViewModel과 자동 동기화 - .onChange(of: selectedBackground) { _, newValue in - if let bg = newValue { - bundleVM.selectedBackground = bg.background - } - } - .onChange(of: selectedCarabiner) { _, newValue in - if let cb = newValue { - bundleVM.selectedCarabiner = cb.carabiner - } - } } } @@ -153,16 +132,17 @@ extension BundleCreateView { } } center: { } trailing: { - if hasUnpurchasedItems { - PurchaseToolbarButton(title: "구매 \(payableItemsCount)") { + if bundleVM.hasUnpurchasedItems { + PurchaseToolbarButton(title: "구매 \(bundleVM.payableItemsCount)") { showPurchaseSheet = true } } else { NextToolbarButton { - if let bg = selectedBackground { + // ViewModel 상태를 selectedBackground/selectedCarabiner로도 동기화 + if let bg = bundleVM.newSelectedBackground { bundleVM.selectedBackground = bg.background } - if let cb = selectedCarabiner { + if let cb = bundleVM.newSelectedCarabiner { bundleVM.selectedCarabiner = cb.carabiner } router.push(.bundleAddKeyringView) @@ -181,44 +161,38 @@ extension BundleCreateView { if showBackgroundSheet { VStack(spacing: 0) { Spacer() - HStack(spacing: 8) { - editBackgroundButton - editCarabinerButton - Spacer() - } - .padding(.leading, 18) - .padding(.bottom, 10) - BundleItemCustomSheet( + BundleSheetToggleButtons( + showBackgroundSheet: $showBackgroundSheet, + showCarabinerSheet: $showCarabinerSheet + ) + DraggableSheet( sheetHeight: $sheetHeight, content: SelectBackgroundSheet( viewModel: bundleVM, - selectedBG: selectedBackground, + selectedBG: bundleVM.newSelectedBackground, onBackgroundTap: { bg in - selectedBackground = bg + bundleVM.newSelectedBackground = bg } ) ) } } - + // 카라비너 시트 if showCarabinerSheet { VStack(spacing: 0) { Spacer() - HStack(spacing: 8) { - editBackgroundButton - editCarabinerButton - Spacer() - } - .padding(.leading, 18) - .padding(.bottom, 10) - BundleItemCustomSheet( + BundleSheetToggleButtons( + showBackgroundSheet: $showBackgroundSheet, + showCarabinerSheet: $showCarabinerSheet + ) + DraggableSheet( sheetHeight: $sheetHeight, content: SelectCarabinerSheet( viewModel: bundleVM, - selectedCarabiner: selectedCarabiner, + selectedCarabiner: bundleVM.newSelectedCarabiner, onCarabinerTap: { carabiner in - selectedCarabiner = carabiner + bundleVM.newSelectedCarabiner = carabiner } ) ) @@ -246,15 +220,15 @@ extension BundleCreateView { } // 현재 선택된 아이템의 ID 저장 - let currentBackgroundId = selectedBackground?.background.id - let currentCarabinerId = selectedCarabiner?.carabiner.id + let currentBackgroundId = bundleVM.newSelectedBackground?.background.id + let currentCarabinerId = bundleVM.newSelectedCarabiner?.carabiner.id // 배경 데이터 새로고침 await withCheckedContinuation { continuation in bundleVM.fetchAllBackgrounds { _ in // 이전에 선택했던 배경을 다시 찾아서 선택 (구매 상태가 업데이트됨) if let bgId = currentBackgroundId { - self.selectedBackground = bundleVM.backgroundViewData.first { $0.background.id == bgId } + self.bundleVM.newSelectedBackground = bundleVM.backgroundViewData.first { $0.background.id == bgId } } continuation.resume() } @@ -265,7 +239,7 @@ extension BundleCreateView { bundleVM.fetchAllCarabiners { _ in // 이전에 선택했던 카라비너를 다시 찾아서 선택 (구매 상태가 업데이트됨) if let cbId = currentCarabinerId { - self.selectedCarabiner = bundleVM.carabinerViewData.first { $0.carabiner.id == cbId } + self.bundleVM.newSelectedCarabiner = bundleVM.carabinerViewData.first { $0.carabiner.id == cbId } } continuation.resume() } @@ -281,17 +255,17 @@ extension BundleCreateView { // 배경 데이터 로드 await withCheckedContinuation { continuation in bundleVM.fetchAllBackgrounds { _ in - if self.selectedBackground == nil { + if self.bundleVM.newSelectedBackground == nil { // 공방에서 미리 선택된 배경이 있으면 해당 배경 선택 if let preSelectedId = bundleVM.preSelectedBackgroundId { - self.selectedBackground = bundleVM.backgroundViewData.first { bg in + self.bundleVM.newSelectedBackground = bundleVM.backgroundViewData.first { bg in bg.background.id == preSelectedId } bundleVM.preSelectedBackgroundId = nil // 사용 후 초기화 } // 미리 선택된 배경이 없으면 "퍼플키치"를 기본으로 선택, 없으면 첫 번째 선택 - if self.selectedBackground == nil { - self.selectedBackground = bundleVM.backgroundViewData.first { bg in + if self.bundleVM.newSelectedBackground == nil { + self.bundleVM.newSelectedBackground = bundleVM.backgroundViewData.first { bg in bg.background.backgroundName == "퍼플키치" } ?? bundleVM.backgroundViewData.first } @@ -304,17 +278,17 @@ extension BundleCreateView { // 카라비너 데이터 로드 await withCheckedContinuation { continuation in bundleVM.fetchAllCarabiners { _ in - if self.selectedCarabiner == nil { + if self.bundleVM.newSelectedCarabiner == nil { // 공방에서 미리 선택된 카라비너가 있으면 해당 카라비너 선택 if let preSelectedId = bundleVM.preSelectedCarabinerId { - self.selectedCarabiner = bundleVM.carabinerViewData.first { cb in + self.bundleVM.newSelectedCarabiner = bundleVM.carabinerViewData.first { cb in cb.carabiner.id == preSelectedId } bundleVM.preSelectedCarabinerId = nil // 사용 후 초기화 } // 미리 선택된 카라비너가 없으면 "웰컴 키치"를 기본으로 선택, 없으면 첫 번째 선택 - if self.selectedCarabiner == nil { - self.selectedCarabiner = bundleVM.carabinerViewData.first { cb in + if self.bundleVM.newSelectedCarabiner == nil { + self.bundleVM.newSelectedCarabiner = bundleVM.carabinerViewData.first { cb in cb.carabiner.carabinerName == "웰컴 키치" } ?? bundleVM.carabinerViewData.first } @@ -396,11 +370,11 @@ extension BundleCreateView { // 구매할 아이템 목록 VStack(spacing: 20) { - if let bg = selectedBackground, !bg.isOwned && bg.background.price > 0 { - cartItemRow(name: bg.background.backgroundName, type: "배경", price: bg.background.price) + if let bg = bundleVM.newSelectedBackground, !bg.isOwned && bg.background.price > 0 { + BundlePurchaseCartItem(name: bg.background.backgroundName, type: "배경", price: bg.background.price) } - if let cb = selectedCarabiner, !cb.isOwned && cb.carabiner.price > 0 { - cartItemRow(name: cb.carabiner.carabinerName, type: "카라비너", price: cb.carabiner.price) + if let cb = bundleVM.newSelectedCarabiner, !cb.isOwned && cb.carabiner.price > 0 { + BundlePurchaseCartItem(name: cb.carabiner.carabinerName, type: "카라비너", price: cb.carabiner.price) } } .padding(.horizontal, 20) @@ -427,33 +401,6 @@ extension BundleCreateView { ) } - private func cartItemRow(name: String, type: String, price: Int) -> some View { - HStack(spacing: 6) { - Image(.selectedIcon) - - Text(name) - .typography(.suit16B) - .foregroundStyle(.black100) - .padding(.trailing, 7) - - Text(type) - .typography(.suit13M) - .foregroundStyle(.gray400) - - Spacer() - - Text("\(price)") - .typography(.nanum16EB) - .foregroundStyle(.main500) - } - .padding(.vertical, 20) - .padding(.horizontal, 16) - .background( - RoundedRectangle(cornerRadius: 12) - .fill(.gray50) - ) - } - // 구매 버튼 private var purchaseButton: some View { Button { @@ -462,83 +409,46 @@ extension BundleCreateView { } } label: { HStack(spacing: 5) { - if isPurchasing { + if bundleVM.isPurchasing { LoadingAlert(type: .short40, message: nil) } else { Image(.myCoinMini) } - - Text("\(totalCartPrice)") + + Text("\(bundleVM.totalCartPrice)") .typography(.nanum18EB) .padding(.top, 16) .padding(.bottom, 12) - - Text("(\(payableItemsCount)개)") + + Text("(\(bundleVM.payableItemsCount)개)") .typography(.suit17SB) } .foregroundStyle(.white100) .frame(maxWidth: .infinity) - .background(isPurchasing ? .gray400 : .black80) + .background(bundleVM.isPurchasing ? .gray400 : .black80) .clipShape(RoundedRectangle(cornerRadius: 100)) } - .disabled(isPurchasing) + .disabled(bundleVM.isPurchasing) } - - var payableItemsCount: Int { - let backgroundCount = (selectedBackground != nil && !selectedBackground!.isOwned && selectedBackground!.background.price > 0) ? 1 : 0 - let carabinerCount = (selectedCarabiner != nil && !selectedCarabiner!.isOwned && selectedCarabiner!.carabiner.price > 0) ? 1 : 0 - return backgroundCount + carabinerCount - } - - var totalCartPrice: Int { - let backgroundPrice = (selectedBackground != nil && !selectedBackground!.isOwned && selectedBackground!.background.price > 0) ? selectedBackground!.background.price : 0 - let carabinerPrice = (selectedCarabiner != nil && !selectedCarabiner!.isOwned && selectedCarabiner!.carabiner.price > 0) ? selectedCarabiner!.carabiner.price : 0 - return backgroundPrice + carabinerPrice - } - + // MARK: - 구매 처리 private func purchaseItems() async { - isPurchasing = true - - var allSuccess = true - - // 선택된 배경이 유료인 경우 구매 - if let bg = selectedBackground, !bg.isOwned && bg.background.price > 0 { - let result = await ItemPurchaseManager.shared.purchaseWorkshopItem(bg.background, userManager: UserManager.shared) - - switch result { - case .success: - break - case .insufficientCoins, .failed(_): - allSuccess = false - } - } - - // 선택된 카라비너가 유료이고 이전 구매가 성공한 경우에만 구매 - if allSuccess, let cb = selectedCarabiner, !cb.isOwned && cb.carabiner.price > 0 { - let result = await ItemPurchaseManager.shared.purchaseWorkshopItem(cb.carabiner, userManager: UserManager.shared) - - switch result { - case .success: - break - case .insufficientCoins, .failed(_): - allSuccess = false - } - } - - if allSuccess { - // 모든 구매 성공 - alert만 표시 + let result = await bundleVM.purchaseSelectedItems() + + switch result { + case .success: + // 모든 구매 성공 await refreshData() - + await MainActor.run { - if let bg = selectedBackground { + // ViewModel 상태 동기화 + if let bg = bundleVM.newSelectedBackground { bundleVM.selectedBackground = bg.background } - if let cb = selectedCarabiner { + if let cb = bundleVM.newSelectedCarabiner { bundleVM.selectedCarabiner = cb.carabiner } - - isPurchasing = false + showPurchaseSheet = false showPurchaseSuccessAlert = true purchasesSuccessScale = 0.3 @@ -546,26 +456,24 @@ extension BundleCreateView { purchasesSuccessScale = 1.0 } } - + // 2.5초 후 알럿 자동 닫기 (Alert duration 2초 + 0.5초 여유) - try? await Task.sleep(nanoseconds: 2_500_000_000) - + try? await Task.sleep(for: .seconds(2.5)) + await MainActor.run { showPurchaseSuccessAlert = false purchasesSuccessScale = 0.3 } - - } else { + + case .insufficientCoins, .failed: // 구매 실패 await MainActor.run { - isPurchasing = false - // 시트 먼저 닫기 showPurchaseSheet = false } - + // 시트 닫히는 애니메이션 대기 - try? await Task.sleep(nanoseconds: 300_000_000) - + try? await Task.sleep(for: .seconds(0.3)) + await MainActor.run { showPurchaseFailAlert = true purchaseFailScale = 0.3 @@ -577,52 +485,3 @@ extension BundleCreateView { } } -// MARK: - 하단 버튼 -extension BundleCreateView { - private var editBackgroundButton: some View { - Button { - // 배경 시트 열기 - showBackgroundSheet = true - } label: { - VStack(spacing: 0) { - Image(showBackgroundSheet ? .backgroundIconWhite100 : .backgroundIconGray600) - Text("배경") - .typography(.suit9SB) - .foregroundStyle(showBackgroundSheet ? .white100 : .gray600) - } - .frame(width: 46, height: 46) - .background( - RoundedRectangle(cornerRadius: 14.38) - .fill(showBackgroundSheet ? .main500 : .white100) - ) - } - .buttonStyle(.plain) - } - - private var editCarabinerButton: some View { - Button { - // 카라비너 시트 열기 - showCarabinerSheet = true - } label: { - VStack(spacing: 0) { - Image(showCarabinerSheet ? .carabinerIconWhite100 : .carabinerIconGray600) - Text("카라비너") - .typography(.suit9SB) - .foregroundStyle(showCarabinerSheet ? .white100 : .gray600) - } - .frame(width: 46, height: 46) - .background( - RoundedRectangle(cornerRadius: 14.38) - .fill(showCarabinerSheet ? .main500 : .white100) - ) - } - .buttonStyle(.plain) - } - - /// 구매하지 않은 유료 아이템이 있는지 확인 - private var hasUnpurchasedItems: Bool { - let hasUnpurchasedBackground = selectedBackground != nil && !selectedBackground!.isOwned && selectedBackground!.background.price > 0 - let hasUnpurchasedCarabiner = selectedCarabiner != nil && !selectedCarabiner!.isOwned && selectedCarabiner!.carabiner.price > 0 - return hasUnpurchasedBackground || hasUnpurchasedCarabiner - } -} diff --git a/Keychy/Keychy/Presentation/Bundle/Views/BundleNameInputView.swift b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleNameInputView.swift similarity index 100% rename from Keychy/Keychy/Presentation/Bundle/Views/BundleNameInputView.swift rename to Keychy/Keychy/Presentation/Bundle/Views/Create/BundleNameInputView.swift diff --git a/Keychy/Keychy/Presentation/Bundle/Views/BundleDetailView+Alert.swift b/Keychy/Keychy/Presentation/Bundle/Views/Detail/BundleDetailView+Alert.swift similarity index 100% rename from Keychy/Keychy/Presentation/Bundle/Views/BundleDetailView+Alert.swift rename to Keychy/Keychy/Presentation/Bundle/Views/Detail/BundleDetailView+Alert.swift diff --git a/Keychy/Keychy/Presentation/Bundle/Views/BundleDetailView+Menu.swift b/Keychy/Keychy/Presentation/Bundle/Views/Detail/BundleDetailView+Menu.swift similarity index 100% rename from Keychy/Keychy/Presentation/Bundle/Views/BundleDetailView+Menu.swift rename to Keychy/Keychy/Presentation/Bundle/Views/Detail/BundleDetailView+Menu.swift diff --git a/Keychy/Keychy/Presentation/Bundle/Views/BundleDetailView+SaveImage.swift b/Keychy/Keychy/Presentation/Bundle/Views/Detail/BundleDetailView+SaveImage.swift similarity index 100% rename from Keychy/Keychy/Presentation/Bundle/Views/BundleDetailView+SaveImage.swift rename to Keychy/Keychy/Presentation/Bundle/Views/Detail/BundleDetailView+SaveImage.swift diff --git a/Keychy/Keychy/Presentation/Bundle/Views/BundleDetailView+VideoGen.swift b/Keychy/Keychy/Presentation/Bundle/Views/Detail/BundleDetailView+VideoGen.swift similarity index 100% rename from Keychy/Keychy/Presentation/Bundle/Views/BundleDetailView+VideoGen.swift rename to Keychy/Keychy/Presentation/Bundle/Views/Detail/BundleDetailView+VideoGen.swift diff --git a/Keychy/Keychy/Presentation/Bundle/Views/BundleDetailView.swift b/Keychy/Keychy/Presentation/Bundle/Views/Detail/BundleDetailView.swift similarity index 100% rename from Keychy/Keychy/Presentation/Bundle/Views/BundleDetailView.swift rename to Keychy/Keychy/Presentation/Bundle/Views/Detail/BundleDetailView.swift diff --git a/Keychy/Keychy/Presentation/Bundle/Views/BundleInventoryView.swift b/Keychy/Keychy/Presentation/Bundle/Views/Detail/BundleInventoryView.swift similarity index 98% rename from Keychy/Keychy/Presentation/Bundle/Views/BundleInventoryView.swift rename to Keychy/Keychy/Presentation/Bundle/Views/Detail/BundleInventoryView.swift index 2794a325d..49643f774 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/BundleInventoryView.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Detail/BundleInventoryView.swift @@ -130,7 +130,7 @@ extension BundleInventoryView { isNavigatingDeeper = true router.push(.bundleDetailView) } label: { - KeyringBundleItem(bundle: bundle) + BundleGridItem(bundle: bundle) } } } diff --git a/Keychy/Keychy/Presentation/Bundle/Views/BundleMenu.swift b/Keychy/Keychy/Presentation/Bundle/Views/Detail/BundleMenu.swift similarity index 100% rename from Keychy/Keychy/Presentation/Bundle/Views/BundleMenu.swift rename to Keychy/Keychy/Presentation/Bundle/Views/Detail/BundleMenu.swift diff --git a/Keychy/Keychy/Presentation/Bundle/Views/BundleEditView+Alert.swift b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+Alert.swift similarity index 99% rename from Keychy/Keychy/Presentation/Bundle/Views/BundleEditView+Alert.swift rename to Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+Alert.swift index 4df4ccc16..e177c5d22 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/BundleEditView+Alert.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+Alert.swift @@ -22,7 +22,7 @@ extension BundleEditView { } VStack { Spacer() - CarabinerChangePopup( + CarabinerPopup( title: "카라비너를 변경하시겠어요?", message: "새 카라비너로 변경하면\n현재 뭉치에 걸린 키링들이 모두 해제돼요.", onCancel: { diff --git a/Keychy/Keychy/Presentation/Bundle/Views/BundleEditView+Initialization.swift b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+Initialization.swift similarity index 100% rename from Keychy/Keychy/Presentation/Bundle/Views/BundleEditView+Initialization.swift rename to Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+Initialization.swift diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+Purchase.swift b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+Purchase.swift new file mode 100644 index 000000000..e2775967b --- /dev/null +++ b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+Purchase.swift @@ -0,0 +1,136 @@ +// +// BundleEditView+Purchase.swift +// Keychy +// +// Created by 김서현 on 1/13/26. +// +// 뭉치 편집뷰의 구매 관련 로직을 모아놓은 파일입니다. +import SwiftUI + +extension BundleEditView { + var purchaseSheetView: some View { + VStack(spacing: 12) { + // 상단 섹션 - 닫기 버튼, 타이틀 + HStack { + Button { + showPurchaseSheet = false + } label: { + Image(systemName: "xmark") + .font(.system(size: 20)) + .foregroundStyle(.gray600) + } + Spacer() + Text("구매하기") + .typography(.suit17B) + .foregroundStyle(.gray600) + Spacer() + } + .padding(EdgeInsets(top: 30, leading: 20, bottom: 10, trailing: 20)) + + // 구매할 아이템 목록 + ScrollView { + VStack(spacing: 20) { + if let bg = bundleVM.newSelectedBackground, !bg.isOwned && bg.background.price > 0 { + BundlePurchaseCartItem(name: bg.background.backgroundName, type: "배경", price: bg.background.price) + } + if let cb = bundleVM.newSelectedCarabiner, !cb.isOwned && cb.carabiner.price > 0 { + BundlePurchaseCartItem(name: cb.carabiner.carabinerName, type: "카라비너", price: cb.carabiner.price) + } + } + .padding(.horizontal, 20) + } + + // 내 보유 재화와 총 가격 + HStack(spacing: 6) { + Text("내 보유 : ") + .typography(.suit15M25) + .foregroundStyle(.black100) + .padding(.vertical, 4.5) + Text("\(UserManager.shared.currentUser?.coin ?? 0)") + .typography(.nanum16EB) + .foregroundStyle(.main500) + } + purchaseButton + .padding(.horizontal, 33.2) + .adaptiveBottomPadding() + } + .background(.white100) + .presentationDetents([.fraction(0.43)]) + } + + // 구매 버튼 + private var purchaseButton: some View { + Button { + Task { + await purchaseItems() + } + } label: { + HStack(spacing: 5) { + if bundleVM.isPurchasing { + LoadingAlert(type: .short40, message: nil) + } else { + Image(.myCoinMini) + } + + Text("\(bundleVM.totalCartPrice)") + .typography(.nanum18EB) + .padding(.top, 16) + .padding(.bottom, 12) + + Text("(\(bundleVM.payableItemsCount)개)") + .typography(.suit17SB) + } + .foregroundStyle(.white100) + .frame(maxWidth: .infinity) + .background(bundleVM.isPurchasing ? .gray400 : .black80) + .clipShape(RoundedRectangle(cornerRadius: 100)) + } + .disabled(bundleVM.isPurchasing) + } + + // MARK: - 구매 처리 + private func purchaseItems() async { + let result = await bundleVM.purchaseSelectedItems() + + switch result { + case .success: + // 모든 구매 성공 + await MainActor.run { + showPurchaseSheet = false + showPurchaseSuccessAlert = true + purchasesSuccessScale = 0.3 + withAnimation(.spring(response: 0.6, dampingFraction: 0.5)) { + purchasesSuccessScale = 1.0 + } + } + + await bundleVM.refreshEditData() + + // 1초 후 알럿 자동 닫기 및 저장 후 화면 이동 + try? await Task.sleep(for: .seconds(1)) + + await bundleVM.saveBundleChanges() + await MainActor.run { + showPurchaseSuccessAlert = false + purchasesSuccessScale = 0.3 + } + + case .insufficientCoins, .failed: + // 구매 실패 + await MainActor.run { + showPurchaseSheet = false + } + + // 시트 닫히는 애니메이션 대기 + try? await Task.sleep(for: .seconds(0.3)) + + await MainActor.run { + showPurchaseFailAlert = true + purchaseFailScale = 0.3 + withAnimation(.spring(response: 0.6, dampingFraction: 0.5)) { + purchaseFailScale = 1.0 + } + } + } + } +} diff --git a/Keychy/Keychy/Presentation/Bundle/Views/BundleEditView+RestoreSelection.swift b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+RestoreSelection.swift similarity index 100% rename from Keychy/Keychy/Presentation/Bundle/Views/BundleEditView+RestoreSelection.swift rename to Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+RestoreSelection.swift diff --git a/Keychy/Keychy/Presentation/Bundle/Views/BundleEditView+SelectSheet.swift b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+SelectSheet.swift similarity index 75% rename from Keychy/Keychy/Presentation/Bundle/Views/BundleEditView+SelectSheet.swift rename to Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+SelectSheet.swift index fe0938439..4ef7b9337 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/BundleEditView+SelectSheet.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+SelectSheet.swift @@ -13,14 +13,11 @@ extension BundleEditView { // 배경 시트 VStack(spacing: 0) { Spacer() - HStack(spacing: 8) { - editBackgroundButton - editCarabinerButton - Spacer() - } - .padding(.leading, 18) - .padding(.bottom, 10) - BundleItemCustomSheet( + BundleSheetToggleButtons( + showBackgroundSheet: $showBackgroundSheet, + showCarabinerSheet: $showCarabinerSheet + ) + DraggableSheet( sheetHeight: $sheetHeight, content: SelectBackgroundSheet( viewModel: bundleVM, @@ -32,18 +29,15 @@ extension BundleEditView { ) } .opacity(showBackgroundSheet ? 1 : 0) - + // 카라비너 시트 VStack(spacing: 0) { Spacer() - HStack(spacing: 8) { - editBackgroundButton - editCarabinerButton - Spacer() - } - .padding(.leading, 18) - .padding(.bottom, 10) - BundleItemCustomSheet( + BundleSheetToggleButtons( + showBackgroundSheet: $showBackgroundSheet, + showCarabinerSheet: $showCarabinerSheet + ) + DraggableSheet( sheetHeight: $sheetHeight, content: SelectCarabinerSheet( viewModel: bundleVM, @@ -56,7 +50,6 @@ extension BundleEditView { ) } .opacity(showCarabinerSheet ? 1 : 0) - } } @@ -108,7 +101,7 @@ extension BundleEditView { ScrollView { LazyVGrid(columns: gridColumns, spacing: 10) { ForEach(bundleVM.sortedKeyringsForSelection(selectedKeyrings: bundleVM.selectedKeyrings, selectedPosition: selectedPosition), id: \.self) { keyring in - KeyringSelectableCell( + KeyringCell( keyring: keyring, isSelectedHere: bundleVM.selectedKeyrings[selectedPosition]?.id == keyring.id, isSelectedElsewhere: bundleVM.selectedKeyrings.values.contains { $0.id == keyring.id } && !(bundleVM.selectedKeyrings[selectedPosition]?.id == keyring.id), @@ -155,44 +148,4 @@ extension BundleEditView { } } - // MARK: - 시트 활성화 버튼 - private var editBackgroundButton: some View { - Button { - // 배경 시트 열기 - showBackgroundSheet = true - } label: { - VStack(spacing: 0) { - Image(showBackgroundSheet ? .backgroundIconWhite100 : .backgroundIconGray600) - Text("배경") - .typography(.suit9SB) - .foregroundStyle(showBackgroundSheet ? .white100 : .gray600) - } - .frame(width: 46, height: 46) - .background( - RoundedRectangle(cornerRadius: 14.38) - .fill(showBackgroundSheet ? .main500 : .white100) - ) - } - .buttonStyle(.plain) - } - - private var editCarabinerButton: some View { - Button { - // 카라비너 시트 열기 - showCarabinerSheet = true - } label: { - VStack(spacing: 0) { - Image(showCarabinerSheet ? .carabinerIconWhite100 : .carabinerIconGray600) - Text("카라비너") - .typography(.suit9SB) - .foregroundStyle(showCarabinerSheet ? .white100 : .gray600) - } - .frame(width: 46, height: 46) - .background( - RoundedRectangle(cornerRadius: 14.38) - .fill(showCarabinerSheet ? .main500 : .white100) - ) - } - .buttonStyle(.plain) - } } diff --git a/Keychy/Keychy/Presentation/Bundle/Views/BundleEditView.swift b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView.swift similarity index 94% rename from Keychy/Keychy/Presentation/Bundle/Views/BundleEditView.swift rename to Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView.swift index aca99ffe9..d76a56dd5 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/BundleEditView.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView.swift @@ -20,7 +20,6 @@ struct BundleEditView: View { // MARK: - Loading @State var isSceneReady = false - @State var isPurchasing = false @State var isNavigatingAway = false // 화면 전환 중인지 추적 @State var isKeyringSheetLoading: Bool = true @State var isCapturing: Bool = false @@ -58,7 +57,7 @@ struct BundleEditView: View { let sheetHeightRatio: CGFloat = 0.43 var shouldApplyBlur: Bool { - showPurchaseFailAlert || showPurchaseSuccessAlert || isCapturing || !isSceneReady || isPurchasing || isKeyringSheetLoading + showPurchaseFailAlert || showPurchaseSuccessAlert || isCapturing || !isSceneReady || bundleVM.isPurchasing || isKeyringSheetLoading } var body: some View { @@ -229,7 +228,7 @@ struct BundleEditView: View { let viewX = dx + carabiner.carabiner.keyringXPosition[index] * scale let viewY = dy + carabiner.carabiner.keyringYPosition[index] * scale - CarabinerAddKeyringButton( + AddKeyringButton( isSelected: selectedPosition == index, action: { selectedPosition = index @@ -364,11 +363,8 @@ extension BundleEditView { } } center: { } trailing: { - let hasPayableItems = (bundleVM.newSelectedBackground != nil && !bundleVM.newSelectedBackground!.isOwned && bundleVM.newSelectedBackground!.background.price > 0) || (bundleVM.newSelectedCarabiner != nil && !bundleVM.newSelectedCarabiner!.isOwned && bundleVM.newSelectedCarabiner!.carabiner.price > 0) - - if hasPayableItems { - let payableCount = ((bundleVM.newSelectedBackground != nil && !bundleVM.newSelectedBackground!.isOwned && bundleVM.newSelectedBackground!.background.price > 0) ? 1 : 0) + ((bundleVM.newSelectedCarabiner != nil && !bundleVM.newSelectedCarabiner!.isOwned && bundleVM.newSelectedCarabiner!.carabiner.price > 0) ? 1 : 0) - PurchaseToolbarButton(title: "구매 \(payableCount)") { + if bundleVM.hasUnpurchasedItems { + PurchaseToolbarButton(title: "구매 \(bundleVM.payableItemsCount)") { showPurchaseSheet = true } } else { diff --git a/Keychy/Keychy/Presentation/Bundle/Views/BundleNameEditView.swift b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleNameEditView.swift similarity index 100% rename from Keychy/Keychy/Presentation/Bundle/Views/BundleNameEditView.swift rename to Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleNameEditView.swift diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Component/CarabinerAddKeyringButton.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/AddKeyringButton.swift similarity index 84% rename from Keychy/Keychy/Presentation/Bundle/Views/Component/CarabinerAddKeyringButton.swift rename to Keychy/Keychy/Presentation/Bundle/Views/Shared/AddKeyringButton.swift index 35a4c955f..a5b59331f 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Component/CarabinerAddKeyringButton.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/AddKeyringButton.swift @@ -1,5 +1,5 @@ // -// CarabinerAddKeyringButton.swift +// AddKeyringButton.swift // Keychy // // Created by 김서현 on 10/29/25. @@ -7,7 +7,7 @@ /// 카라비너에 키링 달릴 위치를 표시하는 + 버튼입니다. import SwiftUI -struct CarabinerAddKeyringButton: View { +struct AddKeyringButton: View { var isSelected: Bool var action: () -> Void diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Component/BackgroundSelectableCell.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/BackgroundCell.swift similarity index 98% rename from Keychy/Keychy/Presentation/Bundle/Views/Component/BackgroundSelectableCell.swift rename to Keychy/Keychy/Presentation/Bundle/Views/Shared/BackgroundCell.swift index 94ebe1b2e..2f84112cb 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Component/BackgroundSelectableCell.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/BackgroundCell.swift @@ -8,7 +8,7 @@ import SwiftUI import NukeUI -struct BackgroundSelectableCell: View { +struct BackgroundCell: View { let background: BackgroundViewData let isSelected: Bool diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Component/KeyringBundleItem.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleGridItem.swift similarity index 99% rename from Keychy/Keychy/Presentation/Bundle/Views/Component/KeyringBundleItem.swift rename to Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleGridItem.swift index 38a1914a6..351d2cc8c 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Component/KeyringBundleItem.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleGridItem.swift @@ -1,5 +1,5 @@ // -// KeyringBundleItem.swift +// BundleGridItem.swift // KeytschPrototype // // Created by 김서현 on 10/25/25. @@ -10,7 +10,7 @@ import SwiftUI import SpriteKit import FirebaseFirestore -struct KeyringBundleItem: View { +struct BundleGridItem: View { let bundle: KeyringBundle @State private var cachedImage: Image? @@ -58,7 +58,7 @@ struct KeyringBundleItem: View { } } -extension KeyringBundleItem { +extension BundleGridItem { // MARK: - Bundle Image View private var bundleImageView: some View { diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundlePurchaseCartItem.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundlePurchaseCartItem.swift new file mode 100644 index 000000000..bca5d2969 --- /dev/null +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundlePurchaseCartItem.swift @@ -0,0 +1,42 @@ +// +// BundlePurchaseCartItem.swift +// Keychy +// +// Created by 길지훈 on 2/5/26. +// + +import SwiftUI + +/// 구매 시트에서 사용하는 장바구니 아이템 행 +struct BundlePurchaseCartItem: View { + let name: String + let type: String + let price: Int + + var body: some View { + HStack(spacing: 6) { + Image(.selectedIcon) + + Text(name) + .typography(.suit16B) + .foregroundStyle(.black100) + .padding(.trailing, 7) + + Text(type) + .typography(.suit13M) + .foregroundStyle(.gray400) + + Spacer() + + Text("\(price)") + .typography(.nanum16EB) + .foregroundStyle(.main500) + } + .padding(.vertical, 15) + .padding(.horizontal, 16) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(.gray50) + ) + } +} diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleSheetToggleButtons.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleSheetToggleButtons.swift new file mode 100644 index 000000000..1f6b87e83 --- /dev/null +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleSheetToggleButtons.swift @@ -0,0 +1,62 @@ +// +// BundleSheetToggleButtons.swift +// Keychy +// +// Created by 길지훈 on 2/5/26. +// + +import SwiftUI + +/// 뭉치 생성/편집 화면에서 배경/카라비너 시트를 토글하는 버튼 컴포넌트 +struct BundleSheetToggleButtons: View { + @Binding var showBackgroundSheet: Bool + @Binding var showCarabinerSheet: Bool + + var body: some View { + HStack(spacing: 8) { + backgroundButton + carabinerButton + Spacer() + } + .padding(.leading, 18) + .padding(.bottom, 10) + } + + private var backgroundButton: some View { + Button { + showBackgroundSheet = true + } label: { + VStack(spacing: 0) { + Image(showBackgroundSheet ? .backgroundIconWhite100 : .backgroundIconGray600) + Text("배경") + .typography(.suit9SB) + .foregroundStyle(showBackgroundSheet ? .white100 : .gray600) + } + .frame(width: 46, height: 46) + .background( + RoundedRectangle(cornerRadius: 14.38) + .fill(showBackgroundSheet ? .main500 : .white100) + ) + } + .buttonStyle(.plain) + } + + private var carabinerButton: some View { + Button { + showCarabinerSheet = true + } label: { + VStack(spacing: 0) { + Image(showCarabinerSheet ? .carabinerIconWhite100 : .carabinerIconGray600) + Text("카라비너") + .typography(.suit9SB) + .foregroundStyle(showCarabinerSheet ? .white100 : .gray600) + } + .frame(width: 46, height: 46) + .background( + RoundedRectangle(cornerRadius: 14.38) + .fill(showCarabinerSheet ? .main500 : .white100) + ) + } + .buttonStyle(.plain) + } +} diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Component/CarabinerSelectableCell.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/CarabinerCell.swift similarity index 98% rename from Keychy/Keychy/Presentation/Bundle/Views/Component/CarabinerSelectableCell.swift rename to Keychy/Keychy/Presentation/Bundle/Views/Shared/CarabinerCell.swift index 4f88e2816..5e34e92e2 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Component/CarabinerSelectableCell.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/CarabinerCell.swift @@ -8,7 +8,7 @@ import SwiftUI import NukeUI -struct CarabinerSelectableCell: View { +struct CarabinerCell: View { var carabiner: CarabinerViewData var isSelected: Bool diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Component/CarabinerChangePopup.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/CarabinerPopup.swift similarity index 97% rename from Keychy/Keychy/Presentation/Bundle/Views/Component/CarabinerChangePopup.swift rename to Keychy/Keychy/Presentation/Bundle/Views/Shared/CarabinerPopup.swift index 00abce227..8dbef64dd 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Component/CarabinerChangePopup.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/CarabinerPopup.swift @@ -1,5 +1,5 @@ // -// CarabinerChangePopup.swift +// CarabinerPopup.swift // Keychy // // Created by 김서현 on 11/12/25. @@ -7,7 +7,7 @@ import SwiftUI -struct CarabinerChangePopup: View { +struct CarabinerPopup: View { let title: String let message: String let onCancel: () -> Void diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Component/BundleItemCustomSheet.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/DraggableSheet.swift similarity index 97% rename from Keychy/Keychy/Presentation/Bundle/Views/Component/BundleItemCustomSheet.swift rename to Keychy/Keychy/Presentation/Bundle/Views/Shared/DraggableSheet.swift index e1178ec7a..1dfe5839a 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Component/BundleItemCustomSheet.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/DraggableSheet.swift @@ -1,5 +1,5 @@ // -// BundleItemCustomSheet.swift +// DraggableSheet.swift // Keychy // // Created by 김서현 on 11/12/25. @@ -7,7 +7,7 @@ import SwiftUI -struct BundleItemCustomSheet: View { +struct DraggableSheet: View { @Binding var sheetHeight: CGFloat let content: Content diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Component/KeyringSelectableCell.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/KeyringCell.swift similarity index 97% rename from Keychy/Keychy/Presentation/Bundle/Views/Component/KeyringSelectableCell.swift rename to Keychy/Keychy/Presentation/Bundle/Views/Shared/KeyringCell.swift index 5f3bc15a3..e06576092 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Component/KeyringSelectableCell.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/KeyringCell.swift @@ -1,5 +1,5 @@ // -// KeyringSelectableCell.swift +// KeyringCell.swift // Keychy // // Created by 김서현 on 01/13/25. @@ -7,7 +7,7 @@ import SwiftUI -struct KeyringSelectableCell: View { +struct KeyringCell: View { let keyring: Keyring let isSelectedHere: Bool let isSelectedElsewhere: Bool diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Component/SelectBackgroundSheetContent.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/SelectBackgroundSheet.swift similarity index 95% rename from Keychy/Keychy/Presentation/Bundle/Views/Component/SelectBackgroundSheetContent.swift rename to Keychy/Keychy/Presentation/Bundle/Views/Shared/SelectBackgroundSheet.swift index 52a9c66dc..427f6ef9e 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Component/SelectBackgroundSheetContent.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/SelectBackgroundSheet.swift @@ -38,7 +38,7 @@ struct SelectBackgroundSheet: View { var body: some View { LazyVGrid(columns: gridColumns, spacing: 10) { ForEach(sortedBackgrounds) { bg in - BackgroundSelectableCell(background: bg, isSelected: (bg == selectedBG)) + BackgroundCell(background: bg, isSelected: (bg == selectedBG)) .onTapGesture { onBackgroundTap(bg) diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Component/SelectCarabinerSheetContent.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/SelectCarabinerSheet.swift similarity index 94% rename from Keychy/Keychy/Presentation/Bundle/Views/Component/SelectCarabinerSheetContent.swift rename to Keychy/Keychy/Presentation/Bundle/Views/Shared/SelectCarabinerSheet.swift index b7a2f21ce..5826b45f0 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Component/SelectCarabinerSheetContent.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/SelectCarabinerSheet.swift @@ -38,7 +38,7 @@ struct SelectCarabinerSheet: View { var body: some View { LazyVGrid(columns: gridColumns, spacing: 10) { ForEach(sortedCarabiners) { cb in - CarabinerSelectableCell(carabiner: cb, isSelected: (selectedCarabiner == cb)) + CarabinerCell(carabiner: cb, isSelected: (selectedCarabiner == cb)) .onTapGesture { onCarabinerTap(cb) diff --git a/Keychy/Keychy/Presentation/Home/ViewModels/HomeViewModel.swift b/Keychy/Keychy/Presentation/Home/ViewModels/HomeViewModel.swift index 974db5f3f..56a7888a9 100644 --- a/Keychy/Keychy/Presentation/Home/ViewModels/HomeViewModel.swift +++ b/Keychy/Keychy/Presentation/Home/ViewModels/HomeViewModel.swift @@ -20,6 +20,9 @@ class HomeViewModel { /// 데이터 로드 완료 여부 var isDataLoaded = false + + /// 마지막으로 로드한 뭉치 ID (뭉치 변경 감지용) + private var lastLoadedBundleId: String? /// 네트워크 에러 발생 여부 var hasNetworkError: Bool = false @@ -37,8 +40,10 @@ class HomeViewModel { @MainActor func loadMainBundle(collectionViewModel: CollectionViewModel, bundleViewModel: BundleViewModel, onBackgroundLoaded: (() -> Void)?) async { - // 이미 데이터가 로드되었고 선택된 뭉치가 있으면 스킵 (탭 전환 후 돌아올 때) - if isDataLoaded && bundleViewModel.selectedBundle != nil { + // 이미 데이터가 로드되었고, 같은 뭉치가 선택된 상태면 스킵 (탭 전환 후 돌아올 때) + if isDataLoaded, + let currentBundle = bundleViewModel.selectedBundle, + lastLoadedBundleId == currentBundle.documentId { return } @@ -95,8 +100,9 @@ class HomeViewModel { // 5. 키링 데이터 생성 guard let carabiner = bundleViewModel.selectedCarabiner else { return } keyringDataList = await createKeyringDataList(bundle: bundle, carabiner: carabiner) - + // 데이터 로드 완료 표시 + lastLoadedBundleId = bundle.documentId isDataLoaded = true } @@ -292,6 +298,7 @@ class HomeViewModel { keyringDataList = newKeyringDataList // 데이터 로드 완료 표시 + lastLoadedBundleId = bundle.documentId isDataLoaded = true } diff --git a/Keychy/Keychy/Presentation/Tab/ViewModels/MainTabViewModel.swift b/Keychy/Keychy/Presentation/Tab/ViewModels/MainTabViewModel.swift index b8aaf9bef..5283667c2 100644 --- a/Keychy/Keychy/Presentation/Tab/ViewModels/MainTabViewModel.swift +++ b/Keychy/Keychy/Presentation/Tab/ViewModels/MainTabViewModel.swift @@ -48,6 +48,7 @@ class MainTabViewModel { // ViewModels let collectionViewModel = CollectionViewModel() + let bundleViewModel = BundleViewModel() // Managers let userManager = UserManager.shared diff --git a/Keychy/Keychy/Presentation/Tab/Views/CollectionTab.swift b/Keychy/Keychy/Presentation/Tab/Views/CollectionTab.swift index fccdb35a8..216db8714 100644 --- a/Keychy/Keychy/Presentation/Tab/Views/CollectionTab.swift +++ b/Keychy/Keychy/Presentation/Tab/Views/CollectionTab.swift @@ -9,8 +9,8 @@ import SwiftUI struct CollectionTab: View { @Bindable var router: NavigationRouter - @State private var collectionViewModel = CollectionViewModel() - @State private var bundleViewModel = BundleViewModel() + @Bindable var bundleViewModel: BundleViewModel + @Bindable var collectionViewModel: CollectionViewModel @Binding var shouldRefresh: Bool var body: some View { diff --git a/Keychy/Keychy/Presentation/Tab/Views/HomeTab.swift b/Keychy/Keychy/Presentation/Tab/Views/HomeTab.swift index 1b11cdcdf..4f938e6d8 100644 --- a/Keychy/Keychy/Presentation/Tab/Views/HomeTab.swift +++ b/Keychy/Keychy/Presentation/Tab/Views/HomeTab.swift @@ -10,8 +10,8 @@ import SwiftUI struct HomeTab: View { @Bindable var router: NavigationRouter @Bindable var userManager: UserManager - @State private var collectionViewModel = CollectionViewModel() - @State private var bundleViewModel = BundleViewModel() + @Bindable var bundleViewModel: BundleViewModel + @Bindable var collectionViewModel: CollectionViewModel @Bindable private var introViewModel = IntroViewModel() @State private var festivalViewModel = Showcase25BoardViewModel() diff --git a/Keychy/Keychy/Presentation/Tab/Views/MainTabView.swift b/Keychy/Keychy/Presentation/Tab/Views/MainTabView.swift index a9d61c57c..6c600070c 100644 --- a/Keychy/Keychy/Presentation/Tab/Views/MainTabView.swift +++ b/Keychy/Keychy/Presentation/Tab/Views/MainTabView.swift @@ -80,6 +80,8 @@ extension MainTabView { HomeTab( router: viewModel.homeRouter, userManager: viewModel.userManager, + bundleViewModel: viewModel.bundleViewModel, + collectionViewModel: viewModel.collectionViewModel, onBackgroundLoaded: { DispatchQueue.main.asyncAfter(deadline: .now() + MainTabViewModel.Delay.splashAnimation) { withAnimation(.easeOut(duration: 0.5)) { @@ -96,7 +98,7 @@ extension MainTabView { } private var workshopTab: some View { - WorkshopTab(router: viewModel.workshopRouter) + WorkshopTab(router: viewModel.workshopRouter, bundleViewModel: viewModel.bundleViewModel, collectionViewModel: viewModel.collectionViewModel) .modifier(TabItemModifier( image: .workshop, title: "공방", @@ -107,6 +109,8 @@ extension MainTabView { private var collectionTab: some View { CollectionTab( router: viewModel.collectionRouter, + bundleViewModel: viewModel.bundleViewModel, + collectionViewModel: viewModel.collectionViewModel, shouldRefresh: $viewModel.shouldRefreshCollection ) .modifier(TabItemModifier( diff --git a/Keychy/Keychy/Presentation/Tab/Views/WorkshopTab.swift b/Keychy/Keychy/Presentation/Tab/Views/WorkshopTab.swift index ca8a6f9a0..c50b6fd17 100644 --- a/Keychy/Keychy/Presentation/Tab/Views/WorkshopTab.swift +++ b/Keychy/Keychy/Presentation/Tab/Views/WorkshopTab.swift @@ -9,6 +9,8 @@ import SwiftUI struct WorkshopTab: View { @Bindable var router: NavigationRouter + @Bindable var bundleViewModel: BundleViewModel + @Bindable var collectionViewModel: CollectionViewModel @State private var acrylicPhotoVM: AcrylicPhotoVM? @State private var neonSignVM: NeonSignVM? @@ -18,10 +20,6 @@ struct WorkshopTab: View { @State private var speechBubbleVM: SpeechBubbleVM? @State private var workshopViewModel = WorkshopViewModel(userManager: UserManager.shared) - // Bundle 관련 ViewModel - @State private var collectionViewModel = CollectionViewModel() - @State private var bundleViewModel = BundleViewModel() - var body: some View { NavigationStack(path: $router.path) { WorkshopView(