From a4a0659f50253c37ffda67e512d3d0e613544acd Mon Sep 17 00:00:00 2001 From: giljihun Date: Fri, 6 Feb 2026 11:34:12 +0900 Subject: [PATCH 01/14] =?UTF-8?q?feat:=20=ED=99=88=20-=20=EB=AD=89?= =?UTF-8?q?=EC=B9=98=EB=B3=80=EA=B2=BD=20=EB=93=9C=EB=A1=AD=EB=A9=94?= =?UTF-8?q?=EB=89=B4=20=EC=8A=A4=ED=81=AC=EB=A1=A4=EB=9F=AC=EB=B8=94,=20?= =?UTF-8?q?=ED=83=AD=20=EC=A0=84=ED=99=98=20=EC=8B=9C=20dismiss?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../View/Popup/BundleSwitchPopup.swift | 30 +++++++++++-------- .../Home/Views/Main/HomeView.swift | 11 +++++++ .../Presentation/Tab/Views/HomeTab.swift | 5 +++- .../Presentation/Tab/Views/MainTabView.swift | 1 + 4 files changed, 33 insertions(+), 14 deletions(-) diff --git a/Keychy/Keychy/Core/Components/View/Popup/BundleSwitchPopup.swift b/Keychy/Keychy/Core/Components/View/Popup/BundleSwitchPopup.swift index 5a1950af..7fb780ba 100644 --- a/Keychy/Keychy/Core/Components/View/Popup/BundleSwitchPopup.swift +++ b/Keychy/Keychy/Core/Components/View/Popup/BundleSwitchPopup.swift @@ -24,24 +24,28 @@ struct BundleSwitchPopup: View { } var body: some View { - VStack(alignment: .leading, spacing: 0) { - // 대표 섹션 - if let main = mainBundle { - mainSection(bundle: main) - } + ScrollView(showsIndicators: false) { + VStack(alignment: .leading, spacing: 0) { + // 대표 섹션 + if let main = mainBundle { + mainSection(bundle: main) + } - // 구분선 - Rectangle() - .fill(.gray100) - .frame(height: 1) - .padding(.horizontal, 18) + // 구분선 + Rectangle() + .fill(.gray100) + .frame(height: 1) + .padding(.horizontal, 18) - // 선택 섹션 - if !selectableBundles.isEmpty { - selectSection(bundles: selectableBundles) + // 선택 섹션 + if !selectableBundles.isEmpty { + selectSection(bundles: selectableBundles) + } } } + .scrollBounceBehavior(.basedOnSize) .frame(width: 196) + .frame(maxHeight: 270) .padding(.vertical, 5) .glassEffect(.regular.interactive(), in: .rect(cornerRadius: 34)) } diff --git a/Keychy/Keychy/Presentation/Home/Views/Main/HomeView.swift b/Keychy/Keychy/Presentation/Home/Views/Main/HomeView.swift index bb3f36f3..a2d56633 100644 --- a/Keychy/Keychy/Presentation/Home/Views/Main/HomeView.swift +++ b/Keychy/Keychy/Presentation/Home/Views/Main/HomeView.swift @@ -18,6 +18,9 @@ struct HomeView: View { @State var collectionViewModel: CollectionViewModel @State var bundleViewModel: BundleViewModel + /// 탭 선택 상태 (탭 전환 감지용) + @Binding var selectedTab: Int + /// 배경 로드 완료 콜백 var onBackgroundLoaded: (() -> Void)? = nil @@ -121,6 +124,14 @@ struct HomeView: View { } } } + .onChange(of: selectedTab) { _, _ in + // 탭 전환 시 뭉치 변경 팝업 닫기 + if showBundleSwitchPopup { + withAnimation(.spring(response: 0.35, dampingFraction: 0.8)) { + showBundleSwitchPopup = false + } + } + } .withToast(position: .tabbar) } } diff --git a/Keychy/Keychy/Presentation/Tab/Views/HomeTab.swift b/Keychy/Keychy/Presentation/Tab/Views/HomeTab.swift index 4f938e6d..3631d87a 100644 --- a/Keychy/Keychy/Presentation/Tab/Views/HomeTab.swift +++ b/Keychy/Keychy/Presentation/Tab/Views/HomeTab.swift @@ -12,6 +12,7 @@ struct HomeTab: View { @Bindable var userManager: UserManager @Bindable var bundleViewModel: BundleViewModel @Bindable var collectionViewModel: CollectionViewModel + @Binding var selectedTab: Int @Bindable private var introViewModel = IntroViewModel() @State private var festivalViewModel = Showcase25BoardViewModel() @@ -23,7 +24,9 @@ struct HomeTab: View { HomeView( router: router, userManager: userManager, - collectionViewModel: collectionViewModel, bundleViewModel: bundleViewModel, + collectionViewModel: collectionViewModel, + bundleViewModel: bundleViewModel, + selectedTab: $selectedTab, onBackgroundLoaded: onBackgroundLoaded ) .navigationDestination(for: HomeRoute.self) {route in diff --git a/Keychy/Keychy/Presentation/Tab/Views/MainTabView.swift b/Keychy/Keychy/Presentation/Tab/Views/MainTabView.swift index 6c600070..5539bcdb 100644 --- a/Keychy/Keychy/Presentation/Tab/Views/MainTabView.swift +++ b/Keychy/Keychy/Presentation/Tab/Views/MainTabView.swift @@ -82,6 +82,7 @@ extension MainTabView { userManager: viewModel.userManager, bundleViewModel: viewModel.bundleViewModel, collectionViewModel: viewModel.collectionViewModel, + selectedTab: $viewModel.selectedTab, onBackgroundLoaded: { DispatchQueue.main.asyncAfter(deadline: .now() + MainTabViewModel.Delay.splashAnimation) { withAnimation(.easeOut(duration: 0.5)) { From 4c91baf4d07a2a0a2d0ba6febb8b5be13849ab53 Mon Sep 17 00:00:00 2001 From: giljihun Date: Fri, 6 Feb 2026 13:54:11 +0900 Subject: [PATCH 02/14] =?UTF-8?q?refactor:=20=EB=B0=B0=EA=B2=BD/=EC=B9=B4?= =?UTF-8?q?=EB=9D=BC=EB=B9=84=EB=84=88=20=EC=8B=9C=ED=8A=B8=EB=A5=BC=20?= =?UTF-8?q?=EB=8B=A8=EC=9D=BC=20=EC=8B=9C=ED=8A=B8=EB=A1=9C=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - showBackgroundSheet, showCarabinerSheet → showItemSheet + isBackgroundMode - BundleSheetToggleButtons 컴포넌트 리팩토링 --- .../Views/Create/BundleCreateView.swift | 486 ++++++++++++++---- .../Edit/BundleEditView+SelectSheet.swift | 82 +-- .../Bundle/Views/Edit/BundleEditView.swift | 20 +- .../Shared/BundleSheetToggleButtons.swift | 47 +- 4 files changed, 469 insertions(+), 166 deletions(-) diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift index 831ddaf9..fc393c46 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift @@ -18,13 +18,22 @@ struct BundleCreateView: View { @Bindable var bundleVM: BundleViewModel // 시트 활성화 상태 - @State private var showBackgroundSheet: Bool = false - @State private var showCarabinerSheet: Bool = false - - // 임시 초기값 + @State private var showItemSheet: Bool = false + @State private var isBackgroundMode: Bool = true // true: 배경, false: 카라비너 + @State private var showKeyringSheet: Bool = false + + // 시트 높이 @State private var sheetHeight: CGFloat = 360 - private let sheetHeightRatio: CGFloat = 0.5 - + + // 키링 선택 상태 + @State private var selectedKeyrings: [Int: Keyring] = [:] + @State private var keyringOrder: [Int] = [] + @State private var selectedPosition: Int = 0 + + // 캡처 상태 + @State private var isCapturing: Bool = false + @State private var sceneRefreshId = UUID() + // 구매 시트 @State var showPurchaseSheet = false @@ -33,62 +42,75 @@ struct BundleCreateView: View { @State var purchasesSuccessScale: CGFloat = 0.3 @State var showPurchaseFailAlert = false @State var purchaseFailScale: CGFloat = 0.3 - - + // 공통 그리드 컬럼 (배경, 카라비너, 키링 모두 동일) private let gridColumns: [GridItem] = [ GridItem(.flexible(), spacing: 10), GridItem(.flexible(), spacing: 10), GridItem(.flexible(), spacing: 10) ] + + private let sheetHeightRatio: CGFloat = 0.5 //MARK: 메인 뷰 var body: some View { ZStack(alignment: .bottom) { if let bg = bundleVM.newSelectedBackground, let cb = bundleVM.newSelectedCarabiner { - // 배경과 카라비너만 보여줌 - MultiKeyringSceneView( - keyringDataList: [], - ringType: .basic, - chainType: .basic, - backgroundColor: .clear, - backgroundImageURL: bg.background.backgroundImage, - carabinerBackImageURL: cb.carabiner.backImageURL, - carabinerFrontImageURL: cb.carabiner.frontImageURL, - carabinerX: cb.carabiner.carabinerX, - carabinerY: cb.carabiner.carabinerY, - carabinerWidth: cb.carabiner.carabinerWidth, - currentCarabinerType: cb.carabiner.type - ) - .id("scene_\(bg.background.id ?? "bg")_\(cb.carabiner.id ?? "cb")") - .blur(radius: showPurchaseSuccessAlert ? 10 : 0) - - sheetContent() - .blur(radius: showPurchaseSuccessAlert ? 10 : 0) - + // 배경 + 카라비너 + 키링 씬 + ZStack { + MultiKeyringSceneView( + keyringDataList: createKeyringDataList(carabiner: cb.carabiner), + ringType: .basic, + chainType: .basic, + backgroundColor: .clear, + backgroundImageURL: bg.background.backgroundImage, + carabinerBackImageURL: cb.carabiner.backImageURL, + carabinerFrontImageURL: cb.carabiner.frontImageURL, + carabinerX: cb.carabiner.carabinerX, + carabinerY: cb.carabiner.carabinerY, + carabinerWidth: cb.carabiner.carabinerWidth, + currentCarabinerType: cb.carabiner.type + ) + .id("scene_\(bg.background.id ?? "bg")_\(cb.carabiner.id ?? "cb")_\(selectedKeyrings.count)_\(sceneRefreshId.uuidString)") + + // 키링 추가 + 버튼들 + keyringButtons(carabiner: cb.carabiner) + } + .blur(radius: showPurchaseSuccessAlert || isCapturing ? 10 : 0) + + // 하단 셀렉터 + 시트 + sheetContent + .blur(radius: showPurchaseSuccessAlert || isCapturing ? 10 : 0) + customNavigationBar - .blur(radius: showPurchaseSuccessAlert ? 10 : 0) + .blur(radius: showPurchaseSuccessAlert || isCapturing ? 10 : 0) } - - - // Alert들, 컨텐츠가 화면의 중앙에 오도록 함 + + // 캡처 중 로딩 + if isCapturing { + Color.black20 + .ignoresSafeArea() + LoadingAlert(type: .longWithKeychy, message: "뭉치 만드는 중...") + } + + // Alert들 alertContent .position(x: screenWidth / 2, y: screenHeight / 2) - + + // 구매 시트 오버레이 ZStack { Color.black20 .ignoresSafeArea() .zIndex(10) - // 구매 시트 - 화면 맨 밑에 표시 - VStack { - Spacer() - purchaseSheetView - } - .zIndex(100) - .ignoresSafeArea() - .transition(.move(edge: .bottom)) - .animation(.easeInOut(duration: 0.3), value: showPurchaseSheet) + VStack { + Spacer() + purchaseSheetView + } + .zIndex(100) + .ignoresSafeArea() + .transition(.move(edge: .bottom)) + .animation(.easeInOut(duration: 0.3), value: showPurchaseSheet) } .opacity(showPurchaseSheet ? 1 : 0) .blur(radius: showPurchaseSuccessAlert ? 10 : 0) @@ -99,26 +121,13 @@ struct BundleCreateView: View { await initializeData() } .onAppear { - // 화면이 나타날 때마다 데이터 새로고침 Task { await refreshData() } TabBarManager.hide() - // 화면 첫 진입 시 배경 시트를 보여줌 - if !showBackgroundSheet && !showCarabinerSheet { - showBackgroundSheet = true - } } - // 선택 타입이 배경화면이면 카라비너 시트는 닫고, 카라비너 열리면 배경화면은 닫힘 - .onChange(of: showBackgroundSheet) { oldValue, newValue in - if newValue { - showCarabinerSheet = false - } - } - .onChange(of: showCarabinerSheet) { oldValue, newValue in - if newValue { - showBackgroundSheet = false - } + .sheet(isPresented: $showKeyringSheet) { + keyringSheetContent } } } @@ -138,67 +147,199 @@ extension BundleCreateView { } } else { NextToolbarButton { - // ViewModel 상태를 selectedBackground/selectedCarabiner로도 동기화 - if let bg = bundleVM.newSelectedBackground { - bundleVM.selectedBackground = bg.background - } - if let cb = bundleVM.newSelectedCarabiner { - bundleVM.selectedCarabiner = cb.carabiner + Task { + await captureAndSaveScene() } - router.push(.bundleAddKeyringView) } + .disabled(isCapturing) } } - } } -//MARK: - 시트 뷰 + +// MARK: - 키링 + 버튼 +extension BundleCreateView { + private func keyringButtons(carabiner: Carabiner) -> some View { + GeometryReader { geometry in + let sceneWidth: CGFloat = 402 + let sceneHeight: CGFloat = 874 + let scale = max(geometry.size.width / sceneWidth, geometry.size.height / sceneHeight) + + let contentW = sceneWidth * scale + let contentH = sceneHeight * scale + + let dx = (geometry.size.width - contentW) / 2 + let dy = (geometry.size.height - contentH) / 2 + + ForEach(0.. some View { - Group { - // 배경 시트 - if showBackgroundSheet { + private var sheetContent: some View { + ZStack(alignment: .bottom) { + Color.clear + + if showItemSheet { + // 시트가 있을 때: 셀렉터 + 시트가 함께 움직임 VStack(spacing: 0) { - Spacer() BundleSheetToggleButtons( - showBackgroundSheet: $showBackgroundSheet, - showCarabinerSheet: $showCarabinerSheet + showItemSheet: $showItemSheet, + isBackgroundMode: $isBackgroundMode ) + .padding(.bottom, 10) + DraggableSheet( sheetHeight: $sheetHeight, - content: SelectBackgroundSheet( - viewModel: bundleVM, - selectedBG: bundleVM.newSelectedBackground, - onBackgroundTap: { bg in - bundleVM.newSelectedBackground = bg - } - ) + content: itemSheetContent, + onDismiss: { + showItemSheet = false + } ) } + .transition(.move(edge: .bottom)) + } else { + // 시트가 없을 때: 셀렉터만 하단에 고정 + BundleSheetToggleButtons( + showItemSheet: $showItemSheet, + isBackgroundMode: $isBackgroundMode + ) + .padding(.bottom, 50) + .transition(.identity) } + } + .animation(.easeInOut(duration: 0.25), value: showItemSheet) + } - // 카라비너 시트 - if showCarabinerSheet { - VStack(spacing: 0) { - Spacer() - BundleSheetToggleButtons( - showBackgroundSheet: $showBackgroundSheet, - showCarabinerSheet: $showCarabinerSheet - ) - DraggableSheet( - sheetHeight: $sheetHeight, - content: SelectCarabinerSheet( - viewModel: bundleVM, - selectedCarabiner: bundleVM.newSelectedCarabiner, - onCarabinerTap: { carabiner in - bundleVM.newSelectedCarabiner = carabiner - } - ) - ) + @ViewBuilder + private var itemSheetContent: some View { + if isBackgroundMode { + SelectBackgroundSheet( + viewModel: bundleVM, + selectedBG: bundleVM.newSelectedBackground, + onBackgroundTap: { bg in + bundleVM.newSelectedBackground = bg + } + ) + } else { + SelectCarabinerSheet( + viewModel: bundleVM, + selectedCarabiner: bundleVM.newSelectedCarabiner, + onCarabinerTap: { carabiner in + bundleVM.newSelectedCarabiner = carabiner + } + ) + } + } + + private var keyringSheetContent: some View { + VStack(spacing: 18) { + Text("키링 선택") + .typography(.suit16B) + .foregroundStyle(.black100) + .padding(.top, 20) + + if bundleVM.keyring.isEmpty { + VStack { + Image(.emptyViewIcon) + .resizable() + .scaledToFit() + .frame(height: 77) + Text("공방에서 키링을 만들 수 있어요") + .typography(.suit15R) + .foregroundStyle(.black100) + .padding(.vertical, 15) + } + .padding(.bottom, 77) + .padding(.top, 62) + .frame(maxWidth: .infinity) + } else { + ScrollView { + LazyVGrid(columns: gridColumns, spacing: 10) { + ForEach(bundleVM.sortedKeyringsForSelection(selectedKeyrings: selectedKeyrings, selectedPosition: selectedPosition), id: \.self) { keyring in + keyringCell(keyring: keyring) + } + } + .padding(.horizontal, 20) } } } + .presentationDetents([.fraction(0.45), .fraction(0.85)]) + .presentationDragIndicator(.visible) + } + + private func keyringCell(keyring: Keyring) -> some View { + let isSelectedHere = selectedKeyrings[selectedPosition]?.id == keyring.id + let isSelectedElsewhere = selectedKeyrings.values.contains { $0.id == keyring.id } && !isSelectedHere + + return Button { + if isSelectedHere { + selectedKeyrings[selectedPosition] = nil + keyringOrder.removeAll { $0 == selectedPosition } + } else if !isSelectedElsewhere { + if selectedKeyrings[selectedPosition] != nil { + keyringOrder.removeAll { $0 == selectedPosition } + } + selectedKeyrings[selectedPosition] = keyring + keyringOrder.append(selectedPosition) + showKeyringSheet = false + } + sceneRefreshId = UUID() + } label: { + ZStack(alignment: .topTrailing) { + VStack(spacing: 10) { + ZStack { + CollectionCellView(keyring: keyring) + .frame(width: threeGridCellWidth, height: threeGridCellHeight) + .cornerRadius(10) + + RoundedRectangle(cornerRadius: 10) + .strokeBorder(isSelectedHere ? .mainOpacity80 : .clear, lineWidth: 1.8) + .frame(width: threeGridCellWidth, height: threeGridCellHeight) + + if isSelectedElsewhere { + RoundedRectangle(cornerRadius: 10) + .fill(.black50) + .frame(width: threeGridCellWidth, height: threeGridCellHeight) + } + } + + Text(keyring.name) + .typography(isSelectedHere ? .notosans14SB : .notosans14M) + .foregroundStyle(isSelectedHere ? .main500 : .black100) + .lineLimit(1) + .truncationMode(.tail) + } + + if isSelectedElsewhere || isSelectedHere { + Text("장착 중") + .foregroundStyle(.white100) + .typography(.suit13M) + .padding(.horizontal, 10) + .padding(.vertical, 4) + .background(RoundedRectangle(cornerRadius: 20).fill(.mainOpacity80)) + .padding(.top, 5) + .padding(.trailing, 5) + } + } + } + .disabled(keyring.status == .packaged || keyring.status == .published || isSelectedElsewhere) } } @@ -251,7 +392,9 @@ extension BundleCreateView { guard let _ = UserManager.shared.currentUser else { return } - + + let uid = UserManager.shared.userUID + // 배경 데이터 로드 await withCheckedContinuation { continuation in bundleVM.fetchAllBackgrounds { _ in @@ -270,7 +413,6 @@ extension BundleCreateView { } ?? bundleVM.backgroundViewData.first } } - continuation.resume() } } @@ -293,10 +435,25 @@ extension BundleCreateView { } ?? bundleVM.carabinerViewData.first } } - continuation.resume() } } + + // 키링 데이터 로드 + await withCheckedContinuation { continuation in + collectionVM.fetchUserCollectionData(uid: uid) { success in + if success { + collectionVM.fetchUserKeyrings(uid: uid) { success in + if success { + bundleVM.keyring = collectionVM.keyring + } + continuation.resume() + } + } else { + continuation.resume() + } + } + } } } @@ -311,13 +468,12 @@ extension BundleCreateView { .onTapGesture { showPurchaseSuccessAlert = false purchasesSuccessScale = 0.3 - router.push(.bundleAddKeyringView) } - + KeychyAlert(type: .checkmark, message: "구매가 완료되었어요!", isPresented: $showPurchaseSuccessAlert) .zIndex(101) } - + // 구매 실패 Alert if showPurchaseFailAlert { ZStack { @@ -327,7 +483,7 @@ extension BundleCreateView { showPurchaseFailAlert = false purchaseFailScale = 0.3 } - + PurchaseFailAlert( checkmarkScale: purchaseFailScale, onCancel: { @@ -485,3 +641,123 @@ extension BundleCreateView { } } +// MARK: - 키링 데이터 및 캡처 +extension BundleCreateView { + /// 키링 데이터 리스트 생성 (씬 표시용) + private func createKeyringDataList(carabiner: Carabiner) -> [MultiKeyringScene.KeyringData] { + var dataList: [MultiKeyringScene.KeyringData] = [] + + for index in keyringOrder { + guard let keyring = selectedKeyrings[index] else { continue } + let soundId = keyring.soundId + + 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] + ) + + let data = MultiKeyringScene.KeyringData( + index: index, + position: position, + bodyImageURL: keyring.bodyImage, + templateId: keyring.selectedTemplate, + soundId: soundId, + customSoundURL: customSoundURL, + particleId: particleId, + hookOffsetY: keyring.hookOffsetY, + chainLength: keyring.chainLength + ) + dataList.append(data) + } + + return dataList + } + + /// 씬 캡처 및 저장 + private func captureAndSaveScene() async { + guard let cb = bundleVM.newSelectedCarabiner, + let bg = bundleVM.newSelectedBackground else { + return + } + + let carabiner = cb.carabiner + let background = bg.background + + // 캡처 시작 + await MainActor.run { + isCapturing = true + bundleVM.selectedKeyringsForBundle = selectedKeyrings + bundleVM.selectedBackground = background + bundleVM.selectedCarabiner = carabiner + } + + // 배경 이미지 미리 로드 + guard let _ = try? await StorageManager.shared.getImage(path: background.backgroundImage) else { + await MainActor.run { + isCapturing = false + } + return + } + + // 캡처용 키링 데이터 생성 + var keyringDataList: [MultiKeyringCaptureScene.KeyringData] = [] + + for (index, keyring) in selectedKeyrings.sorted(by: { $0.key < $1.key }) { + let data = MultiKeyringCaptureScene.KeyringData( + index: index, + position: CGPoint( + x: carabiner.keyringXPosition[index], + y: carabiner.keyringYPosition[index] + ), + bodyImageURL: keyring.bodyImage, + hookOffsetY: keyring.hookOffsetY, + chainLength: keyring.chainLength + ) + keyringDataList.append(data) + } + + // 카라비너 이미지 추출 + let carabinerType = CarabinerType.from(carabiner.carabinerType) + let carabinerBackURL: String? + let carabinerFrontURL: String? + + if carabinerType == .hamburger { + carabinerBackURL = carabiner.carabinerImage[1] + carabinerFrontURL = carabiner.carabinerImage[2] + } else { + carabinerBackURL = carabiner.carabinerImage[0] + carabinerFrontURL = nil + } + + // 씬 캡처 + if let pngData = await MultiKeyringCaptureScene.captureBundleImage( + keyringDataList: keyringDataList, + backgroundImageURL: background.backgroundImage, + carabinerBackImageURL: carabinerBackURL, + carabinerFrontImageURL: carabinerFrontURL, + carabinerType: carabinerType, + carabinerX: carabiner.carabinerX, + carabinerY: carabiner.carabinerY, + carabinerWidth: carabiner.carabinerWidth + ) { + await MainActor.run { + bundleVM.bundleCapturedImage = pngData + } + } + + // 캡처 완료 후 다음 화면으로 이동 + await MainActor.run { + isCapturing = false + router.push(.bundleNameInputView) + } + } +} + diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+SelectSheet.swift b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+SelectSheet.swift index 4ef7b933..ddbd5745 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+SelectSheet.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+SelectSheet.swift @@ -9,47 +9,59 @@ import SwiftUI extension BundleEditView { var selectItemSheetContent: some View { - Group { - // 배경 시트 - VStack(spacing: 0) { - Spacer() - BundleSheetToggleButtons( - showBackgroundSheet: $showBackgroundSheet, - showCarabinerSheet: $showCarabinerSheet - ) - DraggableSheet( - sheetHeight: $sheetHeight, - content: SelectBackgroundSheet( - viewModel: bundleVM, - selectedBG: bundleVM.newSelectedBackground, - onBackgroundTap: { bg in - bundleVM.newSelectedBackground = bg - } + ZStack(alignment: .bottom) { + Color.clear + + if showItemSheet { + // 시트가 있을 때: 셀렉터 + 시트가 함께 움직임 + VStack(spacing: 0) { + BundleSheetToggleButtons( + showItemSheet: $showItemSheet, + isBackgroundMode: $isBackgroundMode ) - ) - } - .opacity(showBackgroundSheet ? 1 : 0) + .padding(.bottom, 10) - // 카라비너 시트 - VStack(spacing: 0) { - Spacer() - BundleSheetToggleButtons( - showBackgroundSheet: $showBackgroundSheet, - showCarabinerSheet: $showCarabinerSheet - ) - DraggableSheet( - sheetHeight: $sheetHeight, - content: SelectCarabinerSheet( - viewModel: bundleVM, - selectedCarabiner: bundleVM.newSelectedCarabiner, - onCarabinerTap: { carabiner in - selectCarabiner = carabiner - showChangeCarabinerAlert = true + DraggableSheet( + sheetHeight: $sheetHeight, + content: itemSheetContent, + onDismiss: { + showItemSheet = false } ) + } + .transition(.move(edge: .bottom)) + } else { + // 시트가 없을 때: 셀렉터만 하단에 고정 + BundleSheetToggleButtons( + showItemSheet: $showItemSheet, + isBackgroundMode: $isBackgroundMode ) + .padding(.bottom, 50) + .transition(.identity) } - .opacity(showCarabinerSheet ? 1 : 0) + } + .animation(.easeInOut(duration: 0.25), value: showItemSheet) + } + + @ViewBuilder + private var itemSheetContent: some View { + if isBackgroundMode { + SelectBackgroundSheet( + viewModel: bundleVM, + selectedBG: bundleVM.newSelectedBackground, + onBackgroundTap: { bg in + bundleVM.newSelectedBackground = bg + } + ) + } else { + SelectCarabinerSheet( + viewModel: bundleVM, + selectedCarabiner: bundleVM.newSelectedCarabiner, + onCarabinerTap: { carabiner in + selectCarabiner = carabiner + showChangeCarabinerAlert = true + } + ) } } diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView.swift b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView.swift index d76a56dd..6225ed1b 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView.swift @@ -25,8 +25,8 @@ struct BundleEditView: View { @State var isCapturing: Bool = false // MARK: - Sheet - @State var showBackgroundSheet: Bool = false - @State var showCarabinerSheet: Bool = false + @State var showItemSheet: Bool = false + @State var isBackgroundMode: Bool = true // true: 배경, false: 카라비너 @State var showPurchaseSheet = false @State var showSelectKeyringSheet = false @@ -97,25 +97,15 @@ struct BundleEditView: View { } TabBarManager.hide() // 화면 첫 진입 시 배경 시트를 보여줌 - if !showBackgroundSheet && !showCarabinerSheet { - showBackgroundSheet = true + if !showItemSheet { + isBackgroundMode = true + showItemSheet = true } } .onDisappear { isNavigatingAway = false } .ignoresSafeArea() - // 배경 시트와 카라비너 시트는 동시에 열릴 수 없음 (하나가 열리면 다른 하나는 자동으로 닫힘) - .onChange(of: showBackgroundSheet) { oldValue, newValue in - if newValue { - showCarabinerSheet = false - } - } - .onChange(of: showCarabinerSheet) { oldValue, newValue in - if newValue { - showBackgroundSheet = false - } - } .onChange(of: bundleVM.newSelectedBackground) { _, newBackground in guard newBackground != nil else { return } // 배경 변경 시에는 키링 데이터 업데이트만 수행 (Firebase 접근 없음) diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleSheetToggleButtons.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleSheetToggleButtons.swift index 1f6b87e8..0503d05e 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleSheetToggleButtons.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleSheetToggleButtons.swift @@ -9,8 +9,16 @@ import SwiftUI /// 뭉치 생성/편집 화면에서 배경/카라비너 시트를 토글하는 버튼 컴포넌트 struct BundleSheetToggleButtons: View { - @Binding var showBackgroundSheet: Bool - @Binding var showCarabinerSheet: Bool + @Binding var showItemSheet: Bool + @Binding var isBackgroundMode: Bool // true: 배경, false: 카라비너 + + private var isBackgroundSelected: Bool { + showItemSheet && isBackgroundMode + } + + private var isCarabinerSelected: Bool { + showItemSheet && !isBackgroundMode + } var body: some View { HStack(spacing: 8) { @@ -19,43 +27,60 @@ struct BundleSheetToggleButtons: View { Spacer() } .padding(.leading, 18) - .padding(.bottom, 10) } private var backgroundButton: some View { Button { - showBackgroundSheet = true + if isBackgroundSelected { + // 배경 모드에서 다시 누르면 시트 닫기 + showItemSheet = false + } else { + // 시트 열기 + 배경 모드로 전환 + isBackgroundMode = true + showItemSheet = true + } } label: { VStack(spacing: 0) { - Image(showBackgroundSheet ? .backgroundIconWhite100 : .backgroundIconGray600) + Image(isBackgroundSelected ? .backgroundIconWhite100 : .backgroundIconGray600) Text("배경") .typography(.suit9SB) - .foregroundStyle(showBackgroundSheet ? .white100 : .gray600) + .foregroundStyle(isBackgroundSelected ? .white100 : .gray600) } .frame(width: 46, height: 46) .background( RoundedRectangle(cornerRadius: 14.38) - .fill(showBackgroundSheet ? .main500 : .white100) + .fill(isBackgroundSelected ? .main500 : .white100) ) + .animation(nil, value: showItemSheet) + .animation(nil, value: isBackgroundMode) } .buttonStyle(.plain) } private var carabinerButton: some View { Button { - showCarabinerSheet = true + if isCarabinerSelected { + // 카라비너 모드에서 다시 누르면 시트 닫기 + showItemSheet = false + } else { + // 시트 열기 + 카라비너 모드로 전환 + isBackgroundMode = false + showItemSheet = true + } } label: { VStack(spacing: 0) { - Image(showCarabinerSheet ? .carabinerIconWhite100 : .carabinerIconGray600) + Image(isCarabinerSelected ? .carabinerIconWhite100 : .carabinerIconGray600) Text("카라비너") .typography(.suit9SB) - .foregroundStyle(showCarabinerSheet ? .white100 : .gray600) + .foregroundStyle(isCarabinerSelected ? .white100 : .gray600) } .frame(width: 46, height: 46) .background( RoundedRectangle(cornerRadius: 14.38) - .fill(showCarabinerSheet ? .main500 : .white100) + .fill(isCarabinerSelected ? .main500 : .white100) ) + .animation(nil, value: showItemSheet) + .animation(nil, value: isBackgroundMode) } .buttonStyle(.plain) } From 572a0da37d42d255a25cbda887d0a20b7c765f68 Mon Sep 17 00:00:00 2001 From: giljihun Date: Fri, 6 Feb 2026 13:54:21 +0900 Subject: [PATCH 03/14] =?UTF-8?q?feat:=20DraggableSheet=EC=97=90=20onDismi?= =?UTF-8?q?ss=20=EC=BD=9C=EB=B0=B1=20=EB=B0=8F=20dismiss=20threshold=20?= =?UTF-8?q?=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Bundle/Views/Shared/DraggableSheet.swift | 49 ++++++++++--------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Shared/DraggableSheet.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/DraggableSheet.swift index 1dfe5839..6f356b20 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Shared/DraggableSheet.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/DraggableSheet.swift @@ -10,21 +10,22 @@ import SwiftUI struct DraggableSheet: View { @Binding var sheetHeight: CGFloat let content: Content - + var onDismiss: (() -> Void)? = nil + // 화면 높이 기준 비율 - private let smallRatio: CGFloat = 0.08 - private let mediumRatio: CGFloat = 0.32 - private let largeRatio: CGFloat = 0.8 + private let dismissRatio: CGFloat = 0.15 + private let mediumRatio: CGFloat = 0.40 + private let largeRatio: CGFloat = 0.80 // 계산된 높이 값들 - private var smallHeight: CGFloat { - screenHeight * smallRatio + private var dismissHeight: CGFloat { + screenHeight * dismissRatio } - + private var mediumHeight: CGFloat { screenHeight * mediumRatio } - + private var largeHeight: CGFloat { screenHeight * largeRatio } @@ -63,9 +64,7 @@ struct DraggableSheet: View { .glassEffect(.regular, in: .rect) .clipShape(RoundedRectangle(cornerRadius: 30)) .onAppear { - if sheetHeight == 360 { - sheetHeight = mediumHeight // 기본값을 중간 크기로 설정 - } + sheetHeight = mediumHeight } } @@ -73,22 +72,26 @@ struct DraggableSheet: View { DragGesture() .onChanged { value in let newHeight = sheetHeight - value.translation.height - if newHeight >= smallHeight && newHeight <= largeHeight { + // 0 이상, largeHeight 이하까지 드래그 가능 + if newHeight >= 0 && newHeight <= largeHeight { sheetHeight = newHeight } } .onEnded { _ in - withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { - // 3단계로 스냅 - let midSmallMedium = (smallHeight + mediumHeight) / 2 - let midMediumLarge = (mediumHeight + largeHeight) / 2 - - if sheetHeight < midSmallMedium { - sheetHeight = smallHeight - } else if sheetHeight < midMediumLarge { - sheetHeight = mediumHeight - } else { - sheetHeight = largeHeight + // 내리면 닫기 (거의 끝까지 내려야 닫힘) + let dismissThreshold = dismissHeight + (mediumHeight - dismissHeight) * 0.05 + let midMediumLarge = (mediumHeight + largeHeight) / 2 + + if sheetHeight < dismissThreshold { + // dismiss는 호출하는 쪽에서 애니메이션 제어 + onDismiss?() + } else { + withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { + if sheetHeight < midMediumLarge { + sheetHeight = mediumHeight + } else { + sheetHeight = largeHeight + } } } } From f4a1752cbc85f1e14e036adc5bcb0212ea696226 Mon Sep 17 00:00:00 2001 From: giljihun Date: Fri, 6 Feb 2026 13:54:38 +0900 Subject: [PATCH 04/14] =?UTF-8?q?fix:=20=EC=85=80=EB=A0=89=ED=84=B0?= =?UTF-8?q?=EC=99=80=20=EC=8B=9C=ED=8A=B8=EA=B0=80=20=ED=95=A8=EA=BB=98=20?= =?UTF-8?q?=EC=9B=80=EC=A7=81=EC=9D=B4=EB=8A=94=20=EC=95=A0=EB=8B=88?= =?UTF-8?q?=EB=A9=94=EC=9D=B4=EC=85=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Keychy/Keychy.xcodeproj/project.pbxproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Keychy/Keychy.xcodeproj/project.pbxproj b/Keychy/Keychy.xcodeproj/project.pbxproj index 93e26e87..36d3927e 100644 --- a/Keychy/Keychy.xcodeproj/project.pbxproj +++ b/Keychy/Keychy.xcodeproj/project.pbxproj @@ -362,7 +362,6 @@ AA3909462EC9F29500D87EEC /* UIApplication+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3909452EC9F29500D87EEC /* UIApplication+Extension.swift */; }; AA39098E2ECA061700D87EEC /* GridItemSpacing.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA39098D2ECA061700D87EEC /* GridItemSpacing.swift */; }; AA390CE52ECC60A700D87EEC /* BundleRoute.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA390CE42ECC60A700D87EEC /* BundleRoute.swift */; }; - 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 /* DraggableSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA6298552EC3AD16001576C0 /* DraggableSheet.swift */; }; @@ -813,7 +812,6 @@ AA3909452EC9F29500D87EEC /* UIApplication+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Extension.swift"; sourceTree = ""; }; AA39098D2ECA061700D87EEC /* GridItemSpacing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridItemSpacing.swift; sourceTree = ""; }; AA390CE42ECC60A700D87EEC /* BundleRoute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleRoute.swift; sourceTree = ""; }; - 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 /* DraggableSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggableSheet.swift; sourceTree = ""; }; @@ -2117,7 +2115,6 @@ children = ( AA6298532EC39065001576C0 /* BundleCreateView.swift */, AA0219DD2EB1C041006EF269 /* BundleNameInputView.swift */, - AA4B07312EB26CD2005F9227 /* BundleAddKeyringView.swift */, ); path = Create; sourceTree = ""; @@ -2446,7 +2443,6 @@ files = ( C6C4028F2EB27458006B58DF /* Particle.swift in Sources */, AA0A54B72EC053E4007B5413 /* CarabinerType.swift in Sources */, - AA4B07322EB26CD2005F9227 /* BundleAddKeyringView.swift in Sources */, 4CEC61E52EAE08C00099ECEE /* KeyringBundle.swift in Sources */, 4C004FAD2F177F2600D9063E /* BundleDetailView+VideoGen.swift in Sources */, 386102462F10F9CE0045C529 /* KeyringReceiveView+Alerts.swift in Sources */, From f97d3128e63c5eaa4ac215e2530bfeaf17d9616b Mon Sep 17 00:00:00 2001 From: giljihun Date: Fri, 6 Feb 2026 13:55:01 +0900 Subject: [PATCH 05/14] =?UTF-8?q?refactor:=20BundleAddKeyringView=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EB=B0=8F=20=EA=B4=80=EB=A0=A8=20route=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Core/Navigation/Routes/BundleRoute.swift | 1 - .../Navigation/Routes/CollectionRoute.swift | 1 - .../Core/Navigation/Routes/HomeRoute.swift | 1 - .../Navigation/Routes/WorkshopRoute.swift | 1 - .../Views/Create/BundleAddKeyringView.swift | 446 ------------------ .../Tab/Views/CollectionTab.swift | 2 - .../Presentation/Tab/Views/HomeTab.swift | 2 - .../Presentation/Tab/Views/WorkshopTab.swift | 2 - 8 files changed, 456 deletions(-) delete mode 100644 Keychy/Keychy/Presentation/Bundle/Views/Create/BundleAddKeyringView.swift diff --git a/Keychy/Keychy/Core/Navigation/Routes/BundleRoute.swift b/Keychy/Keychy/Core/Navigation/Routes/BundleRoute.swift index b8eb6d60..ddb56f52 100644 --- a/Keychy/Keychy/Core/Navigation/Routes/BundleRoute.swift +++ b/Keychy/Keychy/Core/Navigation/Routes/BundleRoute.swift @@ -12,7 +12,6 @@ protocol BundleRoute: Hashable { static var bundleInventoryView: Self { get } static var bundleDetailView: Self { get } static var bundleCreateView: Self { get } - static var bundleAddKeyringView: Self { get } static var bundleNameInputView: Self { get } static var bundleNameEditView: Self { get } static var bundleEditView: Self { get } diff --git a/Keychy/Keychy/Core/Navigation/Routes/CollectionRoute.swift b/Keychy/Keychy/Core/Navigation/Routes/CollectionRoute.swift index 0080193b..7d6ccf24 100644 --- a/Keychy/Keychy/Core/Navigation/Routes/CollectionRoute.swift +++ b/Keychy/Keychy/Core/Navigation/Routes/CollectionRoute.swift @@ -19,7 +19,6 @@ enum CollectionRoute: Hashable, BundleRoute { case bundleInventoryView case bundleDetailView case bundleCreateView - case bundleAddKeyringView case bundleNameInputView case bundleNameEditView case bundleEditView diff --git a/Keychy/Keychy/Core/Navigation/Routes/HomeRoute.swift b/Keychy/Keychy/Core/Navigation/Routes/HomeRoute.swift index 399675ab..e32b99fb 100644 --- a/Keychy/Keychy/Core/Navigation/Routes/HomeRoute.swift +++ b/Keychy/Keychy/Core/Navigation/Routes/HomeRoute.swift @@ -11,7 +11,6 @@ enum HomeRoute: Hashable, BundleRoute { case bundleInventoryView case bundleDetailView case bundleCreateView - case bundleAddKeyringView case bundleNameInputView case bundleNameEditView case bundleEditView diff --git a/Keychy/Keychy/Core/Navigation/Routes/WorkshopRoute.swift b/Keychy/Keychy/Core/Navigation/Routes/WorkshopRoute.swift index b0c7e8ac..2ca1788b 100644 --- a/Keychy/Keychy/Core/Navigation/Routes/WorkshopRoute.swift +++ b/Keychy/Keychy/Core/Navigation/Routes/WorkshopRoute.swift @@ -18,7 +18,6 @@ enum WorkshopRoute: Hashable, BundleRoute { case bundleInventoryView case bundleDetailView case bundleCreateView - case bundleAddKeyringView case bundleNameInputView case bundleNameEditView case bundleEditView diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleAddKeyringView.swift b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleAddKeyringView.swift deleted file mode 100644 index cfa09c70..00000000 --- a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleAddKeyringView.swift +++ /dev/null @@ -1,446 +0,0 @@ -// -// BundleAddKeyringView.swift -// Keychy -// -// Created by 김서현 on 10/28/25. -// - -import SwiftUI -import SpriteKit -import NukeUI - -/// 번들에 키링을 추가하는 뷰 -struct BundleAddKeyringView: View { - // MARK: - Properties - - @Bindable var router: NavigationRouter - @State var collectionVM: CollectionViewModel - @State var bundleVM: BundleViewModel - - @State private var showSelectKeyringSheet = false - @State private var selectedKeyrings: [Int: Keyring] = [:] - @State private var keyringOrder: [Int] = [] - @State private var selectedPosition = 0 - @State private var isCapturing = false - @State private var sceneRefreshId = UUID() - @State private var isSceneReady = false - // 키링 선택 시트 그리드 컬럼 - private let gridColumns: [GridItem] = [ - GridItem(.flexible(), spacing: 10), - GridItem(.flexible(), spacing: 10), - GridItem(.flexible(), spacing: 10) - ] - private let sheetHeightRatio: CGFloat = 0.43 - - // MARK: - Body - - var body: some View { - ZStack(alignment: .bottom) { - // 배경 + 카라비너 + 키링 씬 - ZStack(alignment: .top) { - if let carabiner = bundleVM.selectedCarabiner, - let background = bundleVM.selectedBackground { - keyringEditSceneView(background: background, carabiner: carabiner) - } - customNavigationBar - } - .blur(radius: !isCapturing ? 0 : 10) - - - // Dim 오버레이 (키링 시트가 열릴 때) - if showSelectKeyringSheet || isCapturing { - Color.black20 - .ignoresSafeArea() - .zIndex(1) - .onTapGesture { - if showSelectKeyringSheet { - withAnimation(.easeInOut) { - showSelectKeyringSheet = false - } - } - } - - // 키링 선택 시트 - keyringSelectionSheet() - .opacity(showSelectKeyringSheet ? 1 : 0) - - LoadingAlert(type: .longWithKeychy, message: "뭉치 만드는 중...") - .opacity(isCapturing ? 1 : 0) - .zIndex(50) - } - } - .ignoresSafeArea() - .onAppear { - fetchData() - TabBarManager.hide() - } - .navigationBarBackButtonHidden(true) - } -} - -// MARK: - Toolbar -extension BundleAddKeyringView { - private var customNavigationBar: some View { - CustomNavigationBar { - BackToolbarButton { - router.pop() - } - } center: { - - } trailing: { - NextToolbarButton { - Task { - await captureAndSaveScene() - } - } - .disabled(isCapturing) - } - } -} - -// MARK: - View Components -extension BundleAddKeyringView { - /// 키링 편집 씬 뷰 - private func keyringEditSceneView(background: Background, carabiner: Carabiner) -> some View { - ZStack { - // MultiKeyringScene - MultiKeyringSceneView( - keyringDataList: createKeyringDataList(carabiner: carabiner), - ringType: .basic, - chainType: .basic, - backgroundColor: .clear, - backgroundImageURL: background.backgroundImage, - carabinerBackImageURL: carabiner.backImageURL, - carabinerFrontImageURL: carabiner.frontImageURL, - carabinerX: carabiner.carabinerX, - carabinerY: carabiner.carabinerY, - carabinerWidth: carabiner.carabinerWidth, - currentCarabinerType: carabiner.type - ) - .ignoresSafeArea() - .id("scene_\(background.id ?? "bg")_\(carabiner.id ?? "cb")_\(selectedKeyrings.count)_\(sceneRefreshId.uuidString)") - - // 키링 추가 버튼들 - keyringButtons(carabiner: carabiner) - } - } - - /// 키링 추가 버튼들 - private func keyringButtons(carabiner: Carabiner) -> some View { - GeometryReader { geometry in - let sceneWidth: CGFloat = 402 - let sceneHeight: CGFloat = 874 - // 실제 화면 크기에 맞게 씬을 스케일링하는 비율 계산 - let scale = max(geometry.size.width / sceneWidth, geometry.size.height / sceneHeight) - - // 스케일 적용 후 콘텐츠 크기 - let contentW = sceneWidth * scale - let contentH = sceneHeight * scale - - // 씬을 화면 중앙에 배치하기 위한 오프셋 (실제 뷰 크기와 스케일된 씬의 크기 차이를 균등하게 배분) - let dx = (geometry.size.width - contentW) / 2 - let dy = (geometry.size.height - contentH) / 2 - - // 키링 추가 버튼들 - ForEach(0.. some View { - VStack(spacing: 18) { - Text("키링 선택") - .typography(.suit16B) - .foregroundStyle(.black100) - - if collectionVM.keyring.isEmpty { - VStack { - Image(.emptyViewIcon) - .resizable() - .scaledToFit() - .frame(height: 77) - Text("공방에서 키링을 만들 수 있어요") - .typography(.suit15R) - .foregroundStyle(.black100) - .padding(.vertical, 15) - } - .padding(.bottom, 77) - .padding(.top, 62) - .frame(maxWidth: .infinity) - - } else { - ScrollView { - LazyVGrid(columns: gridColumns, spacing: 10) { - ForEach(bundleVM.sortedKeyringsForSelection(selectedKeyrings: selectedKeyrings, selectedPosition: selectedPosition), id: \.self) { keyring in - keyringCell(keyring: keyring) - } - } - } - } - - } - .padding(EdgeInsets(top: 30, leading: 20, bottom: 0, trailing: 20)) - .frame(maxWidth: .infinity) - .frame(height: screenHeight * sheetHeightRatio) - .glassEffect(.regular, in: .rect) - .clipShape(RoundedRectangle(cornerRadius: 16)) - .shadow(radius: 10) - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom) - .transition(.move(edge: .bottom)) - .zIndex(2) - } - - /// 키링 셀 (체크 토글 + 시트 유지) - private func keyringCell(keyring: Keyring) -> some View { - // 현재 선택된 위치에 이 키링이 선택되어 있는지 - let isSelectedHere: Bool = selectedKeyrings[selectedPosition]?.id == keyring.id - // 다른 위치에 이미 선택된 키링인지 체크 - let isSelectedElsewhere: Bool = selectedKeyrings.values.contains { $0.id == keyring.id } && !isSelectedHere - - return Button { - // 토글 - if isSelectedHere { - // 해제 - selectedKeyrings[selectedPosition] = nil - keyringOrder.removeAll { $0 == selectedPosition } - } else if !isSelectedElsewhere { - // 중복이 아닐 때만 선택 가능 - // 기존 있으면 순서 제거 후 교체 - if selectedKeyrings[selectedPosition] != nil { - keyringOrder.removeAll { $0 == selectedPosition } - } - selectedKeyrings[selectedPosition] = keyring - keyringOrder.append(selectedPosition) - // 키링 선택완료하면 시트 내림! - withAnimation(.easeInOut) { - showSelectKeyringSheet = false - } - } - // 중복인 경우 아무것도 하지 않음 (선택되지 않음) - updateKeyringDataList() - } label: { - ZStack(alignment: .bottomTrailing) { - VStack(spacing: 10) { - ZStack { - CollectionCellView(keyring: keyring) - .frame(width: threeGridCellWidth, height: threeGridCellHeight) - .cornerRadius(10) - - // 외곽선 - RoundedRectangle(cornerRadius: 10) - .strokeBorder(isSelectedHere ? .mainOpacity80 : .clear, lineWidth: 1.8) - .frame(width: threeGridCellWidth, height: threeGridCellHeight) - - // 다른 위치에 장착된 경우 오버레이 - if isSelectedElsewhere { - RoundedRectangle(cornerRadius: 10) - .fill(.black50) - .frame(width: threeGridCellWidth, height: threeGridCellHeight) - } - } - - Text("\(keyring.name)") - .typography(isSelectedHere ? .notosans14SB : .notosans14M) - .foregroundStyle(isSelectedHere ? .main500 : .black100) - .lineLimit(1) - .truncationMode(.tail) - } - - // 장착 중 아이콘 - if isSelectedElsewhere || isSelectedHere { - VStack { - HStack { - Spacer() - Text("장착 중") - .foregroundStyle(.white100) - .typography(.suit13M) - .padding(.horizontal, 10) - .padding(.vertical, 4) - .background( - RoundedRectangle(cornerRadius: 20) - .fill(.mainOpacity80) - ) - } - Spacer() - } - .padding(.top, 5) - .padding(.trailing, 5) - } - } - } - .disabled(keyring.status == .packaged || keyring.status == .published || isSelectedElsewhere) - .opacity(1.0) // 강제로 투명도 1.0 유지 - } - - - /// 키링 데이터 리스트 업데이트 - private func updateKeyringDataList() { - // 씬을 강제로 리프레시하여 키링 변경사항 즉시 반영 - sceneRefreshId = UUID() - } -} - -// MARK: - Scene Management - -extension BundleAddKeyringView { - /// 씬 캡처 및 저장 - private func captureAndSaveScene() async { - guard let carabiner = bundleVM.selectedCarabiner, - let background = bundleVM.selectedBackground else { - return - } - - // 캡처 시작 - await MainActor.run { - isCapturing = true - bundleVM.selectedKeyringsForBundle = selectedKeyrings - } - - // 배경 이미지 미리 로드 - guard let _ = try? await StorageManager.shared.getImage(path: background.backgroundImage) else { - await MainActor.run { - isCapturing = false - } - return - } - - // 캡처용 키링 데이터 생성 - var keyringDataList: [MultiKeyringCaptureScene.KeyringData] = [] - - for (index, keyring) in selectedKeyrings.sorted(by: { $0.key < $1.key }) { - let data = MultiKeyringCaptureScene.KeyringData( - index: index, - position: CGPoint( - x: carabiner.keyringXPosition[index], - y: carabiner.keyringYPosition[index] - ), - bodyImageURL: keyring.bodyImage, - hookOffsetY: keyring.hookOffsetY, - chainLength: keyring.chainLength - ) - keyringDataList.append(data) - } - - // 카라비너 이미지 추출 - let carabinerType = CarabinerType.from(carabiner.carabinerType) - let carabinerBackURL: String? - let carabinerFrontURL: String? - - if carabinerType == .hamburger { - carabinerBackURL = carabiner.carabinerImage[1] - carabinerFrontURL = carabiner.carabinerImage[2] - } else { - // plain 타입 - carabinerBackURL = carabiner.carabinerImage[0] - carabinerFrontURL = nil - } - - // 씬 캡처 - if let pngData = await MultiKeyringCaptureScene.captureBundleImage( - keyringDataList: keyringDataList, - backgroundImageURL: background.backgroundImage, - carabinerBackImageURL: carabinerBackURL, - carabinerFrontImageURL: carabinerFrontURL, - carabinerType: carabinerType, // 카라비너 타입 전달 - carabinerX: carabiner.carabinerX, - carabinerY: carabiner.carabinerY, - carabinerWidth: carabiner.carabinerWidth, - ) { - await MainActor.run { - bundleVM.bundleCapturedImage = pngData - } - } else { - print("❌ [BundleAddKeyring] 캡처 실패") - } - - // 캡처 완료 후 다음 화면으로 이동 - await MainActor.run { - isCapturing = false - router.push(.bundleNameInputView) - } - } -} - -// MARK: - Data Fetching - -extension BundleAddKeyringView { - /// 사용자 데이터 가져오기 - private func fetchData() { - let uid = UserManager.shared.userUID - - collectionVM.fetchUserCollectionData(uid: uid) { success in - if success { - collectionVM.fetchUserKeyrings(uid: uid) { success in - if success { - bundleVM.keyring = collectionVM.keyring - print("데이터 가져오기 성공") - } - } - } - } - } -} - -// MARK: - Helper Methods - -extension BundleAddKeyringView { - /// 키링 데이터 리스트 생성 - private func createKeyringDataList(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] - ) - - let data = MultiKeyringScene.KeyringData( - index: index, - position: position, - bodyImageURL: keyring.bodyImage, - templateId: keyring.selectedTemplate, - soundId: soundId, - customSoundURL: customSoundURL, - particleId: particleId, - hookOffsetY: keyring.hookOffsetY, - chainLength: keyring.chainLength - ) - dataList.append(data) - } - - return dataList - } -} diff --git a/Keychy/Keychy/Presentation/Tab/Views/CollectionTab.swift b/Keychy/Keychy/Presentation/Tab/Views/CollectionTab.swift index 216db871..1a6f3969 100644 --- a/Keychy/Keychy/Presentation/Tab/Views/CollectionTab.swift +++ b/Keychy/Keychy/Presentation/Tab/Views/CollectionTab.swift @@ -31,8 +31,6 @@ struct CollectionTab: View { BundleDetailView(router: router, collectionVM: collectionViewModel, bundleVM: bundleViewModel) case .bundleCreateView: BundleCreateView(router: router, collectionVM: collectionViewModel, bundleVM: bundleViewModel) - case .bundleAddKeyringView: - BundleAddKeyringView(router: router, collectionVM: collectionViewModel, bundleVM: bundleViewModel) case .bundleNameInputView: BundleNameInputView(router: router, collectionVM: collectionViewModel, bundleVM: bundleViewModel) case .bundleNameEditView: diff --git a/Keychy/Keychy/Presentation/Tab/Views/HomeTab.swift b/Keychy/Keychy/Presentation/Tab/Views/HomeTab.swift index 3631d87a..0fd7d8a6 100644 --- a/Keychy/Keychy/Presentation/Tab/Views/HomeTab.swift +++ b/Keychy/Keychy/Presentation/Tab/Views/HomeTab.swift @@ -38,8 +38,6 @@ struct HomeTab: View { BundleDetailView(router: router, collectionVM: collectionViewModel, bundleVM: bundleViewModel) case .bundleCreateView: BundleCreateView(router: router, collectionVM: collectionViewModel, bundleVM: bundleViewModel) - case .bundleAddKeyringView: - BundleAddKeyringView(router: router, collectionVM: collectionViewModel, bundleVM: bundleViewModel) case .bundleNameInputView: BundleNameInputView(router: router, collectionVM: collectionViewModel, bundleVM: bundleViewModel) case .bundleNameEditView: diff --git a/Keychy/Keychy/Presentation/Tab/Views/WorkshopTab.swift b/Keychy/Keychy/Presentation/Tab/Views/WorkshopTab.swift index c50b6fd1..02c89207 100644 --- a/Keychy/Keychy/Presentation/Tab/Views/WorkshopTab.swift +++ b/Keychy/Keychy/Presentation/Tab/Views/WorkshopTab.swift @@ -222,8 +222,6 @@ struct WorkshopTab: View { BundleDetailView(router: router, collectionVM: collectionViewModel, bundleVM: bundleViewModel) case .bundleCreateView: BundleCreateView(router: router, collectionVM: collectionViewModel, bundleVM: bundleViewModel) - case .bundleAddKeyringView: - BundleAddKeyringView(router: router, collectionVM: collectionViewModel, bundleVM: bundleViewModel) case .bundleNameInputView: BundleNameInputView(router: router, collectionVM: collectionViewModel, bundleVM: bundleViewModel) case .bundleNameEditView: From 65ff4f8f8f3887041d0ead31fe69d79aa1a542d4 Mon Sep 17 00:00:00 2001 From: giljihun Date: Fri, 6 Feb 2026 14:00:01 +0900 Subject: [PATCH 06/14] =?UTF-8?q?style:=20=EB=AD=89=EC=B9=98=EB=A7=8C?= =?UTF-8?q?=EB=93=A4=EA=B8=B0=20=EC=85=80=EB=A0=89=ED=84=B0=20=EC=97=90?= =?UTF-8?q?=EC=85=8B=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Shared/BundleSheetToggleButtons.swift | 8 ++++---- .../Contents.json | 2 +- .../backgroundIconGray400.pdf | Bin 0 -> 4715 bytes .../backgroundIconGray600.pdf | Bin 7153 -> 0 bytes .../Contents.json | 2 +- .../carabinerIconGray400.pdf} | Bin 13357 -> 11585 bytes 6 files changed, 6 insertions(+), 6 deletions(-) rename Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Features/{backgroundIconGray600.imageset => backgroundIconGray400.imageset}/Contents.json (71%) create mode 100644 Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Features/backgroundIconGray400.imageset/backgroundIconGray400.pdf delete mode 100644 Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Features/backgroundIconGray600.imageset/backgroundIconGray600.pdf rename Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Features/{carabinerIconGray600.imageset => carabinerIconGray400.imageset}/Contents.json (72%) rename Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Features/{carabinerIconGray600.imageset/carabinerIconGray600.pdf => carabinerIconGray400.imageset/carabinerIconGray400.pdf} (68%) diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleSheetToggleButtons.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleSheetToggleButtons.swift index 0503d05e..d296e115 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleSheetToggleButtons.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleSheetToggleButtons.swift @@ -41,10 +41,10 @@ struct BundleSheetToggleButtons: View { } } label: { VStack(spacing: 0) { - Image(isBackgroundSelected ? .backgroundIconWhite100 : .backgroundIconGray600) + Image(isBackgroundSelected ? .backgroundIconWhite100 : .backgroundIconGray400) Text("배경") .typography(.suit9SB) - .foregroundStyle(isBackgroundSelected ? .white100 : .gray600) + .foregroundStyle(isBackgroundSelected ? .white100 : .gray400) } .frame(width: 46, height: 46) .background( @@ -69,10 +69,10 @@ struct BundleSheetToggleButtons: View { } } label: { VStack(spacing: 0) { - Image(isCarabinerSelected ? .carabinerIconWhite100 : .carabinerIconGray600) + Image(isCarabinerSelected ? .carabinerIconWhite100 : .carabinerIconGray400) Text("카라비너") .typography(.suit9SB) - .foregroundStyle(isCarabinerSelected ? .white100 : .gray600) + .foregroundStyle(isCarabinerSelected ? .white100 : .gray400) } .frame(width: 46, height: 46) .background( diff --git a/Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Features/backgroundIconGray600.imageset/Contents.json b/Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Features/backgroundIconGray400.imageset/Contents.json similarity index 71% rename from Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Features/backgroundIconGray600.imageset/Contents.json rename to Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Features/backgroundIconGray400.imageset/Contents.json index c844a7ff..62747971 100644 --- a/Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Features/backgroundIconGray600.imageset/Contents.json +++ b/Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Features/backgroundIconGray400.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "backgroundIconGray600.pdf", + "filename" : "backgroundIconGray400.pdf", "idiom" : "universal" } ], diff --git a/Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Features/backgroundIconGray400.imageset/backgroundIconGray400.pdf b/Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Features/backgroundIconGray400.imageset/backgroundIconGray400.pdf new file mode 100644 index 0000000000000000000000000000000000000000..c51f66d957ff8bbdfdde85f463ea6a16f54ff5a3 GIT binary patch literal 4715 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 literal 0 HcmV?d00001 diff --git a/Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Features/backgroundIconGray600.imageset/backgroundIconGray600.pdf b/Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Features/backgroundIconGray600.imageset/backgroundIconGray600.pdf deleted file mode 100644 index 402e180944bca74d2cac833aed805b95430ea833..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7153 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 F{{cFdNVfn0 diff --git a/Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Features/carabinerIconGray600.imageset/Contents.json b/Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Features/carabinerIconGray400.imageset/Contents.json similarity index 72% rename from Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Features/carabinerIconGray600.imageset/Contents.json rename to Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Features/carabinerIconGray400.imageset/Contents.json index 70895cb0..7611880d 100644 --- a/Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Features/carabinerIconGray600.imageset/Contents.json +++ b/Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Features/carabinerIconGray400.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "carabinerIconGray600.pdf", + "filename" : "carabinerIconGray400.pdf", "idiom" : "universal" } ], diff --git a/Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Features/carabinerIconGray600.imageset/carabinerIconGray600.pdf b/Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Features/carabinerIconGray400.imageset/carabinerIconGray400.pdf similarity index 68% rename from Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Features/carabinerIconGray600.imageset/carabinerIconGray600.pdf rename to Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Features/carabinerIconGray400.imageset/carabinerIconGray400.pdf index 539ae9c23254023bd6cea36a0e3c2e7b0d86893e..ea1ed4d5da8ddc99e7db504b794f9ef7be51c314 100644 GIT binary patch delta 2231 zcmZ{mdsq`^7RCu6^chIFh$0AfBtV0-$;>27E(Q!21qC4`7*tXvK)6^))+8w4N>CI_ zwxTdlvBefyE0->6i&Am1z(P?_Dq4%M7F=5{BG)1U1(ltNPqBoj^H=80d%kmW&dG14 zGQMHwNyo3XdjT&5gg`~q9smRp50nkafUuxaF>&%y?BRstTgq z(%sSZFP|heCEo3r>kCDAp%cHyG9LUG&8K{;`1kS%v-FG}r#eY>@M+$3;+3(EmhkkdbB&mxR;d4Yqbiy95yqs^Zo{W0Q&7 zt2EVR{u<*<$@)MnNIFwE8u4M$mpLXCypnVG0!1ndqhLd6TGl7su3zqd_a7Ie+8N1^ zgJFQu*Hfsii2n4}O{H&rMlfqM^&@-o8rnUI<$FhFL)vd`bT1uYjW~t{l=G(~Hg_xU z2V5zgx2D z0}9bD6Q$AIN1o)NHQ#xCbL7l~mtwe9Eb4tGoIWJ|!2N7)v!*z*fKqy~)0b+s@n6EW z%hI3o`rRlZhF}N;{N@BQ;9b^hm)P+r^0U4$G2OR z?o3y8T4*M_%?CCXwE})5?|xNK{iPn~Y|9@mJV@DhFW1<*d4#$T$QSvnjw+wrTLNFU zzVb3F=6&P!SjI1d^CQNctrq74_BLi4-81Jgr_ZSZjmnels9%Sh7MNQ4`iPpkAB=eZmqD6Pv93ZGm!H@Vc(eXK%CPnlzO*Z=24Ckn1u9 zr)`++nuo9EA}LQI$AfN6(u=|j9*r#v!^V$&`)F+tJJVnMD(XDvKS;v00&Uyb~!747Tf?c;1~>^XmGmnskX~ z_lEi5h9dc|W5N@?+{C60DNySBy{S3dQ+xfzkGiI5yR$@Vj;jit)y-;07VuRg_~V9> z{u?K0?RztQKq*t`VmTHyKG`O!9+|oAbw9DEF}3x)!N-Sd2VHHueu``r6g8SiCsG+L z9q6-NG>|Vgw|}KTT_)NmB9WFD?8IU;%9q&+Gv7xm91%X%#{Dk2u zva<#tA=q)ct=nHPS0B1C3u~i0kXYDL`tD_1;^$y%8Ln6|BZp3$5K6#SN8CSH4!`Lb zpARyEWb!z5JP4y04EdT9{dmoPCW2W)tFS_7!(UMx1VKiqOr=OxM$1$n%p$VksR-Fu zAy>=fYFs6f{h*PxY!9x71i~B2J+6c7J%`8IRv=4Yz*1-e+=Ox^8f$< delta 3391 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 Jm92~2{{i#Zu>}AC From 7dd9c6a9ba53884536e834c5853e74728e99e996 Mon Sep 17 00:00:00 2001 From: giljihun Date: Fri, 6 Feb 2026 14:05:15 +0900 Subject: [PATCH 07/14] =?UTF-8?q?feat:=20=EB=AD=89=EC=B9=98=EC=94=AC?= =?UTF-8?q?=EC=9D=B4=20=EB=A1=9C=EB=94=A9=EB=90=98=EC=96=B4=EC=95=BC=20?= =?UTF-8?q?=ED=82=A4=EB=A7=81=20=EB=8B=AC=EA=B8=B0=20+=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EB=93=B1=EC=9E=A5=ED=95=98=EA=B2=8C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Bundle/Views/Create/BundleCreateView.swift | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift index fc393c46..ba8dd819 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift @@ -33,6 +33,7 @@ struct BundleCreateView: View { // 캡처 상태 @State private var isCapturing: Bool = false @State private var sceneRefreshId = UUID() + @State private var isSceneReady: Bool = false // 구매 시트 @State var showPurchaseSheet = false @@ -70,7 +71,20 @@ struct BundleCreateView: View { carabinerX: cb.carabiner.carabinerX, carabinerY: cb.carabiner.carabinerY, carabinerWidth: cb.carabiner.carabinerWidth, - currentCarabinerType: cb.carabiner.type + currentCarabinerType: cb.carabiner.type, + onBackgroundLoaded: { + // 키링이 없으면 배경 로드 시 바로 준비 완료 + if selectedKeyrings.isEmpty { + withAnimation(.easeOut(duration: 0.3)) { + isSceneReady = true + } + } + }, + onAllKeyringsReady: { + withAnimation(.easeOut(duration: 0.3)) { + isSceneReady = true + } + } ) .id("scene_\(bg.background.id ?? "bg")_\(cb.carabiner.id ?? "cb")_\(selectedKeyrings.count)_\(sceneRefreshId.uuidString)") @@ -184,6 +198,7 @@ extension BundleCreateView { } ) .position(x: viewX, y: viewY) + .opacity(isSceneReady ? 1.0 : 0.0) } } .ignoresSafeArea() From 9c35652eb8a1ba2cf8f6dc49c1c9cb0b6bb55c3e Mon Sep 17 00:00:00 2001 From: giljihun Date: Fri, 6 Feb 2026 14:09:26 +0900 Subject: [PATCH 08/14] =?UTF-8?q?feat:=20=EB=AD=89=EC=B9=98=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20-=20=EB=8B=A4=EC=9D=8C=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=EB=B9=84=ED=99=9C=EC=84=B1=ED=99=94=20(=ED=82=A4=EB=A7=81=200?= =?UTF-8?q?=EA=B0=9C=EB=A9=B4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Keychy/Core/Components/View/Button/ToolbarButtons.swift | 6 +++++- .../Presentation/Bundle/Views/Create/BundleCreateView.swift | 3 +-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Keychy/Keychy/Core/Components/View/Button/ToolbarButtons.swift b/Keychy/Keychy/Core/Components/View/Button/ToolbarButtons.swift index b029ada9..1b6a3c25 100644 --- a/Keychy/Keychy/Core/Components/View/Button/ToolbarButtons.swift +++ b/Keychy/Keychy/Core/Components/View/Button/ToolbarButtons.swift @@ -26,13 +26,16 @@ struct BackToolbarButton: View { // MARK: - Next Toolbar Button struct NextToolbarButton: View { let title: String + let isDisabled: Bool let action: () -> Void init( title: String = "다음", + isDisabled: Bool = false, action: @escaping () -> Void ) { self.title = title + self.isDisabled = isDisabled self.action = action } @@ -41,10 +44,11 @@ struct NextToolbarButton: View { Text(title) .typography(.suit17B) .padding(4) - .foregroundStyle(.black100) + .foregroundStyle(isDisabled ? .gray300 : .black100) } .frame(width: 62, height: 44) .glassEffect(.regular.interactive(), in: .capsule) + .disabled(isDisabled) } } diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift index ba8dd819..c98c5923 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift @@ -160,12 +160,11 @@ extension BundleCreateView { showPurchaseSheet = true } } else { - NextToolbarButton { + NextToolbarButton(isDisabled: isCapturing || selectedKeyrings.isEmpty) { Task { await captureAndSaveScene() } } - .disabled(isCapturing) } } } From b56bdf8c9b946dc08cd5c4675e02584f17de9150 Mon Sep 17 00:00:00 2001 From: giljihun Date: Fri, 6 Feb 2026 14:56:03 +0900 Subject: [PATCH 09/14] =?UTF-8?q?style:=20=EC=B9=B4=EB=9D=BC=EB=B9=84?= =?UTF-8?q?=EB=84=88,=20bg=20=EC=84=A0=ED=83=9D=20=EC=85=80=20=EC=9C=A0?= =?UTF-8?q?=EB=A3=8C/=EB=B3=B4=EC=9C=A0/=EA=B0=80=EA=B2=A9=20=EC=8A=A4?= =?UTF-8?q?=ED=83=80=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DesignSystem/Typography/Typography.swift | 1 + .../Bundle/Views/Shared/BackgroundCell.swift | 55 +++++++++----- .../Bundle/Views/Shared/CarabinerCell.swift | 44 +++++++---- .../Views/Shared/SelectBackgroundSheet.swift | 75 +++++++++++++------ .../Views/Shared/SelectCarabinerSheet.swift | 73 ++++++++++++------ 5 files changed, 167 insertions(+), 81 deletions(-) diff --git a/Keychy/Keychy/Core/DesignSystem/Typography/Typography.swift b/Keychy/Keychy/Core/DesignSystem/Typography/Typography.swift index 2759f82d..21023dc0 100644 --- a/Keychy/Keychy/Core/DesignSystem/Typography/Typography.swift +++ b/Keychy/Keychy/Core/DesignSystem/Typography/Typography.swift @@ -76,6 +76,7 @@ struct Typography { static let nanum17EB = Typography(font: .custom(.nanumExtraBold, size: 17), lineSpacing: 0) static let nanum16EB = Typography(font: .custom(.nanumExtraBold, size: 16), lineSpacing: 0) static let nanum12EB = Typography(font: .custom(.nanumExtraBold, size: 12), lineSpacing: 0) + static let nanum13EB = Typography(font: .custom(.nanumExtraBold, size: 13), lineSpacing: 0) static let nanum15EB25 = Typography(font: .custom(.nanumExtraBold, size: 15), lineSpacing: 10) static let nanum15B25 = Typography(font: .custom(.nanumBold, size: 15), lineSpacing: 10) diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Shared/BackgroundCell.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/BackgroundCell.swift index 2f84112c..06d888fb 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Shared/BackgroundCell.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/BackgroundCell.swift @@ -13,7 +13,7 @@ struct BackgroundCell: View { let isSelected: Bool var body: some View { - VStack(spacing: 10) { + VStack(spacing: 6) { ZStack(alignment: .top) { // 배경 이미지 LazyImage(url: URL(string: background.background.backgroundImage)) { state in @@ -30,43 +30,58 @@ struct BackgroundCell: View { .clipShape(RoundedRectangle(cornerRadius: 10)) .overlay( RoundedRectangle(cornerRadius: 10) - .strokeBorder(isSelected ? .mainOpacity80 : .clear, lineWidth: 1.8) - + .strokeBorder(isSelected ? .main500 : .clear, lineWidth: 2) ) VStack { HStack { // 유료 아이콘 - Image(.paidIcon) - .padding(.top, 3) + Image(.myCoinMini) .opacity(background.background.isFree ? 0 : 1) Spacer() } + .padding(.horizontal, 8) + .padding(.top, 8) + Spacer() } - .padding(.top, 3) - .padding(.leading, 7) - VStack { - HStack { + // 오른쪽 상단: 유료 아이템만 표시 (보유/가격) + if !background.background.isFree { + VStack { + HStack { + Spacer() + if background.isOwned { + // 유료 + 보유 + Text("보유") + .typography(.suit12M) + .foregroundStyle(.white100) + .padding(.horizontal, 8) + .padding(.vertical, 1.5) + .background(.black60) + .clipShape(RoundedRectangle(cornerRadius: 20)) + } else { + // 유료 + 미보유: 가격 표시 + Text("\(background.background.price)") + .typography(.nanum13EB) + .foregroundStyle(.white100) + .padding(.horizontal, 6) + .padding(.vertical, 2.25) + .padding(.top, 2) + .background(.mainOpacity80) + .clipShape(RoundedRectangle(cornerRadius: 20)) + } + } Spacer() - Text("보유") - .typography(.suit13M) - .foregroundStyle(.white100) - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(.black60) - .clipShape(RoundedRectangle(cornerRadius: 12)) - .opacity((background.isOwned && !background.background.isFree) ? 1 : 0) } - Spacer() + .padding(.top, 7) + .padding(.trailing, 8) } - .padding(.top, 5) - .padding(.trailing, 7) } // 이름 라벨 Text(background.background.backgroundName) .typography(isSelected ? .notosans14SB : .notosans14M) .foregroundStyle(isSelected ? .main500 : .black100) } + .contentShape(Rectangle()) } } diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Shared/CarabinerCell.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/CarabinerCell.swift index 5e34e92e..50c88102 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Shared/CarabinerCell.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/CarabinerCell.swift @@ -13,7 +13,7 @@ struct CarabinerCell: View { var isSelected: Bool var body: some View { - VStack(spacing: 10) { + VStack(spacing: 6) { ZStack(alignment: .topLeading) { // 카라비너 이미지 LazyImage(url: URL(string: carabiner.carabiner.carabinerImage[0])) { state in @@ -52,27 +52,43 @@ struct CarabinerCell: View { .padding(.top, 3) .padding(.leading, 7) - VStack { - HStack { + // 오른쪽 상단: 유료 아이템만 표시 (보유/가격) + if !carabiner.carabiner.isFree { + VStack { + HStack { + Spacer() + if carabiner.isOwned { + // 유료 + 보유 + Text("보유") + .typography(.suit12M) + .foregroundStyle(.white100) + .padding(.horizontal, 8) + .padding(.vertical, 1.5) + .background(.black60) + .clipShape(RoundedRectangle(cornerRadius: 20)) + } else { + // 유료 + 미보유: 가격 표시 + Text("\(carabiner.carabiner.price)") + .typography(.nanum13EB) + .foregroundStyle(.white100) + .padding(.horizontal, 6) + .padding(.vertical, 4.25) + .padding(.top, 2) + .background(.mainOpacity80) + .clipShape(RoundedRectangle(cornerRadius: 20)) + } + } Spacer() - Text("보유") - .typography(.suit13M) - .foregroundStyle(.white100) - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(.black60) - .clipShape(RoundedRectangle(cornerRadius: 12)) - .opacity((carabiner.isOwned && !carabiner.carabiner.isFree) ? 1 : 0) } - Spacer() + .padding(.top, 8) + .padding(.trailing, 8) } - .padding(.top, 5) - .padding(.trailing, 7) } //: ZSTACK .clipped() Text(carabiner.carabiner.carabinerName) .typography(isSelected ? .notosans14SB : .notosans14M) .foregroundStyle(isSelected ? .main500 : .black100) } + .contentShape(Rectangle()) } } diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Shared/SelectBackgroundSheet.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/SelectBackgroundSheet.swift index 427f6ef9..6ce484cf 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Shared/SelectBackgroundSheet.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/SelectBackgroundSheet.swift @@ -8,49 +8,76 @@ import SwiftUI struct SelectBackgroundSheet: View { - let viewModel: BundleViewModel + @Bindable var viewModel: BundleViewModel let selectedBG: BackgroundViewData? let onBackgroundTap: (BackgroundViewData) -> Void - + /// 3열 그리드 컬럼 설정 private let gridColumns: [GridItem] = [ GridItem(.flexible(), spacing: 10), GridItem(.flexible(), spacing: 10), GridItem(.flexible(), spacing: 10) ] - - /// "키치 배경"을 맨 앞으로 정렬 - private var sortedBackgrounds: [BackgroundViewData] { - viewModel.backgroundViewData.sorted { bg1, bg2 in + + /// 필터링 및 정렬된 배경 목록 + private var filteredAndSortedBackgrounds: [BackgroundViewData] { + var result = viewModel.backgroundViewData + + // 필터 적용 + if viewModel.sheetShowFreeOnly { + result = result.filter { $0.background.isFree } + } else if viewModel.sheetShowOwnedOnly { + result = result.filter { $0.isOwned } + } + + // 정렬 + result = result.sorted { bg1, bg2 in + // 키치 배경은 항상 맨 앞 let isKeychy1 = bg1.background.backgroundName == "키치 배경" let isKeychy2 = bg2.background.backgroundName == "키치 배경" - + if isKeychy1 && !isKeychy2 { - return true // bg1이 앞으로 + return true } else if !isKeychy1 && isKeychy2 { - return false // bg2가 앞으로 - } else { - return false // 순서 유지 + return false + } + + // 정렬 기준 적용 + switch viewModel.sheetSortOrder { + case "최신순": + return bg1.background.createdAt > bg2.background.createdAt + case "인기순": + return bg1.background.useCount > bg2.background.useCount + default: + return false } } + + return result } - + var body: some View { - LazyVGrid(columns: gridColumns, spacing: 10) { - ForEach(sortedBackgrounds) { bg in - BackgroundCell(background: bg, isSelected: (bg == selectedBG)) - .onTapGesture { - onBackgroundTap(bg) - - // 무료이고, 유저가 보유x인 경우에만 바로 추가 - if !bg.isOwned && bg.background.isFree { - Task { - await viewModel.addBackgroundToUser(backgroundName: bg.background.backgroundName, userManager: UserManager.shared) + VStack(spacing: 20) { + // 필터바 + BundleSheetFilterBar(viewModel: viewModel) + + // 그리드 + LazyVGrid(columns: gridColumns, spacing: 20) { + ForEach(filteredAndSortedBackgrounds) { bg in + BackgroundCell(background: bg, isSelected: (bg == selectedBG)) + .onTapGesture { + onBackgroundTap(bg) + + // 무료이고, 유저가 보유x인 경우에만 바로 추가 + if !bg.isOwned && bg.background.isFree { + Task { + await viewModel.addBackgroundToUser(backgroundName: bg.background.backgroundName, userManager: UserManager.shared) + } } } - } + } } + .padding(.horizontal, 20) } - .padding(.horizontal, 20) } } diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Shared/SelectCarabinerSheet.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/SelectCarabinerSheet.swift index 5826b45f..da8dca10 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Shared/SelectCarabinerSheet.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/SelectCarabinerSheet.swift @@ -8,48 +8,75 @@ import SwiftUI struct SelectCarabinerSheet: View { - let viewModel: BundleViewModel + @Bindable var viewModel: BundleViewModel let selectedCarabiner: CarabinerViewData? let onCarabinerTap: (CarabinerViewData) -> Void - + /// 3열 그리드 컬럼 설정 private let gridColumns: [GridItem] = [ GridItem(.flexible(), spacing: 10), GridItem(.flexible(), spacing: 10), GridItem(.flexible(), spacing: 10) ] - - /// "키치 카라비너"를 맨 앞으로 정렬 - private var sortedCarabiners: [CarabinerViewData] { - viewModel.carabinerViewData.sorted { cb1, cb2 in + + /// 필터링 및 정렬된 카라비너 목록 + private var filteredAndSortedCarabiners: [CarabinerViewData] { + var result = viewModel.carabinerViewData + + // 필터 적용 + if viewModel.sheetShowFreeOnly { + result = result.filter { $0.carabiner.isFree } + } else if viewModel.sheetShowOwnedOnly { + result = result.filter { $0.isOwned } + } + + // 정렬 + result = result.sorted { cb1, cb2 in + // 키치 카라비너는 항상 맨 앞 let isKeychy1 = cb1.carabiner.carabinerName == "키치 카라비너" let isKeychy2 = cb2.carabiner.carabinerName == "키치 카라비너" - + if isKeychy1 && !isKeychy2 { - return true // cb1이 앞으로 + return true } else if !isKeychy1 && isKeychy2 { - return false // cb2가 앞으로 - } else { - return false // 순서 유지 + return false + } + + // 정렬 기준 적용 + switch viewModel.sheetSortOrder { + case "최신순": + return cb1.carabiner.createdAt > cb2.carabiner.createdAt + case "인기순": + return cb1.carabiner.useCount > cb2.carabiner.useCount + default: + return false } } + + return result } - + var body: some View { - LazyVGrid(columns: gridColumns, spacing: 10) { - ForEach(sortedCarabiners) { cb in - CarabinerCell(carabiner: cb, isSelected: (selectedCarabiner == cb)) - .onTapGesture { - onCarabinerTap(cb) - - if !cb.isOwned && cb.carabiner.isFree { - Task { - await viewModel.addCarabinerToUser(carabinerName: cb.carabiner.carabinerName, userManager: UserManager.shared) + VStack(spacing: 20) { + // 필터바 + BundleSheetFilterBar(viewModel: viewModel) + + // 그리드 + LazyVGrid(columns: gridColumns, spacing: 20) { + ForEach(filteredAndSortedCarabiners) { cb in + CarabinerCell(carabiner: cb, isSelected: (selectedCarabiner == cb)) + .onTapGesture { + onCarabinerTap(cb) + + if !cb.isOwned && cb.carabiner.isFree { + Task { + await viewModel.addCarabinerToUser(carabinerName: cb.carabiner.carabinerName, userManager: UserManager.shared) + } } } - } + } } + .padding(.horizontal, 20) } - .padding(.horizontal, 20) } } From 48a2b995b0fd8869ce10852b6e1b3275fd0787f1 Mon Sep 17 00:00:00 2001 From: giljihun Date: Fri, 6 Feb 2026 14:56:26 +0900 Subject: [PATCH 10/14] =?UTF-8?q?feat:=20=EB=B2=88=EB=93=A4=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20-=20=EC=8B=9C=ED=8A=B8=20=ED=95=84=ED=84=B0=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Keychy/Keychy.xcodeproj/project.pbxproj | 4 + .../Bundle/ViewModels/BundleViewModel.swift | 7 ++ .../Views/Create/BundleCreateView.swift | 11 ++ .../Bundle/Views/Edit/BundleEditView.swift | 6 + .../Views/Shared/BundleSheetFilterBar.swift | 104 ++++++++++++++++++ .../Bundle/Views/Shared/DraggableSheet.swift | 29 ++--- 6 files changed, 142 insertions(+), 19 deletions(-) create mode 100644 Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleSheetFilterBar.swift diff --git a/Keychy/Keychy.xcodeproj/project.pbxproj b/Keychy/Keychy.xcodeproj/project.pbxproj index 36d3927e..f7942fc2 100644 --- a/Keychy/Keychy.xcodeproj/project.pbxproj +++ b/Keychy/Keychy.xcodeproj/project.pbxproj @@ -108,6 +108,7 @@ 4C25259C2F303745003CC5AD /* WidgetBundleModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C25259B2F303745003CC5AD /* WidgetBundleModel.swift */; }; 4C25259D2F3037CD003CC5AD /* WidgetBundleModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C25259B2F303745003CC5AD /* WidgetBundleModel.swift */; }; 4C25259E2F3037D6003CC5AD /* BundleImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6B5707E2EC206CD0049F969 /* BundleImageCache.swift */; }; + 4C2525DA2F35B2A7003CC5AD /* BundleSheetFilterBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2525D92F35B2A7003CC5AD /* BundleSheetFilterBar.swift */; }; 4C3687F72EBFA87800C64E75 /* Pretendard-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 4C3687F62EBFA87800C64E75 /* Pretendard-Medium.ttf */; }; 4C3687FA2EBFC0FB00C64E75 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3687F82EBFC0FB00C64E75 /* NotificationManager.swift */; }; 4C3687FC2EC05E6800C64E75 /* AccountAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3687FB2EC05E6800C64E75 /* AccountAlert.swift */; }; @@ -558,6 +559,7 @@ 4C004FBA2F19F1FE00D9063E /* RootViewModel+ReviewCheck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RootViewModel+ReviewCheck.swift"; sourceTree = ""; }; 4C07024A2ECF10760026D6DC /* EffectSyncManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EffectSyncManager.swift; sourceTree = ""; }; 4C25259B2F303745003CC5AD /* WidgetBundleModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetBundleModel.swift; sourceTree = ""; }; + 4C2525D92F35B2A7003CC5AD /* BundleSheetFilterBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleSheetFilterBar.swift; sourceTree = ""; }; 4C3687F62EBFA87800C64E75 /* Pretendard-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Pretendard-Medium.ttf"; sourceTree = ""; }; 4C3687F82EBFC0FB00C64E75 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; }; 4C3687FB2EC05E6800C64E75 /* AccountAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountAlert.swift; sourceTree = ""; }; @@ -1792,6 +1794,7 @@ AA2146B42F15D8490048D40E /* KeyringCell.swift */, AAA4467B2EC64C9900080AB1 /* SelectBackgroundSheet.swift */, AAA446812EC6519700080AB1 /* SelectCarabinerSheet.swift */, + 4C2525D92F35B2A7003CC5AD /* BundleSheetFilterBar.swift */, AA9115092EB1B7930026E9BC /* AddKeyringButton.swift */, AA6298572EC457DF001576C0 /* CarabinerPopup.swift */, F82FD6112F9442AAAD58DB97 /* BundleSheetToggleButtons.swift */, @@ -2736,6 +2739,7 @@ 4CEC627F2EAE08DF0099ECEE /* CollectionView.swift in Sources */, AA0A54B92EC05C41007B5413 /* BundleRingComponent.swift in Sources */, C6C361422ED44EE4009642F4 /* Showcase25BoardView+Sheet.swift in Sources */, + 4C2525DA2F35B2A7003CC5AD /* BundleSheetFilterBar.swift in Sources */, 3828F5452EC4CC0A00F1B040 /* CollectionViewModel+Filter.swift in Sources */, 38C3C28A2EC1D879003C5DE1 /* UnpackPopup.swift in Sources */, C6C361E42ED4AF48009642F4 /* Showcase25BoardView+Grid.swift in Sources */, diff --git a/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel.swift b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel.swift index fc135d94..7e2b30ee 100644 --- a/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel.swift +++ b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel.swift @@ -75,6 +75,13 @@ class BundleViewModel { var isLoading = false var isPurchasing = false + // MARK: - 시트 필터/정렬 상태 + + var sheetSortOrder: String = "최신순" + var sheetShowFreeOnly: Bool = false + var sheetShowOwnedOnly: Bool = false + var showSheetSortSheet: Bool = false + // MARK: - 편집 화면용 데이터 var newSelectedBackground: BackgroundViewData? diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift index c98c5923..e8e482b3 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift @@ -143,6 +143,17 @@ struct BundleCreateView: View { .sheet(isPresented: $showKeyringSheet) { keyringSheetContent } + .sheet(isPresented: $bundleVM.showSheetSortSheet) { + sortSheetContent + } + } + + /// 정렬 선택 시트 + private var sortSheetContent: some View { + WorkshopSortSheet( + showSheet: $bundleVM.showSheetSortSheet, + sortOrder: $bundleVM.sheetSortOrder + ) } } diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView.swift b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView.swift index 6225ed1b..68808d79 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView.swift @@ -81,6 +81,12 @@ struct BundleEditView: View { .sheet(isPresented: $showPurchaseSheet) { purchaseSheetView } + .sheet(isPresented: $bundleVM.showSheetSortSheet) { + WorkshopSortSheet( + showSheet: $bundleVM.showSheetSortSheet, + sortOrder: $bundleVM.sheetSortOrder + ) + } .navigationBarBackButtonHidden() .withToast(position: .default) .task { diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleSheetFilterBar.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleSheetFilterBar.swift new file mode 100644 index 00000000..0924d252 --- /dev/null +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleSheetFilterBar.swift @@ -0,0 +1,104 @@ +// +// BundleSheetFilterBar.swift +// Keychy +// +// Created by 길지훈 on 2/6/26. +// + +import SwiftUI + +/// 뭉치 생성/편집 시트에서 사용하는 필터바 +/// 공방과 달리 정렬 버튼에 배경색이 없음 +struct BundleSheetFilterBar: View { + @Bindable var viewModel: BundleViewModel + + var body: some View { + HStack(spacing: 0) { + // 정렬 버튼 (배경 없음) + sortButton + + Spacer() + + // 퀵 필터 버튼 (무료, 마이) + quickFilters + } + .padding(.horizontal, 20) + .padding(.top, 16) + .background(Color.clear) + .contentShape(Rectangle()) + } + + /// 정렬 버튼 (배경색 없음) + private var sortButton: some View { + Button { + viewModel.showSheetSortSheet = true + } label: { + HStack(spacing: 4) { + Text(viewModel.sheetSortOrder) + .typography(.suit14SB18) + .foregroundColor(.gray500) + + Image(systemName: "chevron.down") + .foregroundColor(.gray500) + } + .padding(.horizontal, Spacing.gap) + .padding(.vertical, Spacing.sm) + .frame(height: 34) + } + .buttonStyle(PlainButtonStyle()) + } + + /// 퀵 필터 버튼 그룹 + private var quickFilters: some View { + HStack(spacing: 15) { + quickFilterButton(title: "무료", icon: nil, isSelected: viewModel.sheetShowFreeOnly) { + viewModel.sheetShowFreeOnly.toggle() + if viewModel.sheetShowFreeOnly { + viewModel.sheetShowOwnedOnly = false + } + } + + quickFilterButton(title: "마이", icon: .workshopOwnedIcon, isSelected: viewModel.sheetShowOwnedOnly) { + viewModel.sheetShowOwnedOnly.toggle() + if viewModel.sheetShowOwnedOnly { + viewModel.sheetShowFreeOnly = false + } + } + } + } + + /// 퀵 필터 버튼 + private func quickFilterButton( + title: String, + icon: ImageResource?, + isSelected: Bool, + action: @escaping () -> Void + ) -> some View { + Button { + action() + } label: { + HStack(spacing: 4) { + if let icon = icon { + Image(icon) + } + + HStack(spacing: 5) { + Text(title) + .typography(.suit14SB) + .foregroundColor(.gray500) + + Circle() + .fill(isSelected ? Color.main500 : Color.clear) + .stroke(.gray100, lineWidth: 1) + .frame(width: 16, height: 16) + .overlay { + if isSelected { + Image(.quickFilterChecked) + } + } + } + } + } + .buttonStyle(.plain) + } +} diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Shared/DraggableSheet.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/DraggableSheet.swift index 6f356b20..a1253cd3 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Shared/DraggableSheet.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/DraggableSheet.swift @@ -31,29 +31,20 @@ struct DraggableSheet: View { } var body: some View { - VStack(spacing: 10) { - // 인디케이터 - VStack(spacing:0) { - RoundedRectangle(cornerRadius: 3) - .fill(.gray300) - .frame(width: 36, height: 5) - .padding(6) - Text("선택") - .typography(.suit16B) - .foregroundStyle(.black100) - .padding(EdgeInsets(top: 14, leading: 0, bottom: 12, trailing: 0)) - } - .frame(maxWidth: .infinity) - .contentShape(Rectangle()) - .highPriorityGesture(dragGesture) - + VStack(spacing: 0) { + // 인디케이터 (터치 영역은 인디케이터 주변만) + RoundedRectangle(cornerRadius: 3) + .fill(.gray100) + .frame(width: 40, height: 4) + .frame(maxWidth: .infinity) + .padding(.vertical, 10) + .contentShape(Rectangle()) + .highPriorityGesture(dragGesture) + ScrollView { content - .frame(maxWidth: .infinity, maxHeight: .infinity) } .scrollContentBackground(.hidden) - .safeAreaPadding(.top, 0) // ScrollView 상단 패딩 제거 - .gesture(dragGesture) } .frame(height: sheetHeight) .background( From 902bd904d9cafa634c6a59a17b19508cb4706c88 Mon Sep 17 00:00:00 2001 From: giljihun Date: Fri, 6 Feb 2026 15:01:02 +0900 Subject: [PATCH 11/14] =?UTF-8?q?style:=20=EB=B2=88=EB=93=A4=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20-=20=EC=8B=9C=ED=8A=B8=20=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A1=A4=20=EC=98=81=EC=97=AD=20=ED=95=84=ED=84=B0=ED=97=A4?= =?UTF-8?q?=EB=8D=94=EB=8A=94=20=EC=A0=9C=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Views/Create/BundleCreateView.swift | 1 + .../Edit/BundleEditView+SelectSheet.swift | 1 + .../Bundle/Views/Shared/DraggableSheet.swift | 8 ++++- .../Views/Shared/SelectBackgroundSheet.swift | 29 ++++++++----------- .../Views/Shared/SelectCarabinerSheet.swift | 27 +++++++---------- 5 files changed, 32 insertions(+), 34 deletions(-) diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift index e8e482b3..e4d1570c 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift @@ -232,6 +232,7 @@ extension BundleCreateView { DraggableSheet( sheetHeight: $sheetHeight, + header: BundleSheetFilterBar(viewModel: bundleVM), content: itemSheetContent, onDismiss: { showItemSheet = false diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+SelectSheet.swift b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+SelectSheet.swift index ddbd5745..55203263 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+SelectSheet.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+SelectSheet.swift @@ -23,6 +23,7 @@ extension BundleEditView { DraggableSheet( sheetHeight: $sheetHeight, + header: BundleSheetFilterBar(viewModel: bundleVM), content: itemSheetContent, onDismiss: { showItemSheet = false diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Shared/DraggableSheet.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/DraggableSheet.swift index a1253cd3..c8ba47c8 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Shared/DraggableSheet.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/DraggableSheet.swift @@ -7,8 +7,9 @@ import SwiftUI -struct DraggableSheet: View { +struct DraggableSheet: View { @Binding var sheetHeight: CGFloat + let header: Header let content: Content var onDismiss: (() -> Void)? = nil @@ -41,6 +42,11 @@ struct DraggableSheet: View { .contentShape(Rectangle()) .highPriorityGesture(dragGesture) + // 고정 헤더 (스크롤 안 됨) + header + .padding(.bottom, 10) + + // 스크롤 콘텐츠 ScrollView { content } diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Shared/SelectBackgroundSheet.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/SelectBackgroundSheet.swift index 6ce484cf..4e393523 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Shared/SelectBackgroundSheet.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/SelectBackgroundSheet.swift @@ -57,27 +57,22 @@ struct SelectBackgroundSheet: View { } var body: some View { - VStack(spacing: 20) { - // 필터바 - BundleSheetFilterBar(viewModel: viewModel) + // 그리드만 (필터바는 DraggableSheet header로 이동) + LazyVGrid(columns: gridColumns, spacing: 20) { + ForEach(filteredAndSortedBackgrounds) { bg in + BackgroundCell(background: bg, isSelected: (bg == selectedBG)) + .onTapGesture { + onBackgroundTap(bg) - // 그리드 - LazyVGrid(columns: gridColumns, spacing: 20) { - ForEach(filteredAndSortedBackgrounds) { bg in - BackgroundCell(background: bg, isSelected: (bg == selectedBG)) - .onTapGesture { - onBackgroundTap(bg) - - // 무료이고, 유저가 보유x인 경우에만 바로 추가 - if !bg.isOwned && bg.background.isFree { - Task { - await viewModel.addBackgroundToUser(backgroundName: bg.background.backgroundName, userManager: UserManager.shared) - } + // 무료이고, 유저가 보유x인 경우에만 바로 추가 + if !bg.isOwned && bg.background.isFree { + Task { + await viewModel.addBackgroundToUser(backgroundName: bg.background.backgroundName, userManager: UserManager.shared) } } - } + } } - .padding(.horizontal, 20) } + .padding(.horizontal, 20) } } diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Shared/SelectCarabinerSheet.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/SelectCarabinerSheet.swift index da8dca10..4c4fc3ea 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Shared/SelectCarabinerSheet.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/SelectCarabinerSheet.swift @@ -57,26 +57,21 @@ struct SelectCarabinerSheet: View { } var body: some View { - VStack(spacing: 20) { - // 필터바 - BundleSheetFilterBar(viewModel: viewModel) + // 그리드만 (필터바는 DraggableSheet header로 이동) + LazyVGrid(columns: gridColumns, spacing: 20) { + ForEach(filteredAndSortedCarabiners) { cb in + CarabinerCell(carabiner: cb, isSelected: (selectedCarabiner == cb)) + .onTapGesture { + onCarabinerTap(cb) - // 그리드 - LazyVGrid(columns: gridColumns, spacing: 20) { - ForEach(filteredAndSortedCarabiners) { cb in - CarabinerCell(carabiner: cb, isSelected: (selectedCarabiner == cb)) - .onTapGesture { - onCarabinerTap(cb) - - if !cb.isOwned && cb.carabiner.isFree { - Task { - await viewModel.addCarabinerToUser(carabinerName: cb.carabiner.carabinerName, userManager: UserManager.shared) - } + if !cb.isOwned && cb.carabiner.isFree { + Task { + await viewModel.addCarabinerToUser(carabinerName: cb.carabiner.carabinerName, userManager: UserManager.shared) } } - } + } } - .padding(.horizontal, 20) } + .padding(.horizontal, 20) } } From 9b5bb029e8fb4d68703d7ff78328c8799ddf9743 Mon Sep 17 00:00:00 2001 From: giljihun Date: Fri, 6 Feb 2026 15:22:56 +0900 Subject: [PATCH 12/14] =?UTF-8?q?style:=20=EB=B2=88=EB=93=A4=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=8B=9C=ED=8A=B8=20-=20=EC=95=84=EC=9D=B4?= =?UTF-8?q?=ED=85=9C=EB=93=A4=20=ED=9D=B0=20=EB=B0=B0=EA=B2=BD=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(=EB=A1=9C=EB=94=A9=EC=8B=9C=EC=97=90=EB=8F=84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Presentation/Bundle/Views/Shared/BackgroundCell.swift | 1 + .../Bundle/Views/Shared/BundleSheetFilterBar.swift | 1 - .../Presentation/Bundle/Views/Shared/CarabinerCell.swift | 3 ++- .../Presentation/Bundle/Views/Shared/DraggableSheet.swift | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Shared/BackgroundCell.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/BackgroundCell.swift index 06d888fb..a1654583 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Shared/BackgroundCell.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/BackgroundCell.swift @@ -27,6 +27,7 @@ struct BackgroundCell: View { } } .frame(width: threeSquareGridCellSize, height: threeSquareGridCellSize) + .background(.white100) .clipShape(RoundedRectangle(cornerRadius: 10)) .overlay( RoundedRectangle(cornerRadius: 10) diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleSheetFilterBar.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleSheetFilterBar.swift index 0924d252..38807b09 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleSheetFilterBar.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleSheetFilterBar.swift @@ -41,7 +41,6 @@ struct BundleSheetFilterBar: View { Image(systemName: "chevron.down") .foregroundColor(.gray500) } - .padding(.horizontal, Spacing.gap) .padding(.vertical, Spacing.sm) .frame(height: 34) } diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Shared/CarabinerCell.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/CarabinerCell.swift index 50c88102..bb6b8123 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Shared/CarabinerCell.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/CarabinerCell.swift @@ -32,10 +32,11 @@ struct CarabinerCell: View { } .padding(3.55) .frame(width: threeSquareGridCellSize, height: threeSquareGridCellSize) + .background(.white100) + .clipShape(RoundedRectangle(cornerRadius: 10)) .overlay( RoundedRectangle(cornerRadius: 10) .strokeBorder(isSelected ? .mainOpacity80 : .clear, lineWidth: 1.8) - ) // 유료 재화 표시 diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Shared/DraggableSheet.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/DraggableSheet.swift index c8ba47c8..da927bbf 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Shared/DraggableSheet.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/DraggableSheet.swift @@ -54,7 +54,7 @@ struct DraggableSheet: View { } .frame(height: sheetHeight) .background( - RoundedRectangle(cornerRadius: 30) + RoundedRectangle(cornerRadius: 34) .stroke(.gray50, lineWidth: 1) .shadow(color: .black15, radius: 9, x: 0, y: 0) ) From bf0ea95c15034d9e82f3afe2308e27c32a8697cf Mon Sep 17 00:00:00 2001 From: giljihun Date: Fri, 6 Feb 2026 15:31:01 +0900 Subject: [PATCH 13/14] =?UTF-8?q?feat:=20=EB=B2=88=EB=93=A4=EB=A7=8C?= =?UTF-8?q?=EB=93=A4=EB=8B=A4=EA=B0=80=20=EB=82=98=EA=B0=88=20=EA=B2=BD?= =?UTF-8?q?=EC=9A=B0,=20=EC=84=A0=ED=83=9D=EA=B0=92=20=EC=B4=88=EA=B8=B0?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Presentation/Bundle/ViewModels/BundleViewModel.swift | 8 ++++++++ .../Bundle/Views/Create/BundleCreateView.swift | 1 + .../Presentation/Bundle/Views/Edit/BundleEditView.swift | 1 + 3 files changed, 10 insertions(+) diff --git a/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel.swift b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel.swift index 7e2b30ee..97e202c6 100644 --- a/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel.swift +++ b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel.swift @@ -82,6 +82,14 @@ class BundleViewModel { var sheetShowOwnedOnly: Bool = false var showSheetSortSheet: Bool = false + /// 시트 필터/정렬 상태 초기화 + func resetSheetFilterState() { + sheetSortOrder = "최신순" + sheetShowFreeOnly = false + sheetShowOwnedOnly = false + showSheetSortSheet = false + } + // MARK: - 편집 화면용 데이터 var newSelectedBackground: BackgroundViewData? diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift index e4d1570c..9b243b3d 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift @@ -139,6 +139,7 @@ struct BundleCreateView: View { await refreshData() } TabBarManager.hide() + bundleVM.resetSheetFilterState() } .sheet(isPresented: $showKeyringSheet) { keyringSheetContent diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView.swift b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView.swift index 68808d79..72983283 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView.swift @@ -102,6 +102,7 @@ struct BundleEditView: View { await bundleVM.refreshEditData() } TabBarManager.hide() + bundleVM.resetSheetFilterState() // 화면 첫 진입 시 배경 시트를 보여줌 if !showItemSheet { isBackgroundMode = true From ce7383789d8f09445c39f12ee9081f71cbb8e4a9 Mon Sep 17 00:00:00 2001 From: giljihun Date: Fri, 6 Feb 2026 17:27:42 +0900 Subject: [PATCH 14/14] =?UTF-8?q?feat:=20Bundle=20=EC=83=9D=EC=84=B1/?= =?UTF-8?q?=ED=8E=B8=EC=A7=91=20=ED=99=94=EB=A9=B4=20=EC=83=81=ED=83=9C=20?= =?UTF-8?q?=EC=B4=88=EA=B8=B0=ED=99=94=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BundleViewModel에 resetEditState() 추가 (배경/카라비너/키링 선택 상태 초기화 용도) - BundleCreateView, BundleEditView onDisappear에서 상태 초기화 - 코인 충전 시 선택 상태 저장/복원 로직 추가 - saveCurrentSelection(): UserDefaults에 임시 저장 - restoreSelectionIfNeeded(): 복원 후 삭제 - BundleEditView 초기화 시 항상 현재 뭉치 기준으로 설정 --- .../Bundle/ViewModels/BundleViewModel.swift | 35 ++++++++ .../Views/Create/BundleCreateView.swift | 85 ++++++++++++------- .../Edit/BundleEditView+Initialization.swift | 16 ++-- .../Views/Edit/BundleEditView+Purchase.swift | 21 ++++- .../Bundle/Views/Edit/BundleEditView.swift | 2 + .../Views/Shared/BundlePurchaseCartItem.swift | 66 +++++++++++--- .../Bundle/Views/Shared/CarabinerCell.swift | 7 +- 7 files changed, 170 insertions(+), 62 deletions(-) diff --git a/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel.swift b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel.swift index 97e202c6..9e568b45 100644 --- a/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel.swift +++ b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel.swift @@ -90,6 +90,41 @@ class BundleViewModel { showSheetSortSheet = false } + /// 편집 화면 상태 초기화 (편집 화면 나갈 때 호출) + func resetEditState() { + newSelectedBackground = nil + newSelectedCarabiner = nil + selectedKeyrings = [:] + keyringOrder = [] + selectedKeyringPosition = 0 + } + + /// 현재 선택 상태를 UserDefaults에 임시 저장 (코인 충전 등 화면 이동 전) + func saveCurrentSelection() { + if let bg = newSelectedBackground { + UserDefaults.standard.set(bg.background.id, forKey: "tempSelectedBackgroundId") + } + if let cb = newSelectedCarabiner { + UserDefaults.standard.set(cb.carabiner.id, forKey: "tempSelectedCarabinerId") + } + } + + /// UserDefaults에서 선택 상태 복원 (복원 후 삭제) + func restoreSelectionIfNeeded() { + if let savedBackgroundId = UserDefaults.standard.string(forKey: "tempSelectedBackgroundId") { + if let restoredBackground = backgroundViewData.first(where: { $0.background.id == savedBackgroundId }) { + newSelectedBackground = restoredBackground + } + UserDefaults.standard.removeObject(forKey: "tempSelectedBackgroundId") + } + if let savedCarabinerId = UserDefaults.standard.string(forKey: "tempSelectedCarabinerId") { + if let restoredCarabiner = carabinerViewData.first(where: { $0.carabiner.id == savedCarabinerId }) { + newSelectedCarabiner = restoredCarabiner + } + UserDefaults.standard.removeObject(forKey: "tempSelectedCarabinerId") + } + } + // MARK: - 편집 화면용 데이터 var newSelectedBackground: BackgroundViewData? diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift index 9b243b3d..15579717 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift @@ -141,6 +141,9 @@ struct BundleCreateView: View { TabBarManager.hide() bundleVM.resetSheetFilterState() } + .onDisappear { + bundleVM.resetEditState() + } .sheet(isPresented: $showKeyringSheet) { keyringSheetContent } @@ -425,21 +428,6 @@ extension BundleCreateView { // 배경 데이터 로드 await withCheckedContinuation { continuation in bundleVM.fetchAllBackgrounds { _ in - if self.bundleVM.newSelectedBackground == nil { - // 공방에서 미리 선택된 배경이 있으면 해당 배경 선택 - if let preSelectedId = bundleVM.preSelectedBackgroundId { - self.bundleVM.newSelectedBackground = bundleVM.backgroundViewData.first { bg in - bg.background.id == preSelectedId - } - bundleVM.preSelectedBackgroundId = nil // 사용 후 초기화 - } - // 미리 선택된 배경이 없으면 "퍼플키치"를 기본으로 선택, 없으면 첫 번째 선택 - if self.bundleVM.newSelectedBackground == nil { - self.bundleVM.newSelectedBackground = bundleVM.backgroundViewData.first { bg in - bg.background.backgroundName == "퍼플키치" - } ?? bundleVM.backgroundViewData.first - } - } continuation.resume() } } @@ -447,25 +435,47 @@ extension BundleCreateView { // 카라비너 데이터 로드 await withCheckedContinuation { continuation in bundleVM.fetchAllCarabiners { _ in - if self.bundleVM.newSelectedCarabiner == nil { - // 공방에서 미리 선택된 카라비너가 있으면 해당 카라비너 선택 - if let preSelectedId = bundleVM.preSelectedCarabinerId { - self.bundleVM.newSelectedCarabiner = bundleVM.carabinerViewData.first { cb in - cb.carabiner.id == preSelectedId - } - bundleVM.preSelectedCarabinerId = nil // 사용 후 초기화 - } - // 미리 선택된 카라비너가 없으면 "웰컴 키치"를 기본으로 선택, 없으면 첫 번째 선택 - if self.bundleVM.newSelectedCarabiner == nil { - self.bundleVM.newSelectedCarabiner = bundleVM.carabinerViewData.first { cb in - cb.carabiner.carabinerName == "웰컴 키치" - } ?? bundleVM.carabinerViewData.first - } - } continuation.resume() } } + // 코인 충전 후 복귀 시 저장된 선택 복원 + bundleVM.restoreSelectionIfNeeded() + + // 배경 선택 (복원된 값이 없을 때만) + if bundleVM.newSelectedBackground == nil { + // 공방에서 미리 선택된 배경이 있으면 해당 배경 선택 + if let preSelectedId = bundleVM.preSelectedBackgroundId { + bundleVM.newSelectedBackground = bundleVM.backgroundViewData.first { bg in + bg.background.id == preSelectedId + } + bundleVM.preSelectedBackgroundId = nil + } + // 미리 선택된 배경이 없으면 "퍼플키치"를 기본으로 선택 + if bundleVM.newSelectedBackground == nil { + bundleVM.newSelectedBackground = bundleVM.backgroundViewData.first { bg in + bg.background.backgroundName == "퍼플키치" + } ?? bundleVM.backgroundViewData.first + } + } + + // 카라비너 선택 (복원된 값이 없을 때만) + if bundleVM.newSelectedCarabiner == nil { + // 공방에서 미리 선택된 카라비너가 있으면 해당 카라비너 선택 + if let preSelectedId = bundleVM.preSelectedCarabinerId { + bundleVM.newSelectedCarabiner = bundleVM.carabinerViewData.first { cb in + cb.carabiner.id == preSelectedId + } + bundleVM.preSelectedCarabinerId = nil + } + // 미리 선택된 카라비너가 없으면 "웰컴 키치"를 기본으로 선택 + if bundleVM.newSelectedCarabiner == nil { + bundleVM.newSelectedCarabiner = bundleVM.carabinerViewData.first { cb in + cb.carabiner.carabinerName == "웰컴 키치" + } ?? bundleVM.carabinerViewData.first + } + } + // 키링 데이터 로드 await withCheckedContinuation { continuation in collectionVM.fetchUserCollectionData(uid: uid) { success in @@ -520,6 +530,7 @@ extension BundleCreateView { onCharge: { showPurchaseFailAlert = false purchaseFailScale = 0.3 + bundleVM.saveCurrentSelection() router.push(.coinCharge) } ) @@ -554,10 +565,20 @@ extension BundleCreateView { // 구매할 아이템 목록 VStack(spacing: 20) { if let bg = bundleVM.newSelectedBackground, !bg.isOwned && bg.background.price > 0 { - BundlePurchaseCartItem(name: bg.background.backgroundName, type: "배경", price: bg.background.price) + BundlePurchaseCartItem( + imageURL: bg.background.backgroundImage, + 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) + BundlePurchaseCartItem( + imageURL: cb.carabiner.carabinerImage.first ?? "", + name: cb.carabiner.carabinerName, + type: "카라비너", + price: cb.carabiner.price + ) } } .padding(.horizontal, 20) diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+Initialization.swift b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+Initialization.swift index 52c0b758..23e5c4c1 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+Initialization.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+Initialization.swift @@ -37,21 +37,19 @@ extension BundleEditView { func loadBackgroundAndCarabiner() async { await withCheckedContinuation { continuation in bundleVM.fetchAllBackgrounds { _ in + // 현재 뭉치의 배경으로 항상 초기화 if let selectedBundle = bundleVM.selectedBundle { - if bundleVM.newSelectedBackground == nil { - bundleVM.newSelectedBackground = bundleVM.backgroundViewData.first { bgData in - bgData.background.id == selectedBundle.selectedBackground - } + bundleVM.newSelectedBackground = bundleVM.backgroundViewData.first { bgData in + bgData.background.id == selectedBundle.selectedBackground } } self.restoreBackgroundSelection() - + bundleVM.fetchAllCarabiners { _ in + // 현재 뭉치의 카라비너로 항상 초기화 if let selectedBundle = bundleVM.selectedBundle { - if bundleVM.newSelectedCarabiner == nil { - bundleVM.newSelectedCarabiner = bundleVM.carabinerViewData.first { cbData in - cbData.carabiner.id == selectedBundle.selectedCarabiner - } + bundleVM.newSelectedCarabiner = bundleVM.carabinerViewData.first { cbData in + cbData.carabiner.id == selectedBundle.selectedCarabiner } } self.restoreCarabinerSelection() diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+Purchase.swift b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+Purchase.swift index e2775967..84f8c106 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+Purchase.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+Purchase.swift @@ -9,7 +9,7 @@ import SwiftUI extension BundleEditView { var purchaseSheetView: some View { - VStack(spacing: 12) { + VStack(spacing: 0) { // 상단 섹션 - 닫기 버튼, 타이틀 HStack { Button { @@ -26,15 +26,26 @@ extension BundleEditView { Spacer() } .padding(EdgeInsets(top: 30, leading: 20, bottom: 10, trailing: 20)) + .padding(.bottom, 12) // 구매할 아이템 목록 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) + BundlePurchaseCartItem( + imageURL: bg.background.backgroundImage, + 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) + BundlePurchaseCartItem( + imageURL: cb.carabiner.carabinerImage.first ?? "", + name: cb.carabiner.carabinerName, + type: "카라비너", + price: cb.carabiner.price + ) } } .padding(.horizontal, 20) @@ -45,11 +56,13 @@ extension BundleEditView { Text("내 보유 : ") .typography(.suit15M25) .foregroundStyle(.black100) - .padding(.vertical, 4.5) Text("\(UserManager.shared.currentUser?.coin ?? 0)") .typography(.nanum16EB) .foregroundStyle(.main500) + .padding(.top, 2) } + .padding(.top, 20) + purchaseButton .padding(.horizontal, 33.2) .adaptiveBottomPadding() diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView.swift b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView.swift index 72983283..a778ba16 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView.swift @@ -111,6 +111,8 @@ struct BundleEditView: View { } .onDisappear { isNavigatingAway = false + // 편집 화면 나갈 때 상태 초기화 + bundleVM.resetEditState() } .ignoresSafeArea() .onChange(of: bundleVM.newSelectedBackground) { _, newBackground in diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundlePurchaseCartItem.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundlePurchaseCartItem.swift index bca5d296..4e456d73 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundlePurchaseCartItem.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundlePurchaseCartItem.swift @@ -6,33 +6,73 @@ // import SwiftUI +import NukeUI /// 구매 시트에서 사용하는 장바구니 아이템 행 struct BundlePurchaseCartItem: View { + let imageURL: String let name: String let type: String let price: Int var body: some View { - HStack(spacing: 6) { - Image(.selectedIcon) + HStack(spacing: 12) { + // 아이템 썸네일 이미지 + if type == "카라비너" { + LazyImage(url: URL(string: imageURL)) { state in + if let image = state.image { + image + .resizable() + .scaledToFit() + } else if state.isLoading { + Color.gray50 + } else { + Color.gray50 + } + } + .padding(2) + .frame(width: 60, height: 60) + .background(.white100) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } else { + LazyImage(url: URL(string: imageURL)) { state in + if let image = state.image { + image + .resizable() + .scaledToFill() + } else if state.isLoading { + Color.gray50 + } else { + Color.gray50 + } + } + .frame(width: 60, height: 60) + .background(.white100) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } - Text(name) - .typography(.suit16B) - .foregroundStyle(.black100) - .padding(.trailing, 7) + // 이름 + 카테고리 + VStack(alignment: .leading, spacing: 4) { + Text(name) + .typography(.suit16B) + .foregroundStyle(.black100) - Text(type) - .typography(.suit13M) - .foregroundStyle(.gray400) + Text(type) + .typography(.suit13M) + .foregroundStyle(.gray400) + } Spacer() - Text("\(price)") - .typography(.nanum16EB) - .foregroundStyle(.main500) + // 가격 + HStack(spacing: 4) { + Image(.myCoinMini) + Text("\(price)") + .typography(.nanum16EB) + .foregroundStyle(.main500) + } } - .padding(.vertical, 15) + .padding(.vertical, 12) .padding(.horizontal, 16) .background( RoundedRectangle(cornerRadius: 12) diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Shared/CarabinerCell.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/CarabinerCell.swift index bb6b8123..2531edaa 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Shared/CarabinerCell.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/CarabinerCell.swift @@ -43,15 +43,14 @@ struct CarabinerCell: View { VStack { HStack { // 유료 아이콘 - Image(.paidIcon) - .padding(.top, 3) + Image(.myCoinMini) .opacity(carabiner.carabiner.isFree ? 0 : 1) Spacer() } + .padding(.horizontal, 8) + .padding(.top, 8) Spacer() } - .padding(.top, 3) - .padding(.leading, 7) // 오른쪽 상단: 유료 아이템만 표시 (보유/가격) if !carabiner.carabiner.isFree {