diff --git a/Keychy/Keychy.xcodeproj/project.pbxproj b/Keychy/Keychy.xcodeproj/project.pbxproj index f598c075..849310f9 100644 --- a/Keychy/Keychy.xcodeproj/project.pbxproj +++ b/Keychy/Keychy.xcodeproj/project.pbxproj @@ -3235,7 +3235,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1.0; + MARKETING_VERSION = 1.1.1; PRODUCT_BUNDLE_IDENTIFIER = com.KeychyOfficial.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3284,7 +3284,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1.0; + MARKETING_VERSION = 1.1.1; PRODUCT_BUNDLE_IDENTIFIER = com.KeychyOfficial.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3324,7 +3324,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.1.0; + MARKETING_VERSION = 1.1.1; PRODUCT_BUNDLE_IDENTIFIER = com.KeychyOfficial.app.WidgetKeychy; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3367,7 +3367,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.1.0; + MARKETING_VERSION = 1.1.1; PRODUCT_BUNDLE_IDENTIFIER = com.KeychyOfficial.app.WidgetKeychy; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel.swift b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel.swift index d4e4b139..bb28ed62 100644 --- a/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel.swift +++ b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel.swift @@ -60,6 +60,7 @@ class BundleViewModel { // MARK: - 뭉치 캡쳐 이미지 var bundleCapturedImage: Data? + var bundleWidgetImage: Data? // MARK: - 현재 선택된 뭉치 diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+Capture.swift b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+Capture.swift index 96a97c3e..50d5d352 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+Capture.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+Capture.swift @@ -104,7 +104,7 @@ extension BundleCreateView { carabinerFrontURL = nil } - // 씬 캡처 + // 1. 배경 포함 캡처 (앱용) if let pngData = await MultiKeyringCaptureScene.captureBundleImage( keyringDataList: keyringDataList, backgroundImageURL: background.backgroundImage, @@ -121,6 +121,23 @@ extension BundleCreateView { } } + // 2. 배경 없이 캡처 (위젯용 - 투명 여백 제거) + let widgetData = await MultiKeyringCaptureScene.captureBundleImage( + keyringDataList: keyringDataList, + backgroundImageURL: nil, + carabinerBackImageURL: carabinerBackURL, + carabinerFrontImageURL: carabinerFrontURL, + carabinerType: carabinerType, + carabinerId: carabiner.id ?? "", + carabinerX: carabiner.carabinerX, + carabinerY: carabiner.carabinerY, + carabinerWidth: carabiner.carabinerWidth, + trimTransparentEdges: true + ) + await MainActor.run { + bundleVM.bundleWidgetImage = widgetData + } + // 캡처 완료 후 다음 화면으로 이동 await MainActor.run { isCapturing = false diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+Initialization.swift b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+Initialization.swift index 590e56ec..25d41f2f 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+Initialization.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+Initialization.swift @@ -6,14 +6,24 @@ // import SwiftUI +import Nuke // MARK: - 데이터 초기화 extension BundleCreateView { /// 초기 데이터 로딩 func initializeData() async { - // 사용자가 소유한 배경과 카라비너 데이터를 가져옴 await loadUserOwnedItems() + // 시트 이미지 프리페칭 (백그라운드에서 Nuke 캐시에 미리 로드) + prefetchSheetImages() + } + + /// 배경/카라비너 썸네일을 Nuke 캐시에 미리 로드 + private func prefetchSheetImages() { + let bgURLs = bundleVM.backgroundViewData.compactMap { URL(string: $0.background.backgroundImage) } + let cbURLs = bundleVM.carabinerViewData.compactMap { URL(string: $0.carabiner.carabinerImage[0]) } + let prefetcher = ImagePrefetcher() + prefetcher.startPrefetching(with: bgURLs + cbURLs) } /// 화면이 다시 나타날 때 데이터 새로고침 diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+SelectSheet.swift b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+SelectSheet.swift index 7715e2fb..4fd64e35 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+SelectSheet.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+SelectSheet.swift @@ -29,7 +29,15 @@ extension BundleCreateView { isSelected: selectedPosition == index, action: { selectedPosition = index - showKeyringSheet = true + if showItemSheet { + // 배경/카라비너 시트 닫기 → 닫힌 후 키링 시트 표시 + showItemSheet = false + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + showKeyringSheet = true + } + } else { + showKeyringSheet = true + } } ) .position(x: viewX, y: viewY) @@ -45,42 +53,46 @@ extension BundleCreateView { var sheetContent: some View { ZStack(alignment: .bottom) { Color.clear - - if showItemSheet { - // 시트가 있을 때: 셀렉터 + 시트가 함께 움직임 - VStack(spacing: 0) { - BundleSheetToggleButtons( - showItemSheet: $showItemSheet, - isBackgroundMode: $isBackgroundMode - ) - .padding(.bottom, 10) - - DraggableSheet( - sheetHeight: $sheetHeight, - header: BundleSheetFilterBar(viewModel: bundleVM), - content: itemSheetContent, - onDismiss: { - showItemSheet = false - } - ) + + // 시트 레이어 (항상 존재, 오프셋으로 숨김 → 즉시 반응) + DraggableSheet( + sheetHeight: $sheetHeight, + header: BundleSheetFilterBar(viewModel: bundleVM), + content: itemSheetContent, + onDismiss: { + showItemSheet = false } - .transition(.move(edge: .bottom)) + ) + .offset(y: showItemSheet ? 0 : sheetHeight) + .allowsHitTesting(showItemSheet) + + // 버튼 레이어 (matchedGeometryEffect로 위치만 보간) + if showItemSheet { + BundleSheetToggleButtons( + showItemSheet: $showItemSheet, + isBackgroundMode: $isBackgroundMode + ) + .matchedGeometryEffect(id: "toggleButtons", in: sheetButtonNamespace) + .padding(.bottom, sheetHeight + 10) } else { - // 시트가 없을 때: 셀렉터만 하단에 고정 BundleSheetToggleButtons( showItemSheet: $showItemSheet, isBackgroundMode: $isBackgroundMode ) + .matchedGeometryEffect(id: "toggleButtons", in: sheetButtonNamespace) .padding(.bottom, 50) - .transition(.identity) } } - .animation(.easeInOut(duration: 0.25), value: showItemSheet) + .animation(.easeOut(duration: 0.2), value: showItemSheet) + .onChange(of: showItemSheet) { _, isShowing in + if isShowing { + sheetHeight = UIScreen.main.bounds.height * 0.4 + } + } } - @ViewBuilder var itemSheetContent: some View { - if isBackgroundMode { + ZStack(alignment: .top) { SelectBackgroundSheet( viewModel: bundleVM, selectedBG: bundleVM.newSelectedBackground, @@ -88,7 +100,9 @@ extension BundleCreateView { bundleVM.newSelectedBackground = bg } ) - } else { + .opacity(isBackgroundMode ? 1 : 0) + .allowsHitTesting(isBackgroundMode) + SelectCarabinerSheet( viewModel: bundleVM, selectedCarabiner: bundleVM.newSelectedCarabiner, @@ -97,6 +111,8 @@ extension BundleCreateView { bundleVM.newSelectedCarabiner = carabiner } ) + .opacity(isBackgroundMode ? 0 : 1) + .allowsHitTesting(!isBackgroundMode) } } diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift index 2c40c0b5..41d60e2f 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift @@ -24,12 +24,13 @@ struct BundleCreateView: View { @Bindable var bundleVM: BundleViewModel // 시트 활성화 상태 + @Namespace var sheetButtonNamespace @State var showItemSheet: Bool = false @State var isBackgroundMode: Bool = true // true: 배경, false: 카라비너 @State var showKeyringSheet: Bool = false - // 시트 높이 - @State var sheetHeight: CGFloat = 360 + // 시트 높이 (DraggableSheet.onAppear에서 mediumHeight로 갱신됨) + @State var sheetHeight: CGFloat = UIScreen.main.bounds.height * 0.4 // 키링 선택 상태 @State var selectedKeyrings: [Int: Keyring] = [:] @@ -97,6 +98,9 @@ struct BundleCreateView: View { keyringButtons(carabiner: cb.carabiner) } .blur(radius: showPurchaseSuccessAlert || isCapturing ? 10 : 0) + .onTapGesture { + if showItemSheet { showItemSheet = false } + } // 하단 셀렉터 + 시트 sheetContent diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleNameInputView.swift b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleNameInputView.swift index 44ad6ce1..8673c272 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleNameInputView.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleNameInputView.swift @@ -223,7 +223,8 @@ extension BundleNameInputView { // Firebase 저장 성공 후 ViewModel의 이미지를 캐시에 저장 bundleVM.saveBundleImageToCache( bundleId: bundleId, - bundleName: bundleNameToSave + bundleName: bundleNameToSave, + widgetImageData: bundleVM.bundleWidgetImage ) isUploading = false diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+Initialization.swift b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+Initialization.swift index d724f702..4436b0ac 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+Initialization.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+Initialization.swift @@ -7,15 +7,26 @@ import SwiftUI import FirebaseFirestore +import Nuke extension BundleEditView { func initializeData() async { resetSceneState() - + await loadUserKeyring() - + await loadBackgroundAndCarabiner() - + + // 시트 이미지 프리페칭 (백그라운드에서 Nuke 캐시에 미리 로드) + prefetchSheetImages() + } + + /// 배경/카라비너 썸네일을 Nuke 캐시에 미리 로드 + private func prefetchSheetImages() { + let bgURLs = bundleVM.backgroundViewData.compactMap { URL(string: $0.background.backgroundImage) } + let cbURLs = bundleVM.carabinerViewData.compactMap { URL(string: $0.carabiner.carabinerImage[0]) } + let prefetcher = ImagePrefetcher() + prefetcher.startPrefetching(with: bgURLs + cbURLs) } func resetSceneState() { diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+SelectSheet.swift b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+SelectSheet.swift index 2105941e..7088a02d 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+SelectSheet.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+SelectSheet.swift @@ -12,41 +12,45 @@ extension BundleEditView { ZStack(alignment: .bottom) { Color.clear - if showItemSheet { - // 시트가 있을 때: 셀렉터 + 시트가 함께 움직임 - VStack(spacing: 0) { - BundleSheetToggleButtons( - showItemSheet: $showItemSheet, - isBackgroundMode: $isBackgroundMode - ) - .padding(.bottom, 10) - - DraggableSheet( - sheetHeight: $sheetHeight, - header: BundleSheetFilterBar(viewModel: bundleVM), - content: itemSheetContent, - onDismiss: { - showItemSheet = false - } - ) + // 시트 레이어 (항상 존재, 오프셋으로 숨김 → 즉시 반응) + DraggableSheet( + sheetHeight: $sheetHeight, + header: BundleSheetFilterBar(viewModel: bundleVM), + content: itemSheetContent, + onDismiss: { + showItemSheet = false } - .transition(.move(edge: .bottom)) + ) + .offset(y: showItemSheet ? 0 : sheetHeight) + .allowsHitTesting(showItemSheet) + + // 버튼 레이어 (matchedGeometryEffect로 위치만 보간) + if showItemSheet { + BundleSheetToggleButtons( + showItemSheet: $showItemSheet, + isBackgroundMode: $isBackgroundMode + ) + .matchedGeometryEffect(id: "toggleButtons", in: sheetButtonNamespace) + .padding(.bottom, sheetHeight + 10) } else { - // 시트가 없을 때: 셀렉터만 하단에 고정 BundleSheetToggleButtons( showItemSheet: $showItemSheet, isBackgroundMode: $isBackgroundMode ) + .matchedGeometryEffect(id: "toggleButtons", in: sheetButtonNamespace) .padding(.bottom, 50) - .transition(.identity) } } - .animation(.easeInOut(duration: 0.25), value: showItemSheet) + .animation(.easeOut(duration: 0.2), value: showItemSheet) + .onChange(of: showItemSheet) { _, isShowing in + if isShowing { + sheetHeight = UIScreen.main.bounds.height * 0.4 + } + } } - @ViewBuilder private var itemSheetContent: some View { - if isBackgroundMode { + ZStack(alignment: .top) { SelectBackgroundSheet( viewModel: bundleVM, selectedBG: bundleVM.newSelectedBackground, @@ -54,7 +58,9 @@ extension BundleEditView { bundleVM.newSelectedBackground = bg } ) - } else { + .opacity(isBackgroundMode ? 1 : 0) + .allowsHitTesting(isBackgroundMode) + SelectCarabinerSheet( viewModel: bundleVM, selectedCarabiner: bundleVM.newSelectedCarabiner, @@ -63,6 +69,8 @@ extension BundleEditView { showChangeCarabinerAlert = true } ) + .opacity(isBackgroundMode ? 0 : 1) + .allowsHitTesting(!isBackgroundMode) } } diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView.swift b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView.swift index 427a2816..848e16cb 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView.swift @@ -25,6 +25,7 @@ struct BundleEditView: View { @State var isCapturing: Bool = false // MARK: - Sheet + @Namespace var sheetButtonNamespace @State var showItemSheet: Bool = false @State var isBackgroundMode: Bool = true // true: 배경, false: 카라비너 @State var showPurchaseSheet = false @@ -52,7 +53,8 @@ struct BundleEditView: View { ] // 시트 높이 (화면의 약 43%에 해당) - @State var sheetHeight: CGFloat = 360 + // 시트 높이 (DraggableSheet.onAppear에서 mediumHeight로 갱신됨) + @State var sheetHeight: CGFloat = UIScreen.main.bounds.height * 0.4 @State var purchasesSuccessScale: CGFloat = 0.3 @State var purchaseFailScale: CGFloat = 0.3 let sheetHeightRatio: CGFloat = 0.43 @@ -171,6 +173,9 @@ struct BundleEditView: View { // navigationBar customNavigationBar } + .onTapGesture { + if showItemSheet { showItemSheet = false } + } } // MARK: - 키링 편집 씬 뷰 @@ -228,8 +233,18 @@ struct BundleEditView: View { isSelected: selectedPosition == index, action: { selectedPosition = index - withAnimation(.easeInOut) { - showSelectKeyringSheet = true + if showItemSheet { + // 배경/카라비너 시트 닫기 → 닫힌 후 키링 시트 표시 + showItemSheet = false + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + withAnimation(.easeInOut) { + showSelectKeyringSheet = true + } + } + } else { + withAnimation(.easeInOut) { + showSelectKeyringSheet = true + } } } ) diff --git a/Keychy/Keychy/Presentation/KeyringMaker/Shared/Components/TemplatePreviewComponents.swift b/Keychy/Keychy/Presentation/KeyringMaker/Shared/Components/TemplatePreviewComponents.swift index a1d358f1..aa512916 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Shared/Components/TemplatePreviewComponents.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Shared/Components/TemplatePreviewComponents.swift @@ -201,7 +201,7 @@ extension TemplatePreviewBody { LoadingAlert(type: .short40, message: nil) } } - .frame(maxHeight: 500) + .frame(maxWidth: .infinity, maxHeight: 500) } /// 템플릿 정보 섹션 diff --git a/Keychy/Keychy/Presentation/Workshop/Views/Keyring/WorkshopTemplateSelectSheet.swift b/Keychy/Keychy/Presentation/Workshop/Views/Keyring/WorkshopTemplateSelectSheet.swift index d84e7b25..a2c8bb83 100644 --- a/Keychy/Keychy/Presentation/Workshop/Views/Keyring/WorkshopTemplateSelectSheet.swift +++ b/Keychy/Keychy/Presentation/Workshop/Views/Keyring/WorkshopTemplateSelectSheet.swift @@ -192,7 +192,7 @@ struct WorkshopTemplateSelectSheet: View { Spacer() - // 보유 뱃지 (오른쪽 상단) - 보유 또는 무료일 때 + // 보유 뱃지 (오른쪽 상단) - 유료 + 보유일 때만 Text("보유") .typography(.suit13M) .foregroundStyle(.white100) @@ -202,7 +202,7 @@ struct WorkshopTemplateSelectSheet: View { RoundedRectangle(cornerRadius: 20) .fill(.black60) ) - .opacity(isOwned || template.isFree ? 1 : 0) + .opacity(isOwned && !template.isFree ? 1 : 0) } .padding(.top, 6) .padding(.horizontal, 7) diff --git a/functions/package-lock.json b/functions/package-lock.json index 72632e41..ff04a6a3 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -5910,9 +5910,9 @@ "peer": true }, "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0"