From cd7637c11e45e01970d44b530b95ad5791ac127a Mon Sep 17 00:00:00 2001 From: giljihun Date: Fri, 6 Feb 2026 19:13:36 +0900 Subject: [PATCH 01/10] =?UTF-8?q?refactor:=20Bundle=20=EB=B7=B0=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EA=B5=AC=EC=A1=B0=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (아니 왜 수정뷰는 익스텐션 잘되어있는데, 생성뷰는 뭉쳐있노) - BundleCreateView를 BundleEditView와 동일한 익스텐션 구조로 분리 - +SelectSheet, +Alert, +Purchase, +Initialization, +Capture - BundleEditView+Capture.swift 추가 (캡처 로직 분리) - BundleEditView+RestoreSelection.swift 삭제 (ViewModel로 이동) - BundleEditView.swift에서 중복 캡처 코드 제거 --- Keychy/Keychy.xcodeproj/project.pbxproj | 28 +- .../Views/Create/BundleCreateView+Alert.swift | 55 ++ .../Create/BundleCreateView+Capture.swift | 128 ++++ .../BundleCreateView+Initialization.swift | 127 ++++ .../Create/BundleCreateView+Purchase.swift | 156 ++++ .../Create/BundleCreateView+SelectSheet.swift | 191 +++++ .../Views/Create/BundleCreateView.swift | 665 +----------------- .../Views/Edit/BundleEditView+Alert.swift | 2 +- .../Views/Edit/BundleEditView+Capture.swift | 116 +++ .../Edit/BundleEditView+Initialization.swift | 5 +- .../BundleEditView+RestoreSelection.swift | 46 -- .../Edit/BundleEditView+SelectSheet.swift | 14 +- .../Bundle/Views/Edit/BundleEditView.swift | 104 --- .../surprisedAlert.imageset/Contents.json | 12 + .../surprisedAlert.imageset/suprisedAlert.pdf | Bin 0 -> 5601 bytes 15 files changed, 841 insertions(+), 808 deletions(-) create mode 100644 Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+Alert.swift create mode 100644 Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+Capture.swift create mode 100644 Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+Initialization.swift create mode 100644 Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+Purchase.swift create mode 100644 Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+SelectSheet.swift create mode 100644 Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+Capture.swift delete mode 100644 Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+RestoreSelection.swift create mode 100644 Keychy/Keychy/Resources/Assets.xcassets/21. Alert/surprisedAlert.imageset/Contents.json create mode 100644 Keychy/Keychy/Resources/Assets.xcassets/21. Alert/surprisedAlert.imageset/suprisedAlert.pdf diff --git a/Keychy/Keychy.xcodeproj/project.pbxproj b/Keychy/Keychy.xcodeproj/project.pbxproj index f7942fc2c..c2847e4f5 100644 --- a/Keychy/Keychy.xcodeproj/project.pbxproj +++ b/Keychy/Keychy.xcodeproj/project.pbxproj @@ -357,8 +357,13 @@ AA2146B12F15D43C0048D40E /* BundleEditView+Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA2146B02F15D43C0048D40E /* BundleEditView+Alert.swift */; }; AA2146B52F15D8490048D40E /* KeyringCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA2146B42F15D8490048D40E /* KeyringCell.swift */; }; AA2146B72F15E5B60048D40E /* BundleEditView+SelectSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA2146B62F15E5B60048D40E /* BundleEditView+SelectSheet.swift */; }; - AA2146B92F160E2B0048D40E /* BundleEditView+RestoreSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA2146B82F160E2B0048D40E /* BundleEditView+RestoreSelection.swift */; }; AA2146BB2F161D0C0048D40E /* BundleEditView+Initialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA2146BA2F161D0C0048D40E /* BundleEditView+Initialization.swift */; }; + BC0001072F35F00100000007 /* BundleCreateView+SelectSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0001012F35F00100000001 /* BundleCreateView+SelectSheet.swift */; }; + BC0001082F35F00100000008 /* BundleCreateView+Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0001022F35F00100000002 /* BundleCreateView+Alert.swift */; }; + BC0001092F35F00100000009 /* BundleCreateView+Purchase.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0001032F35F00100000003 /* BundleCreateView+Purchase.swift */; }; + BC00010A2F35F0010000000A /* BundleCreateView+Initialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0001042F35F00100000004 /* BundleCreateView+Initialization.swift */; }; + BC00010B2F35F0010000000B /* BundleCreateView+Capture.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0001052F35F00100000005 /* BundleCreateView+Capture.swift */; }; + BC00010C2F35F0010000000C /* BundleEditView+Capture.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0001062F35F00100000006 /* BundleEditView+Capture.swift */; }; AA3908F82EC8BF0400D87EEC /* BundleDetailView+SaveImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3908F72EC8BF0400D87EEC /* BundleDetailView+SaveImage.swift */; }; AA3909462EC9F29500D87EEC /* UIApplication+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3909452EC9F29500D87EEC /* UIApplication+Extension.swift */; }; AA39098E2ECA061700D87EEC /* GridItemSpacing.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA39098D2ECA061700D87EEC /* GridItemSpacing.swift */; }; @@ -808,8 +813,13 @@ AA2146B02F15D43C0048D40E /* BundleEditView+Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleEditView+Alert.swift"; sourceTree = ""; }; AA2146B42F15D8490048D40E /* KeyringCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyringCell.swift; sourceTree = ""; }; AA2146B62F15E5B60048D40E /* BundleEditView+SelectSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleEditView+SelectSheet.swift"; sourceTree = ""; }; - AA2146B82F160E2B0048D40E /* BundleEditView+RestoreSelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleEditView+RestoreSelection.swift"; sourceTree = ""; }; AA2146BA2F161D0C0048D40E /* BundleEditView+Initialization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleEditView+Initialization.swift"; sourceTree = ""; }; + BC0001012F35F00100000001 /* BundleCreateView+SelectSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleCreateView+SelectSheet.swift"; sourceTree = ""; }; + BC0001022F35F00100000002 /* BundleCreateView+Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleCreateView+Alert.swift"; sourceTree = ""; }; + BC0001032F35F00100000003 /* BundleCreateView+Purchase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleCreateView+Purchase.swift"; sourceTree = ""; }; + BC0001042F35F00100000004 /* BundleCreateView+Initialization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleCreateView+Initialization.swift"; sourceTree = ""; }; + BC0001052F35F00100000005 /* BundleCreateView+Capture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleCreateView+Capture.swift"; sourceTree = ""; }; + BC0001062F35F00100000006 /* BundleEditView+Capture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleEditView+Capture.swift"; sourceTree = ""; }; AA3908F72EC8BF0400D87EEC /* BundleDetailView+SaveImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleDetailView+SaveImage.swift"; sourceTree = ""; }; AA3909452EC9F29500D87EEC /* UIApplication+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Extension.swift"; sourceTree = ""; }; AA39098D2ECA061700D87EEC /* GridItemSpacing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridItemSpacing.swift; sourceTree = ""; }; @@ -2117,6 +2127,11 @@ isa = PBXGroup; children = ( AA6298532EC39065001576C0 /* BundleCreateView.swift */, + BC0001042F35F00100000004 /* BundleCreateView+Initialization.swift */, + BC0001052F35F00100000005 /* BundleCreateView+Capture.swift */, + BC0001022F35F00100000002 /* BundleCreateView+Alert.swift */, + BC0001012F35F00100000001 /* BundleCreateView+SelectSheet.swift */, + BC0001032F35F00100000003 /* BundleCreateView+Purchase.swift */, AA0219DD2EB1C041006EF269 /* BundleNameInputView.swift */, ); path = Create; @@ -2184,7 +2199,7 @@ children = ( AA6298512EC233D2001576C0 /* BundleEditView.swift */, AA2146BA2F161D0C0048D40E /* BundleEditView+Initialization.swift */, - AA2146B82F160E2B0048D40E /* BundleEditView+RestoreSelection.swift */, + BC0001062F35F00100000006 /* BundleEditView+Capture.swift */, AA2146B02F15D43C0048D40E /* BundleEditView+Alert.swift */, AA2146B62F15E5B60048D40E /* BundleEditView+SelectSheet.swift */, AA2146AE2F15D0160048D40E /* BundleEditView+Purchase.swift */, @@ -2799,7 +2814,12 @@ 4CEC61EC2EAE08C00099ECEE /* BodyType.swift in Sources */, C6C361E22ED4AC49009642F4 /* SubmitCompleteAlert.swift in Sources */, C6C361442ED44EF1009642F4 /* Showcase25BoardView+Detail.swift in Sources */, - AA2146B92F160E2B0048D40E /* BundleEditView+RestoreSelection.swift in Sources */, + BC0001072F35F00100000007 /* BundleCreateView+SelectSheet.swift in Sources */, + BC0001082F35F00100000008 /* BundleCreateView+Alert.swift in Sources */, + BC0001092F35F00100000009 /* BundleCreateView+Purchase.swift in Sources */, + BC00010A2F35F0010000000A /* BundleCreateView+Initialization.swift in Sources */, + BC00010B2F35F0010000000B /* BundleCreateView+Capture.swift in Sources */, + BC00010C2F35F0010000000C /* BundleEditView+Capture.swift in Sources */, 38C3C2922EC1F787003C5DE1 /* CollectionKeyringDetailView+Lifecycle.swift in Sources */, 4C8426642ED375840050B6FE /* ColorPalette.swift in Sources */, C6C402A32EB40ACA006B58DF /* AlarmView.swift in Sources */, diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+Alert.swift b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+Alert.swift new file mode 100644 index 000000000..eb1283d72 --- /dev/null +++ b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+Alert.swift @@ -0,0 +1,55 @@ +// +// BundleCreateView+Alert.swift +// Keychy +// +// Created by 김서현 on 11/12/25. +// + +import SwiftUI + +// MARK: - Alert 컨텐츠 +extension BundleCreateView { + var alertContent: some View { + ZStack { + // 구매 성공 Alert + if showPurchaseSuccessAlert { + Color.black20 + .ignoresSafeArea() + .onTapGesture { + showPurchaseSuccessAlert = false + purchasesSuccessScale = 0.3 + } + + KeychyAlert(type: .checkmark, message: "구매가 완료되었어요!", isPresented: $showPurchaseSuccessAlert) + .zIndex(101) + } + + // 구매 실패 Alert + if showPurchaseFailAlert { + ZStack { + Color.black20 + .ignoresSafeArea() + .onTapGesture { + showPurchaseFailAlert = false + purchaseFailScale = 0.3 + } + + PurchaseFailAlert( + checkmarkScale: purchaseFailScale, + onCancel: { + showPurchaseFailAlert = false + purchaseFailScale = 0.3 + }, + onCharge: { + showPurchaseFailAlert = false + purchaseFailScale = 0.3 + bundleVM.saveCurrentSelection() + router.push(.coinCharge) + } + ) + .padding(.horizontal, 51) + } + } + } + } +} diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+Capture.swift b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+Capture.swift new file mode 100644 index 000000000..076083089 --- /dev/null +++ b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+Capture.swift @@ -0,0 +1,128 @@ +// +// BundleCreateView+Capture.swift +// Keychy +// +// Created by 김서현 on 11/12/25. +// + +import SwiftUI + +// MARK: - 키링 데이터 및 캡처 +extension BundleCreateView { + /// 키링 데이터 리스트 생성 (씬 표시용) + 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 + } + + /// 씬 캡처 및 저장 + 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/Create/BundleCreateView+Initialization.swift b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+Initialization.swift new file mode 100644 index 000000000..125060a3a --- /dev/null +++ b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+Initialization.swift @@ -0,0 +1,127 @@ +// +// BundleCreateView+Initialization.swift +// Keychy +// +// Created by 김서현 on 11/12/25. +// + +import SwiftUI + +// MARK: - 데이터 초기화 +extension BundleCreateView { + + /// 초기 데이터 로딩 + func initializeData() async { + // 사용자가 소유한 배경과 카라비너 데이터를 가져옴 + await loadUserOwnedItems() + } + + /// 화면이 다시 나타날 때 데이터 새로고침 + func refreshData() async { + guard let _ = UserManager.shared.currentUser else { + return + } + + // 현재 선택된 아이템의 ID 저장 + let currentBackgroundId = bundleVM.newSelectedBackground?.background.id + let currentCarabinerId = bundleVM.newSelectedCarabiner?.carabiner.id + + // 배경 데이터 새로고침 + await withCheckedContinuation { continuation in + bundleVM.fetchAllBackgrounds { _ in + // 이전에 선택했던 배경을 다시 찾아서 선택 (구매 상태가 업데이트됨) + if let bgId = currentBackgroundId { + self.bundleVM.newSelectedBackground = bundleVM.backgroundViewData.first { $0.background.id == bgId } + } + continuation.resume() + } + } + + // 카라비너 데이터 새로고침 + await withCheckedContinuation { continuation in + bundleVM.fetchAllCarabiners { _ in + // 이전에 선택했던 카라비너를 다시 찾아서 선택 (구매 상태가 업데이트됨) + if let cbId = currentCarabinerId { + self.bundleVM.newSelectedCarabiner = bundleVM.carabinerViewData.first { $0.carabiner.id == cbId } + } + continuation.resume() + } + } + } + + /// 사용자가 소유한 배경과 카라비너 아이템들을 로드 + func loadUserOwnedItems() async { + guard let _ = UserManager.shared.currentUser else { + return + } + + let uid = UserManager.shared.userUID + + // 배경 데이터 로드 + await withCheckedContinuation { continuation in + bundleVM.fetchAllBackgrounds { _ in + continuation.resume() + } + } + + // 카라비너 데이터 로드 + await withCheckedContinuation { continuation in + bundleVM.fetchAllCarabiners { _ in + 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 + if success { + collectionVM.fetchUserKeyrings(uid: uid) { success in + if success { + bundleVM.keyring = collectionVM.keyring + } + continuation.resume() + } + } else { + continuation.resume() + } + } + } + } +} diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+Purchase.swift b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+Purchase.swift new file mode 100644 index 000000000..d4e73f47e --- /dev/null +++ b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+Purchase.swift @@ -0,0 +1,156 @@ +// +// BundleCreateView+Purchase.swift +// Keychy +// +// Created by 김서현 on 11/12/25. +// + +import SwiftUI + +// MARK: - 구매 시트 뷰 +extension BundleCreateView { + var purchaseSheetView: some View { + VStack(spacing: 12) { + // 상단 섹션 - 닫기 버튼, 타이틀 + HStack { + Button { + showPurchaseSheet = false + } label: { + Image(systemName: "xmark") + .font(.system(size: 20)) + .foregroundStyle(.gray600) + } + Spacer() + Text("구매하기") + .typography(.suit17B) + .foregroundStyle(.gray600) + Spacer() + } + .padding(EdgeInsets(top: 30, leading: 20, bottom: 10, trailing: 20)) + + // 구매할 아이템 목록 + VStack(spacing: 20) { + if let bg = bundleVM.newSelectedBackground, !bg.isOwned && bg.background.price > 0 { + 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( + imageURL: cb.carabiner.carabinerImage.first ?? "", + name: cb.carabiner.carabinerName, + type: "카라비너", + price: cb.carabiner.price + ) + } + } + .padding(.horizontal, 20) + .padding(.bottom, 30) + + // 내 보유 재화와 총 가격 + HStack(spacing: 6) { + Text("내 보유 : ") + .typography(.suit15M25) + .foregroundStyle(.black100) + .padding(.vertical, 4.5) + Text("\(UserManager.shared.currentUser?.coin ?? 0)") + .typography(.nanum16EB) + .foregroundStyle(.main500) + } + purchaseButton + .padding(.horizontal, 33.2) + .padding(.bottom, 40) + .adaptiveBottomPadding() + } + .background( + UnevenRoundedRectangle(topLeadingRadius: 38, topTrailingRadius: 38) + .fill(.white100) + ) + } + + // 구매 버튼 + var purchaseButton: some View { + Button { + Task { + await purchaseItems() + } + } label: { + HStack(spacing: 5) { + if bundleVM.isPurchasing { + LoadingAlert(type: .short40, message: nil) + } else { + Image(.myCoinMini) + } + + Text("\(bundleVM.totalCartPrice)") + .typography(.nanum18EB) + .padding(.top, 16) + .padding(.bottom, 12) + + Text("(\(bundleVM.payableItemsCount)개)") + .typography(.suit17SB) + } + .foregroundStyle(.white100) + .frame(maxWidth: .infinity) + .background(bundleVM.isPurchasing ? .gray400 : .black80) + .clipShape(RoundedRectangle(cornerRadius: 100)) + } + .disabled(bundleVM.isPurchasing) + } + + // MARK: - 구매 처리 + func purchaseItems() async { + let result = await bundleVM.purchaseSelectedItems() + + switch result { + case .success: + // 모든 구매 성공 + await refreshData() + + await MainActor.run { + // ViewModel 상태 동기화 + if let bg = bundleVM.newSelectedBackground { + bundleVM.selectedBackground = bg.background + } + if let cb = bundleVM.newSelectedCarabiner { + bundleVM.selectedCarabiner = cb.carabiner + } + + showPurchaseSheet = false + showPurchaseSuccessAlert = true + purchasesSuccessScale = 0.3 + withAnimation(.spring(response: 0.6, dampingFraction: 0.5)) { + purchasesSuccessScale = 1.0 + } + } + + // 2.5초 후 알럿 자동 닫기 (Alert duration 2초 + 0.5초 여유) + try? await Task.sleep(for: .seconds(2.5)) + + await MainActor.run { + showPurchaseSuccessAlert = false + purchasesSuccessScale = 0.3 + } + + case .insufficientCoins, .failed: + // 구매 실패 + await MainActor.run { + showPurchaseSheet = false + } + + // 시트 닫히는 애니메이션 대기 + try? await Task.sleep(for: .seconds(0.3)) + + await MainActor.run { + showPurchaseFailAlert = true + purchaseFailScale = 0.3 + withAnimation(.spring(response: 0.6, dampingFraction: 0.5)) { + purchaseFailScale = 1.0 + } + } + } + } +} diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+SelectSheet.swift b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+SelectSheet.swift new file mode 100644 index 000000000..8892577d0 --- /dev/null +++ b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+SelectSheet.swift @@ -0,0 +1,191 @@ +// +// BundleCreateView+SelectSheet.swift +// Keychy +// +// Created by 김서현 on 11/12/25. +// + +import SwiftUI + +// MARK: - 키링 버튼 +extension BundleCreateView { + 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 { + 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) + } +} diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift index 15579717c..06c76e2fc 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift @@ -4,6 +4,12 @@ // // Created by 김서현 on 11/12/25. // +// Extension 파일: +// +SelectSheet - 시트 컨텐츠 (배경/카라비너/키링) +// +Alert - 알럿 뷰 +// +Purchase - 구매 시트, 구매 로직 +// +Initialization - 데이터 로딩 +// +Capture - 씬 캡처 import SwiftUI import NukeUI @@ -11,29 +17,29 @@ import SceneKit import FirebaseFirestore struct BundleCreateView: View { - - //MARK: - 프로퍼티들 + + // MARK: - 프로퍼티들 @Bindable var router: NavigationRouter @State var collectionVM: CollectionViewModel @Bindable var bundleVM: BundleViewModel // 시트 활성화 상태 - @State private var showItemSheet: Bool = false - @State private var isBackgroundMode: Bool = true // true: 배경, false: 카라비너 - @State private var showKeyringSheet: Bool = false + @State var showItemSheet: Bool = false + @State var isBackgroundMode: Bool = true // true: 배경, false: 카라비너 + @State var showKeyringSheet: Bool = false // 시트 높이 - @State private var sheetHeight: CGFloat = 360 + @State var sheetHeight: CGFloat = 360 // 키링 선택 상태 - @State private var selectedKeyrings: [Int: Keyring] = [:] - @State private var keyringOrder: [Int] = [] - @State private var selectedPosition: Int = 0 + @State var selectedKeyrings: [Int: Keyring] = [:] + @State var keyringOrder: [Int] = [] + @State var selectedPosition: Int = 0 // 캡처 상태 - @State private var isCapturing: Bool = false - @State private var sceneRefreshId = UUID() - @State private var isSceneReady: Bool = false + @State var isCapturing: Bool = false + @State var sceneRefreshId = UUID() + @State var isSceneReady: Bool = false // 구매 시트 @State var showPurchaseSheet = false @@ -45,15 +51,15 @@ struct BundleCreateView: View { @State var purchaseFailScale: CGFloat = 0.3 // 공통 그리드 컬럼 (배경, 카라비너, 키링 모두 동일) - private let gridColumns: [GridItem] = [ + let gridColumns: [GridItem] = [ GridItem(.flexible(), spacing: 10), GridItem(.flexible(), spacing: 10), GridItem(.flexible(), spacing: 10) ] - private let sheetHeightRatio: CGFloat = 0.5 - - //MARK: 메인 뷰 + let sheetHeightRatio: CGFloat = 0.5 + + // MARK: - 메인 뷰 var body: some View { ZStack(alignment: .bottom) { if let bg = bundleVM.newSelectedBackground, @@ -163,7 +169,7 @@ struct BundleCreateView: View { // MARK: - 커스텀 네비게이션 바 extension BundleCreateView { - private var customNavigationBar: some View { + var customNavigationBar: some View { CustomNavigationBar { BackToolbarButton { router.pop() @@ -184,628 +190,3 @@ extension BundleCreateView { } } } - - -// 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 { - 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) - } -} - - - -// MARK: - 데이터 가져오는 메서드 -extension BundleCreateView { - - /// 초기 데이터 로딩 - private func initializeData() async { - // 사용자가 소유한 배경과 카라비너 데이터를 가져옴 - await loadUserOwnedItems() - } - - /// 화면이 다시 나타날 때 데이터 새로고침 - private func refreshData() async { - guard let _ = UserManager.shared.currentUser else { - return - } - - // 현재 선택된 아이템의 ID 저장 - let currentBackgroundId = bundleVM.newSelectedBackground?.background.id - let currentCarabinerId = bundleVM.newSelectedCarabiner?.carabiner.id - - // 배경 데이터 새로고침 - await withCheckedContinuation { continuation in - bundleVM.fetchAllBackgrounds { _ in - // 이전에 선택했던 배경을 다시 찾아서 선택 (구매 상태가 업데이트됨) - if let bgId = currentBackgroundId { - self.bundleVM.newSelectedBackground = bundleVM.backgroundViewData.first { $0.background.id == bgId } - } - continuation.resume() - } - } - - // 카라비너 데이터 새로고침 - await withCheckedContinuation { continuation in - bundleVM.fetchAllCarabiners { _ in - // 이전에 선택했던 카라비너를 다시 찾아서 선택 (구매 상태가 업데이트됨) - if let cbId = currentCarabinerId { - self.bundleVM.newSelectedCarabiner = bundleVM.carabinerViewData.first { $0.carabiner.id == cbId } - } - continuation.resume() - } - } - } - - /// 사용자가 소유한 배경과 카라비너 아이템들을 로드 - private func loadUserOwnedItems() async { - guard let _ = UserManager.shared.currentUser else { - return - } - - let uid = UserManager.shared.userUID - - // 배경 데이터 로드 - await withCheckedContinuation { continuation in - bundleVM.fetchAllBackgrounds { _ in - continuation.resume() - } - } - - // 카라비너 데이터 로드 - await withCheckedContinuation { continuation in - bundleVM.fetchAllCarabiners { _ in - 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 - if success { - collectionVM.fetchUserKeyrings(uid: uid) { success in - if success { - bundleVM.keyring = collectionVM.keyring - } - continuation.resume() - } - } else { - continuation.resume() - } - } - } - } -} - -// MARK: - Alert 컨텐츠 -extension BundleCreateView { - private var alertContent: some View { - ZStack { - // 구매 성공 Alert - if showPurchaseSuccessAlert { - Color.black20 - .ignoresSafeArea() - .onTapGesture { - showPurchaseSuccessAlert = false - purchasesSuccessScale = 0.3 - } - - KeychyAlert(type: .checkmark, message: "구매가 완료되었어요!", isPresented: $showPurchaseSuccessAlert) - .zIndex(101) - } - - // 구매 실패 Alert - if showPurchaseFailAlert { - ZStack { - Color.black20 - .ignoresSafeArea() - .onTapGesture { - showPurchaseFailAlert = false - purchaseFailScale = 0.3 - } - - PurchaseFailAlert( - checkmarkScale: purchaseFailScale, - onCancel: { - showPurchaseFailAlert = false - purchaseFailScale = 0.3 - }, - onCharge: { - showPurchaseFailAlert = false - purchaseFailScale = 0.3 - bundleVM.saveCurrentSelection() - router.push(.coinCharge) - } - ) - .padding(.horizontal, 51) - } - } - } - } -} - -// MARK: - 구매 시트 뷰 -extension BundleCreateView { - private var purchaseSheetView: some View { - VStack(spacing: 12) { - // 상단 섹션 - 닫기 버튼, 타이틀 - HStack { - Button { - showPurchaseSheet = false - } label: { - Image(systemName: "xmark") - .font(.system(size: 20)) - .foregroundStyle(.gray600) - } - Spacer() - Text("구매하기") - .typography(.suit17B) - .foregroundStyle(.gray600) - Spacer() - } - .padding(EdgeInsets(top: 30, leading: 20, bottom: 10, trailing: 20)) - - // 구매할 아이템 목록 - VStack(spacing: 20) { - if let bg = bundleVM.newSelectedBackground, !bg.isOwned && bg.background.price > 0 { - 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( - imageURL: cb.carabiner.carabinerImage.first ?? "", - name: cb.carabiner.carabinerName, - type: "카라비너", - price: cb.carabiner.price - ) - } - } - .padding(.horizontal, 20) - .padding(.bottom, 30) - - // 내 보유 재화와 총 가격 - HStack(spacing: 6) { - Text("내 보유 : ") - .typography(.suit15M25) - .foregroundStyle(.black100) - .padding(.vertical, 4.5) - Text("\(UserManager.shared.currentUser?.coin ?? 0)") - .typography(.nanum16EB) - .foregroundStyle(.main500) - } - purchaseButton - .padding(.horizontal, 33.2) - .padding(.bottom, 40) - .adaptiveBottomPadding() - } - .background( - UnevenRoundedRectangle(topLeadingRadius: 38, topTrailingRadius: 38) - .fill(.white100) - ) - } - - // 구매 버튼 - private var purchaseButton: some View { - Button { - Task { - await purchaseItems() - } - } label: { - HStack(spacing: 5) { - if bundleVM.isPurchasing { - LoadingAlert(type: .short40, message: nil) - } else { - Image(.myCoinMini) - } - - Text("\(bundleVM.totalCartPrice)") - .typography(.nanum18EB) - .padding(.top, 16) - .padding(.bottom, 12) - - Text("(\(bundleVM.payableItemsCount)개)") - .typography(.suit17SB) - } - .foregroundStyle(.white100) - .frame(maxWidth: .infinity) - .background(bundleVM.isPurchasing ? .gray400 : .black80) - .clipShape(RoundedRectangle(cornerRadius: 100)) - } - .disabled(bundleVM.isPurchasing) - } - - // MARK: - 구매 처리 - private func purchaseItems() async { - let result = await bundleVM.purchaseSelectedItems() - - switch result { - case .success: - // 모든 구매 성공 - await refreshData() - - await MainActor.run { - // ViewModel 상태 동기화 - if let bg = bundleVM.newSelectedBackground { - bundleVM.selectedBackground = bg.background - } - if let cb = bundleVM.newSelectedCarabiner { - bundleVM.selectedCarabiner = cb.carabiner - } - - showPurchaseSheet = false - showPurchaseSuccessAlert = true - purchasesSuccessScale = 0.3 - withAnimation(.spring(response: 0.6, dampingFraction: 0.5)) { - purchasesSuccessScale = 1.0 - } - } - - // 2.5초 후 알럿 자동 닫기 (Alert duration 2초 + 0.5초 여유) - try? await Task.sleep(for: .seconds(2.5)) - - await MainActor.run { - showPurchaseSuccessAlert = false - purchasesSuccessScale = 0.3 - } - - case .insufficientCoins, .failed: - // 구매 실패 - await MainActor.run { - showPurchaseSheet = false - } - - // 시트 닫히는 애니메이션 대기 - try? await Task.sleep(for: .seconds(0.3)) - - await MainActor.run { - showPurchaseFailAlert = true - purchaseFailScale = 0.3 - withAnimation(.spring(response: 0.6, dampingFraction: 0.5)) { - purchaseFailScale = 1.0 - } - } - } - } -} - -// 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+Alert.swift b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+Alert.swift index e177c5d22..c40e45293 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+Alert.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+Alert.swift @@ -96,7 +96,7 @@ extension BundleEditView { onCharge: { showPurchaseFailAlert = false purchaseFailScale = 0.3 - saveCurrentSelection() + bundleVM.saveCurrentSelection() router.push(.coinCharge) } ) diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+Capture.swift b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+Capture.swift new file mode 100644 index 000000000..ed88e1652 --- /dev/null +++ b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+Capture.swift @@ -0,0 +1,116 @@ +// +// BundleEditView+Capture.swift +// Keychy +// +// Created by 김서현 on 11/10/25. +// + +import SwiftUI + +// MARK: - 키링 데이터 및 캡처 +extension BundleEditView { + + /// 키링 데이터 리스트 업데이트 + func updateKeyringDataList() { + guard let carabiner = bundleVM.newSelectedCarabiner?.carabiner else { + keyringDataList = [] + return + } + + let newData = bundleVM.createKeyringDataListFromSelected( + selectedKeyrings: bundleVM.selectedKeyrings, + keyringOrder: bundleVM.keyringOrder, + carabiner: carabiner + ) + + // 데이터가 실제로 변경된 경우에만 업데이트 + if keyringDataList != newData { + keyringDataList = newData + + // 키링이 추가/변경될 때도 씬을 새로고침하여 확실히 반영되도록 함 + if !newData.isEmpty { + sceneRefreshId = UUID() + } + } + } + + // MARK: - 썸네일 재캡쳐 & 캐시 저장 + func recaptureAndCacheBundleThumbnail(bundleId: String, bundleName: String, createdAt: Date) async { + // 편집 중 상태로 캡쳐 + guard let bg = bundleVM.newSelectedBackground?.background, + let cb = bundleVM.newSelectedCarabiner?.carabiner else { + return + } + + await MainActor.run { + isCapturing = true + } + + // 캡쳐용 키링 데이터 생성 (편집 중 keyringDataList -> 캡쳐용으로 변환) + let captureKeyrings: [MultiKeyringCaptureScene.KeyringData] = keyringDataList.map { item in + MultiKeyringCaptureScene.KeyringData( + index: item.index, + position: item.position, + bodyImageURL: item.bodyImageURL, + hookOffsetY: item.hookOffsetY, + chainLength: item.chainLength + ) + } + + // 카라비너 타입 및 이미지 URL + let carabinerType = cb.type + let carabinerBackURL: String? + let carabinerFrontURL: String? + if carabinerType == .hamburger { + carabinerBackURL = cb.carabinerImage[1] + carabinerFrontURL = cb.carabinerImage[2] + } else { + carabinerBackURL = cb.carabinerImage[0] + carabinerFrontURL = nil + } + + // 1. 배경 포함 캡쳐 (앱용) + guard let fullImageData = await MultiKeyringCaptureScene.captureBundleImage( + keyringDataList: captureKeyrings, + backgroundImageURL: bg.backgroundImage, + carabinerBackImageURL: carabinerBackURL, + carabinerFrontImageURL: carabinerFrontURL, + carabinerType: carabinerType, + carabinerX: cb.carabinerX, + carabinerY: cb.carabinerY, + carabinerWidth: cb.carabinerWidth + ) else { + await MainActor.run { + isCapturing = false + } + return + } + + // 2. 배경 없이 캡쳐 (위젯용 - 투명 여백 제거 후 리사이즈) + let widgetImageData = await MultiKeyringCaptureScene.captureBundleImage( + keyringDataList: captureKeyrings, + backgroundImageURL: nil, + carabinerBackImageURL: carabinerBackURL, + carabinerFrontImageURL: carabinerFrontURL, + carabinerType: carabinerType, + carabinerX: cb.carabinerX, + carabinerY: cb.carabinerY, + carabinerWidth: cb.carabinerWidth, + trimTransparentEdges: true + ) + + // 캐시 저장 (full + widget) + BundleImageCache.shared.syncBundle( + id: bundleId, + name: bundleName, + fullImageData: fullImageData, + widgetImageData: widgetImageData, + createdAt: createdAt + ) + + await MainActor.run { + bundleVM.bundleCapturedImage = fullImageData + isCapturing = false + } + } +} diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+Initialization.swift b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+Initialization.swift index 23e5c4c1f..d724f7028 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+Initialization.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+Initialization.swift @@ -43,7 +43,6 @@ extension BundleEditView { bgData.background.id == selectedBundle.selectedBackground } } - self.restoreBackgroundSelection() bundleVM.fetchAllCarabiners { _ in // 현재 뭉치의 카라비너로 항상 초기화 @@ -52,7 +51,9 @@ extension BundleEditView { cbData.carabiner.id == selectedBundle.selectedCarabiner } } - self.restoreCarabinerSelection() + + // 코인 충전 후 복귀 시 저장된 선택 복원 + bundleVM.restoreSelectionIfNeeded() Task { // Firebase 데이터를 한 번만 로컬 상태로 초기화 diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+RestoreSelection.swift b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+RestoreSelection.swift deleted file mode 100644 index 5b9b2fc30..000000000 --- a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+RestoreSelection.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// BundleEditView+RestoreSelection.swift -// Keychy -// -// Created by 김서현 on 1/13/26. -// -// 뭉치 편집뷰에서 선택했던 항목을 복구하는 메서드를 모아둔 파일입니다. - -import SwiftUI - -extension BundleEditView { - // MARK: - 선택 상태 저장/복원 - func saveCurrentSelection() { - if let bg = bundleVM.newSelectedBackground { - UserDefaults.standard.set(bg.background.id, forKey: "tempSelectedBackgroundId") - } - if let cb = bundleVM.newSelectedCarabiner { - UserDefaults.standard.set(cb.carabiner.id, forKey: "tempSelectedCarabinerId") - } - } - - func restoreSelection() { - restoreBackgroundSelection() - restoreCarabinerSelection() - } - - func restoreBackgroundSelection() { - if let savedBackgroundId = UserDefaults.standard.string(forKey: "tempSelectedBackgroundId") { - if let restoredBackground = bundleVM.backgroundViewData.first(where: { $0.background.id == savedBackgroundId }) { - bundleVM.newSelectedBackground = restoredBackground - // 복원 후 삭제 - UserDefaults.standard.removeObject(forKey: "tempSelectedBackgroundId") - } - } - } - - func restoreCarabinerSelection() { - if let savedCarbinerId = UserDefaults.standard.string(forKey: "tempSelectedCarabinerId") { - if let restoredCarabiner = bundleVM.carabinerViewData.first(where: { $0.carabiner.id == savedCarbinerId }) { - bundleVM.newSelectedCarabiner = restoredCarabiner - // 복원 후 삭제 - UserDefaults.standard.removeObject(forKey: "tempSelectedCarabinerId") - } - } - } -} diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+SelectSheet.swift b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+SelectSheet.swift index 552032639..921f96501 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+SelectSheet.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+SelectSheet.swift @@ -96,19 +96,15 @@ extension BundleEditView { .frame(maxWidth: .infinity) .frame(height: screenHeight * (sheetHeightRatio - 0.08)) // 버튼 영역 제외한 대략 높이 } else if collectionVM.keyring.isEmpty { - VStack { - Image(.emptyViewIcon) - .resizable() - .scaledToFit() - .frame(height: 77) - Text("공방에서 키링을 만들 수 있어요") + VStack(spacing: 16) { + Image(.surprisedAlert) + + Text("공방에서 키링을 만들어보세요.\n아직 만들어진 키링이 없어요.") .typography(.suit15R) .foregroundStyle(.black100) - .padding(.vertical, 15) } - .padding(.bottom, 77) - .padding(.top, 62) .frame(maxWidth: .infinity) + .padding(.vertical, 60) } else { ScrollView { diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView.swift b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView.swift index a778ba165..62e517bec 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView.swift @@ -246,110 +246,6 @@ struct BundleEditView: View { } .ignoresSafeArea() } - - /// 키링 데이터 리스트 업데이트 - func updateKeyringDataList() { - guard let carabiner = bundleVM.newSelectedCarabiner?.carabiner else { - keyringDataList = [] - return - } - - let newData = bundleVM.createKeyringDataListFromSelected( - selectedKeyrings: bundleVM.selectedKeyrings, - keyringOrder: bundleVM.keyringOrder, - carabiner: carabiner - ) - - // 데이터가 실제로 변경된 경우에만 업데이트 - if keyringDataList != newData { - keyringDataList = newData - - // 키링이 추가/변경될 때도 씬을 새로고침하여 확실히 반영되도록 함 - if !newData.isEmpty { - sceneRefreshId = UUID() - } - } - } - - // MARK: - 썸네일 재캡쳐 & 캐시 저장 - private func recaptureAndCacheBundleThumbnail(bundleId: String, bundleName: String, createdAt: Date) async { - // 편집 중 상태로 캡쳐 - guard let bg = bundleVM.newSelectedBackground?.background, - let cb = bundleVM.newSelectedCarabiner?.carabiner else { - return - } - - await MainActor.run { - isCapturing = true - } - - // 캡쳐용 키링 데이터 생성 (편집 중 keyringDataList -> 캡쳐용으로 변환) - let captureKeyrings: [MultiKeyringCaptureScene.KeyringData] = keyringDataList.map { item in - MultiKeyringCaptureScene.KeyringData( - index: item.index, - position: item.position, - bodyImageURL: item.bodyImageURL, - hookOffsetY: item.hookOffsetY, - chainLength: item.chainLength - ) - } - - // 카라비너 타입 및 이미지 URL - let carabinerType = cb.type - let carabinerBackURL: String? - let carabinerFrontURL: String? - if carabinerType == .hamburger { - carabinerBackURL = cb.carabinerImage[1] - carabinerFrontURL = cb.carabinerImage[2] - } else { - carabinerBackURL = cb.carabinerImage[0] - carabinerFrontURL = nil - } - - // 1. 배경 포함 캡쳐 (앱용) - guard let fullImageData = await MultiKeyringCaptureScene.captureBundleImage( - keyringDataList: captureKeyrings, - backgroundImageURL: bg.backgroundImage, - carabinerBackImageURL: carabinerBackURL, - carabinerFrontImageURL: carabinerFrontURL, - carabinerType: carabinerType, - carabinerX: cb.carabinerX, - carabinerY: cb.carabinerY, - carabinerWidth: cb.carabinerWidth - ) else { - await MainActor.run { - isCapturing = false - } - return - } - - // 2. 배경 없이 캡쳐 (위젯용 - 투명 여백 제거 후 리사이즈) - let widgetImageData = await MultiKeyringCaptureScene.captureBundleImage( - keyringDataList: captureKeyrings, - backgroundImageURL: nil, - carabinerBackImageURL: carabinerBackURL, - carabinerFrontImageURL: carabinerFrontURL, - carabinerType: carabinerType, - carabinerX: cb.carabinerX, - carabinerY: cb.carabinerY, - carabinerWidth: cb.carabinerWidth, - trimTransparentEdges: true - ) - - // 캐시 저장 (full + widget) - BundleImageCache.shared.syncBundle( - id: bundleId, - name: bundleName, - fullImageData: fullImageData, - widgetImageData: widgetImageData, - createdAt: createdAt - ) - - await MainActor.run { - bundleVM.bundleCapturedImage = fullImageData - isCapturing = false - } - } } // MARK: - 툴바 diff --git a/Keychy/Keychy/Resources/Assets.xcassets/21. Alert/surprisedAlert.imageset/Contents.json b/Keychy/Keychy/Resources/Assets.xcassets/21. Alert/surprisedAlert.imageset/Contents.json new file mode 100644 index 000000000..145a6f158 --- /dev/null +++ b/Keychy/Keychy/Resources/Assets.xcassets/21. Alert/surprisedAlert.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "suprisedAlert.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Keychy/Keychy/Resources/Assets.xcassets/21. Alert/surprisedAlert.imageset/suprisedAlert.pdf b/Keychy/Keychy/Resources/Assets.xcassets/21. Alert/surprisedAlert.imageset/suprisedAlert.pdf new file mode 100644 index 0000000000000000000000000000000000000000..e8e41a50b89ee90fbd8571eda21baab9f6f47c77 GIT binary patch literal 5601 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 auM4)$&KQIf=Au$ih>#EoGqdbHx&H$cdisz6 literal 0 HcmV?d00001 From 9b8e164b4b588b87d25e083ae8d830e91957af98 Mon Sep 17 00:00:00 2001 From: giljihun Date: Fri, 6 Feb 2026 19:26:53 +0900 Subject: [PATCH 02/10] =?UTF-8?q?feat:=20=EB=AD=89=EC=B9=98=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20-=20=ED=82=A4=EB=A7=81=EC=9D=B4=200=EC=9D=BC?= =?UTF-8?q?=EB=95=8C=20notice?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Create/BundleCreateView+SelectSheet.swift | 51 ++++++++----------- .../Edit/BundleEditView+SelectSheet.swift | 1 + 2 files changed, 23 insertions(+), 29 deletions(-) diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+SelectSheet.swift b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+SelectSheet.swift index 8892577d0..0a11ea4a8 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+SelectSheet.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+SelectSheet.swift @@ -14,17 +14,17 @@ extension BundleCreateView { 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 { let isSelectedHere = selectedKeyrings[selectedPosition]?.id == keyring.id let isSelectedElsewhere = selectedKeyrings.values.contains { $0.id == keyring.id } && !isSelectedHere - + return Button { if isSelectedHere { selectedKeyrings[selectedPosition] = nil @@ -155,25 +148,25 @@ extension BundleCreateView { 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) diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+SelectSheet.swift b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+SelectSheet.swift index 921f96501..3d26ce12c 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+SelectSheet.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+SelectSheet.swift @@ -102,6 +102,7 @@ extension BundleEditView { Text("공방에서 키링을 만들어보세요.\n아직 만들어진 키링이 없어요.") .typography(.suit15R) .foregroundStyle(.black100) + .multilineTextAlignment(.center) } .frame(maxWidth: .infinity) .padding(.vertical, 60) From eeb9e4c0fd933d8cbc71bf703c086d73fc683af2 Mon Sep 17 00:00:00 2001 From: giljihun Date: Fri, 6 Feb 2026 21:05:30 +0900 Subject: [PATCH 03/10] =?UTF-8?q?refactor:=20=ED=82=A4=EB=A7=81=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=20=EC=8B=9C=ED=8A=B8=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94=EC=B6=9C=20?= =?UTF-8?q?=EB=B0=8F=20Create/Edit=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BundleSearchBar, KeyringEmptyStateView, KeyringSelectionContent를 Shared로 추출 - Edit의 키링 시트를 Create와 동일한 SwiftUI sheet 방식으로 변경 --- Keychy/Keychy.xcodeproj/project.pbxproj | 12 ++ .../Create/BundleCreateView+SelectSheet.swift | 106 +++++--------- .../Views/Create/BundleCreateView.swift | 1 + .../Edit/BundleEditView+SelectSheet.swift | 130 ++++++------------ .../Bundle/Views/Edit/BundleEditView.swift | 21 +-- .../Bundle/Views/Shared/BundleSearchBar.swift | 38 +++++ .../Views/Shared/KeyringEmptyStateView.swift | 23 ++++ .../Shared/KeyringSelectionContent.swift | 90 ++++++++++++ 8 files changed, 245 insertions(+), 176 deletions(-) create mode 100644 Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleSearchBar.swift create mode 100644 Keychy/Keychy/Presentation/Bundle/Views/Shared/KeyringEmptyStateView.swift create mode 100644 Keychy/Keychy/Presentation/Bundle/Views/Shared/KeyringSelectionContent.swift diff --git a/Keychy/Keychy.xcodeproj/project.pbxproj b/Keychy/Keychy.xcodeproj/project.pbxproj index c2847e4f5..20ce30c51 100644 --- a/Keychy/Keychy.xcodeproj/project.pbxproj +++ b/Keychy/Keychy.xcodeproj/project.pbxproj @@ -109,6 +109,9 @@ 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 */; }; + BC00020F2F35F00200000003 /* BundleSearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC00020D2F35F00200000001 /* BundleSearchBar.swift */; }; + BC0002102F35F00200000004 /* KeyringEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC00020E2F35F00200000002 /* KeyringEmptyStateView.swift */; }; + BC0002132F35F00200000006 /* KeyringSelectionContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0002112F35F00200000005 /* KeyringSelectionContent.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 */; }; @@ -565,6 +568,9 @@ 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 = ""; }; + BC00020D2F35F00200000001 /* BundleSearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleSearchBar.swift; sourceTree = ""; }; + BC00020E2F35F00200000002 /* KeyringEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyringEmptyStateView.swift; sourceTree = ""; }; + BC0002112F35F00200000005 /* KeyringSelectionContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyringSelectionContent.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 = ""; }; @@ -1810,6 +1816,9 @@ F82FD6112F9442AAAD58DB97 /* BundleSheetToggleButtons.swift */, 5E036016318043AC801A19DA /* BundlePurchaseCartItem.swift */, AA6298552EC3AD16001576C0 /* DraggableSheet.swift */, + BC00020D2F35F00200000001 /* BundleSearchBar.swift */, + BC00020E2F35F00200000002 /* KeyringEmptyStateView.swift */, + BC0002112F35F00200000005 /* KeyringSelectionContent.swift */, ); path = Shared; sourceTree = ""; @@ -2755,6 +2764,9 @@ AA0A54B92EC05C41007B5413 /* BundleRingComponent.swift in Sources */, C6C361422ED44EE4009642F4 /* Showcase25BoardView+Sheet.swift in Sources */, 4C2525DA2F35B2A7003CC5AD /* BundleSheetFilterBar.swift in Sources */, + BC00020F2F35F00200000003 /* BundleSearchBar.swift in Sources */, + BC0002102F35F00200000004 /* KeyringEmptyStateView.swift in Sources */, + BC0002132F35F00200000006 /* KeyringSelectionContent.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/Views/Create/BundleCreateView+SelectSheet.swift b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+SelectSheet.swift index 0a11ea4a8..eea53a3cc 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+SelectSheet.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+SelectSheet.swift @@ -100,85 +100,45 @@ extension BundleCreateView { } var keyringSheetContent: some View { - VStack(spacing: 15) { - if bundleVM.keyring.isEmpty { - Image(.surprisedAlert) - - Text("공방에서 키링을 만들어보세요.\n아직 만들어진 키링이 없어요.") - .typography(.suit15R) - .foregroundStyle(.black100) - .multilineTextAlignment(.center) - - } 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) - } - - 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 { + KeyringSelectionContent( + searchText: $keyringSearchText, + keyrings: sortedKeyringsForSelection, + isLoading: false, + gridColumns: gridColumns, + cellWidth: threeGridCellWidth, + cellHeight: threeGridCellHeight, + isSelectedHere: { keyring in + selectedKeyrings[selectedPosition]?.id == keyring.id + }, + isSelectedElsewhere: { keyring in + selectedKeyrings.values.contains { $0.id == keyring.id } && + !(selectedKeyrings[selectedPosition]?.id == keyring.id) + }, + onTapSelect: { keyring in if selectedKeyrings[selectedPosition] != nil { keyringOrder.removeAll { $0 == selectedPosition } } selectedKeyrings[selectedPosition] = keyring keyringOrder.append(selectedPosition) showKeyringSheet = false + sceneRefreshId = UUID() + }, + onTapDeselect: { keyring in + selectedKeyrings[selectedPosition] = nil + keyringOrder.removeAll { $0 == selectedPosition } + sceneRefreshId = UUID() } - 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) + ) + .padding(.horizontal, 20) + .presentationDetents([.fraction(0.45), .fraction(0.85)]) + .presentationDragIndicator(.visible) + } + + // MARK: - 정렬된 키링 목록 (필터링은 KeyringSelectionContent에서 처리) + var sortedKeyringsForSelection: [Keyring] { + bundleVM.sortedKeyringsForSelection( + selectedKeyrings: selectedKeyrings, + selectedPosition: selectedPosition + ) } } diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift index 06c76e2fc..d1aae8783 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift @@ -35,6 +35,7 @@ struct BundleCreateView: View { @State var selectedKeyrings: [Int: Keyring] = [:] @State var keyringOrder: [Int] = [] @State var selectedPosition: Int = 0 + @State var keyringSearchText: String = "" // 캡처 상태 @State var isCapturing: Bool = false diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+SelectSheet.swift b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+SelectSheet.swift index 3d26ce12c..68b04570a 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+SelectSheet.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+SelectSheet.swift @@ -66,96 +66,48 @@ extension BundleEditView { } } - /// 키링 선택 시트 오버레이 - var keyringSheetOverlay: some View { - Group { - if showSelectKeyringSheet { - Color.black20 - .ignoresSafeArea() - .zIndex(1) - .onTapGesture { - withAnimation(.easeInOut) { - showSelectKeyringSheet = false - } - } - - VStack(spacing: 18) { - Text("키링 선택") - .typography(.suit16B) - .foregroundStyle(.black100) - - if isKeyringSheetLoading { - VStack { - LoadingAlert(type: .short40, message: nil) - .padding(.vertical, 24) - Text("키링을 불러오고 있어요") - .typography(.suit15R) - .foregroundStyle(.black100) - .padding(.vertical, 15) - } - .frame(maxWidth: .infinity) - .frame(height: screenHeight * (sheetHeightRatio - 0.08)) // 버튼 영역 제외한 대략 높이 - } else if collectionVM.keyring.isEmpty { - VStack(spacing: 16) { - Image(.surprisedAlert) - - Text("공방에서 키링을 만들어보세요.\n아직 만들어진 키링이 없어요.") - .typography(.suit15R) - .foregroundStyle(.black100) - .multilineTextAlignment(.center) - } - .frame(maxWidth: .infinity) - .padding(.vertical, 60) - - } else { - ScrollView { - LazyVGrid(columns: gridColumns, spacing: 10) { - ForEach(bundleVM.sortedKeyringsForSelection(selectedKeyrings: bundleVM.selectedKeyrings, selectedPosition: selectedPosition), id: \.self) { keyring in - KeyringCell( - keyring: keyring, - isSelectedHere: bundleVM.selectedKeyrings[selectedPosition]?.id == keyring.id, - isSelectedElsewhere: bundleVM.selectedKeyrings.values.contains { $0.id == keyring.id } && !(bundleVM.selectedKeyrings[selectedPosition]?.id == keyring.id), - width: threeGridCellWidth, - height: threeGridCellHeight, - onTapSelect: { - // 기존 있으면 순서 제거 후 교체 - if bundleVM.selectedKeyrings[selectedPosition] != nil { - bundleVM.keyringOrder.removeAll { $0 == selectedPosition } - } - bundleVM.selectedKeyrings[selectedPosition] = keyring - bundleVM.keyringOrder.append(selectedPosition) - withAnimation(.easeInOut) { - showSelectKeyringSheet = false - } - updateKeyringDataList() - }, - onTapDeselect: { - bundleVM.selectedKeyrings[selectedPosition] = nil - bundleVM.keyringOrder.removeAll { $0 == selectedPosition } - withAnimation(.easeInOut) { - showSelectKeyringSheet = false - } - updateKeyringDataList() - } - ) - } - } - } - } - + /// 키링 선택 시트 (Create와 동일한 SwiftUI sheet 방식) + var keyringSheetContent: some View { + KeyringSelectionContent( + searchText: $keyringSearchText, + keyrings: sortedKeyringsForSelection, + isLoading: isKeyringSheetLoading, + gridColumns: gridColumns, + cellWidth: threeGridCellWidth, + cellHeight: threeGridCellHeight, + isSelectedHere: { keyring in + bundleVM.selectedKeyrings[selectedPosition]?.id == keyring.id + }, + isSelectedElsewhere: { keyring in + bundleVM.selectedKeyrings.values.contains { $0.id == keyring.id } && + !(bundleVM.selectedKeyrings[selectedPosition]?.id == keyring.id) + }, + onTapSelect: { keyring in + if bundleVM.selectedKeyrings[selectedPosition] != nil { + bundleVM.keyringOrder.removeAll { $0 == selectedPosition } } - .padding(EdgeInsets(top: 30, leading: 20, bottom: 0, trailing: 20)) - .frame(maxWidth: .infinity) - .frame(height: screenHeight * sheetHeightRatio) - .glassEffect(.regular, in: .rect) - .clipShape(UnevenRoundedRectangle(topLeadingRadius: 30, topTrailingRadius: 30)) - .shadow(radius: 10) - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom) - .transition(.move(edge: .bottom)) - .zIndex(2) - + bundleVM.selectedKeyrings[selectedPosition] = keyring + bundleVM.keyringOrder.append(selectedPosition) + showSelectKeyringSheet = false + updateKeyringDataList() + }, + onTapDeselect: { keyring in + bundleVM.selectedKeyrings[selectedPosition] = nil + bundleVM.keyringOrder.removeAll { $0 == selectedPosition } + showSelectKeyringSheet = false + updateKeyringDataList() } - } + ) + .padding(.horizontal, 20) + .presentationDetents([.fraction(0.45), .fraction(0.85)]) + .presentationDragIndicator(.visible) + } + + // MARK: - 정렬된 키링 목록 (필터링은 KeyringSelectionContent에서 처리) + var sortedKeyringsForSelection: [Keyring] { + bundleVM.sortedKeyringsForSelection( + selectedKeyrings: bundleVM.selectedKeyrings, + selectedPosition: selectedPosition + ) } - } diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView.swift b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView.swift index 62e517bec..6e0075c57 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView.swift @@ -42,7 +42,8 @@ struct BundleEditView: View { @State var selectedPosition = 0 @State var sceneRefreshId = UUID() - + @State var keyringSearchText: String = "" + // 공통 그리드 컬럼 (배경, 카라비너, 키링 모두 동일) let gridColumns: [GridItem] = [ GridItem(.flexible(), spacing: 10), @@ -57,17 +58,14 @@ struct BundleEditView: View { let sheetHeightRatio: CGFloat = 0.43 var shouldApplyBlur: Bool { - showPurchaseFailAlert || showPurchaseSuccessAlert || isCapturing || !isSceneReady || bundleVM.isPurchasing || isKeyringSheetLoading + showPurchaseFailAlert || showPurchaseSuccessAlert || isCapturing || !isSceneReady || bundleVM.isPurchasing } var body: some View { ZStack { ZStack(alignment: .bottom) { mainContentView - - // 키링 선택 시트 - keyringSheetOverlay - + // 배경, 카라비너 선택 시트 selectItemSheetContent } @@ -81,6 +79,9 @@ struct BundleEditView: View { .sheet(isPresented: $showPurchaseSheet) { purchaseSheetView } + .sheet(isPresented: $showSelectKeyringSheet) { + keyringSheetContent + } .sheet(isPresented: $bundleVM.showSheetSortSheet) { WorkshopSortSheet( showSheet: $bundleVM.showSheetSortSheet, @@ -125,14 +126,6 @@ struct BundleEditView: View { // 카라비너 변경 시에는 키링 데이터 업데이트만 수행 (Firebase 접근 없음) updateKeyringDataList() } - // 키링 선택 시트 활성화 시 배경, 카라비너 선택 시트의 높이를 낮춤 - .onChange(of: showSelectKeyringSheet) { _, newValue in - withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { - if newValue { - sheetHeight = screenHeight * 0.08 - } - } - } } // MARK: - Main Content Views diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleSearchBar.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleSearchBar.swift new file mode 100644 index 000000000..3a7aba297 --- /dev/null +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleSearchBar.swift @@ -0,0 +1,38 @@ +// +// BundleSearchBar.swift +// Keychy +// +// Created by Claude on 2/6/26. +// + +import SwiftUI + +struct BundleSearchBar: View { + @Binding var searchText: String + + var body: some View { + HStack(spacing: 6) { + Image(systemName: "magnifyingglass") + .foregroundStyle(Color(#colorLiteral(red: 0.6, green: 0.6, blue: 0.6, alpha: 1))) + + TextField("검색", text: $searchText) + .foregroundStyle(Color(#colorLiteral(red: 0.6, green: 0.6, blue: 0.6, alpha: 1))) + .autocorrectionDisabled() + + if !searchText.isEmpty { + Button { + searchText = "" + } label: { + Image(systemName: "xmark.circle.fill") + .foregroundStyle(.gray400) + } + } + } + .padding(.vertical, 10) + .padding(.horizontal, 20) + .background( + RoundedRectangle(cornerRadius: 100) + .fill(Color(#colorLiteral(red: 0.462745098, green: 0.462745098, blue: 0.5019607843, alpha: 0.12))) + ) + } +} diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Shared/KeyringEmptyStateView.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/KeyringEmptyStateView.swift new file mode 100644 index 000000000..69ebf873a --- /dev/null +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/KeyringEmptyStateView.swift @@ -0,0 +1,23 @@ +// +// KeyringEmptyStateView.swift +// Keychy +// +// Created by Claude on 2/6/26. +// + +import SwiftUI + +struct KeyringEmptyStateView: View { + var body: some View { + VStack(spacing: 16) { + Image(.surprisedAlert) + + Text("공방에서 키링을 만들어보세요.\n아직 만들어진 키링이 없어요.") + .typography(.suit15R) + .foregroundStyle(.black100) + .multilineTextAlignment(.center) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 60) + } +} diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Shared/KeyringSelectionContent.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/KeyringSelectionContent.swift new file mode 100644 index 000000000..832176157 --- /dev/null +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/KeyringSelectionContent.swift @@ -0,0 +1,90 @@ +// +// KeyringSelectionContent.swift +// Keychy +// +// Created by Claude on 2/6/26. +// + +import SwiftUI + +/// 키링 선택 시트의 내용물을 담당하는 공유 컴포넌트 +/// Create와 Edit에서 동일한 내용을 다른 시트 방식으로 감싸 사용 +struct KeyringSelectionContent: View { + @Binding var searchText: String + let keyrings: [Keyring] + let isLoading: Bool + let gridColumns: [GridItem] + let cellWidth: CGFloat + let cellHeight: CGFloat + + /// 현재 선택 위치에서 선택된 키링인지 확인 + let isSelectedHere: (Keyring) -> Bool + /// 다른 위치에서 이미 선택된 키링인지 확인 + let isSelectedElsewhere: (Keyring) -> Bool + /// 키링 선택 액션 + let onTapSelect: (Keyring) -> Void + /// 키링 선택 해제 액션 + let onTapDeselect: (Keyring) -> Void + + var body: some View { + if isLoading { + loadingView + } else if keyrings.isEmpty { + KeyringEmptyStateView() + } else { + contentView + } + } + + // MARK: - 로딩 뷰 + private var loadingView: some View { + VStack { + LoadingAlert(type: .short40, message: nil) + .padding(.vertical, 24) + Text("키링을 불러오고 있어요") + .typography(.suit15R) + .foregroundStyle(.black100) + .padding(.vertical, 15) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 60) + } + + // MARK: - 메인 콘텐츠 + private var contentView: some View { + VStack(spacing: 20) { + BundleSearchBar(searchText: $searchText) + .padding(.top, 45) + + ScrollView { + LazyVGrid(columns: gridColumns, spacing: 10) { + ForEach(filteredKeyrings, id: \.self) { keyring in + KeyringCell( + keyring: keyring, + isSelectedHere: isSelectedHere(keyring), + isSelectedElsewhere: isSelectedElsewhere(keyring), + width: cellWidth, + height: cellHeight, + onTapSelect: { + onTapSelect(keyring) + }, + onTapDeselect: { + onTapDeselect(keyring) + } + ) + } + } + } + } + } + + // MARK: - 필터링된 키링 목록 + private var filteredKeyrings: [Keyring] { + if searchText.isEmpty { + return keyrings + } + return keyrings.filter { keyring in + keyring.name.localizedCaseInsensitiveContains(searchText) + } + } +} From d65a74dcf0a07fbf2cbfadeb587ac301ba2630bc Mon Sep 17 00:00:00 2001 From: giljihun Date: Sat, 7 Feb 2026 19:40:14 +0900 Subject: [PATCH 04/10] =?UTF-8?q?style:=20=EC=84=A0=ED=83=9D=20=ED=85=8C?= =?UTF-8?q?=EB=91=90=EB=A6=AC,=20=EC=9E=A5=EC=B0=A9=EC=A4=91=20=ED=85=8D?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Core/DesignSystem/Typography/Typography.swift | 1 + .../Presentation/Bundle/Views/Shared/KeyringCell.swift | 10 +++++----- .../Bundle/Views/Shared/KeyringSelectionContent.swift | 4 ++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Keychy/Keychy/Core/DesignSystem/Typography/Typography.swift b/Keychy/Keychy/Core/DesignSystem/Typography/Typography.swift index 21023dc00..001ae59c3 100644 --- a/Keychy/Keychy/Core/DesignSystem/Typography/Typography.swift +++ b/Keychy/Keychy/Core/DesignSystem/Typography/Typography.swift @@ -99,6 +99,7 @@ struct Typography { static let notosans12R = Typography(font: .custom(.notoSansRegular, size: 12), lineSpacing: 0) static let notosans13M = Typography(font: .custom(.notoSansMedium, size: 13), lineSpacing: 0) + static let notosans13SB = Typography(font: .custom(.notoSansSemiBold, size: 13), lineSpacing: 0) static let notosans14SB = Typography(font: .custom(.notoSansSemiBold, size: 14), lineSpacing: 0) static let notosans14M = Typography(font: .custom(.notoSansMedium, size: 14), lineSpacing: 0) diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Shared/KeyringCell.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/KeyringCell.swift index e06576092..f6f252305 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Shared/KeyringCell.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/KeyringCell.swift @@ -27,7 +27,7 @@ struct KeyringCell: View { } } label: { ZStack(alignment: .bottomTrailing) { - VStack(spacing: 10) { + VStack(spacing: 6) { ZStack { CollectionCellView(keyring: keyring) .frame(width: width, height: height) @@ -35,7 +35,7 @@ struct KeyringCell: View { // 선택 테두리 RoundedRectangle(cornerRadius: 10) - .strokeBorder(isSelectedHere ? .mainOpacity80 : .clear, lineWidth: 1.8) + .strokeBorder(isSelectedHere ? .main500 : .clear, lineWidth: 2) .frame(width: width, height: height) // 다른 위치에 장착된 경우 dim @@ -47,7 +47,7 @@ struct KeyringCell: View { } Text(keyring.name) - .typography(isSelectedHere ? .notosans14SB : .notosans14M) + .typography(isSelectedHere ? .notosans13SB : .notosans13M) .foregroundStyle(isSelectedHere ? .main500 : .black100) .lineLimit(1) .truncationMode(.tail) @@ -58,9 +58,9 @@ struct KeyringCell: View { VStack { HStack { Spacer() - Text("장착 중") + Text("장착중") .foregroundStyle(.white100) - .typography(.suit13M) + .typography(.suit12M) .padding(.horizontal, 10) .padding(.vertical, 4) .background( diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Shared/KeyringSelectionContent.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/KeyringSelectionContent.swift index 832176157..59a1de510 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Shared/KeyringSelectionContent.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/KeyringSelectionContent.swift @@ -56,8 +56,8 @@ struct KeyringSelectionContent: View { BundleSearchBar(searchText: $searchText) .padding(.top, 45) - ScrollView { - LazyVGrid(columns: gridColumns, spacing: 10) { + ScrollView(showsIndicators: false) { + LazyVGrid(columns: gridColumns, spacing: 14) { ForEach(filteredKeyrings, id: \.self) { keyring in KeyringCell( keyring: keyring, From 0d33d73e525e29d165b80ac03e4902fe8584b49f Mon Sep 17 00:00:00 2001 From: giljihun Date: Sat, 7 Feb 2026 20:26:05 +0900 Subject: [PATCH 05/10] =?UTF-8?q?style:=20Gradient=20-=20=ED=8E=98?= =?UTF-8?q?=EC=8A=A4=ED=8B=B0=EB=B2=8C=EC=B6=9C=ED=92=88=20=EC=98=A4?= =?UTF-8?q?=EB=B2=84=EB=A0=88=EC=9D=B4=20=EC=9A=A9=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Keychy/Keychy.xcodeproj/project.pbxproj | 40 +++--- .../DesignSystem/Colors/Gradient+Keychy.swift | 7 + .../Views/Shared/BundleKeyringCellView.swift | 123 ++++++++++++++++++ .../Bundle/Views/Shared/KeyringCell.swift | 2 +- .../01. Colors/5. Gradient/Contents.json | 6 + .../Gradient3.colorset/Contents.json | 38 ++++++ .../Gradient4.colorset/Contents.json | 38 ++++++ 7 files changed, 235 insertions(+), 19 deletions(-) create mode 100644 Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleKeyringCellView.swift create mode 100644 Keychy/Keychy/Resources/Assets.xcassets/01. Colors/5. Gradient/Contents.json create mode 100644 Keychy/Keychy/Resources/Assets.xcassets/01. Colors/5. Gradient/Gradient3.colorset/Contents.json create mode 100644 Keychy/Keychy/Resources/Assets.xcassets/01. Colors/5. Gradient/Gradient4.colorset/Contents.json diff --git a/Keychy/Keychy.xcodeproj/project.pbxproj b/Keychy/Keychy.xcodeproj/project.pbxproj index 20ce30c51..48f5546cb 100644 --- a/Keychy/Keychy.xcodeproj/project.pbxproj +++ b/Keychy/Keychy.xcodeproj/project.pbxproj @@ -109,9 +109,6 @@ 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 */; }; - BC00020F2F35F00200000003 /* BundleSearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC00020D2F35F00200000001 /* BundleSearchBar.swift */; }; - BC0002102F35F00200000004 /* KeyringEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC00020E2F35F00200000002 /* KeyringEmptyStateView.swift */; }; - BC0002132F35F00200000006 /* KeyringSelectionContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0002112F35F00200000005 /* KeyringSelectionContent.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 */; }; @@ -361,12 +358,6 @@ AA2146B52F15D8490048D40E /* KeyringCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA2146B42F15D8490048D40E /* KeyringCell.swift */; }; AA2146B72F15E5B60048D40E /* BundleEditView+SelectSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA2146B62F15E5B60048D40E /* BundleEditView+SelectSheet.swift */; }; AA2146BB2F161D0C0048D40E /* BundleEditView+Initialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA2146BA2F161D0C0048D40E /* BundleEditView+Initialization.swift */; }; - BC0001072F35F00100000007 /* BundleCreateView+SelectSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0001012F35F00100000001 /* BundleCreateView+SelectSheet.swift */; }; - BC0001082F35F00100000008 /* BundleCreateView+Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0001022F35F00100000002 /* BundleCreateView+Alert.swift */; }; - BC0001092F35F00100000009 /* BundleCreateView+Purchase.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0001032F35F00100000003 /* BundleCreateView+Purchase.swift */; }; - BC00010A2F35F0010000000A /* BundleCreateView+Initialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0001042F35F00100000004 /* BundleCreateView+Initialization.swift */; }; - BC00010B2F35F0010000000B /* BundleCreateView+Capture.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0001052F35F00100000005 /* BundleCreateView+Capture.swift */; }; - BC00010C2F35F0010000000C /* BundleEditView+Capture.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0001062F35F00100000006 /* BundleEditView+Capture.swift */; }; AA3908F82EC8BF0400D87EEC /* BundleDetailView+SaveImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3908F72EC8BF0400D87EEC /* BundleDetailView+SaveImage.swift */; }; AA3909462EC9F29500D87EEC /* UIApplication+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3909452EC9F29500D87EEC /* UIApplication+Extension.swift */; }; AA39098E2ECA061700D87EEC /* GridItemSpacing.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA39098D2ECA061700D87EEC /* GridItemSpacing.swift */; }; @@ -392,6 +383,16 @@ AABA4DBD2ED2D6CA00A7D062 /* festivalCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = AABA4DBC2ED2D6CA00A7D062 /* festivalCard.swift */; }; AAEB46AD2EC1C893002B13E5 /* BundleMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAEB46AC2EC1C893002B13E5 /* BundleMenu.swift */; }; AAEB46AF2EC1D648002B13E5 /* BundleNameEditView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAEB46AE2EC1D648002B13E5 /* BundleNameEditView.swift */; }; + BC0001072F35F00100000007 /* BundleCreateView+SelectSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0001012F35F00100000001 /* BundleCreateView+SelectSheet.swift */; }; + BC0001082F35F00100000008 /* BundleCreateView+Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0001022F35F00100000002 /* BundleCreateView+Alert.swift */; }; + BC0001092F35F00100000009 /* BundleCreateView+Purchase.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0001032F35F00100000003 /* BundleCreateView+Purchase.swift */; }; + BC00010A2F35F0010000000A /* BundleCreateView+Initialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0001042F35F00100000004 /* BundleCreateView+Initialization.swift */; }; + BC00010B2F35F0010000000B /* BundleCreateView+Capture.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0001052F35F00100000005 /* BundleCreateView+Capture.swift */; }; + BC00010C2F35F0010000000C /* BundleEditView+Capture.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0001062F35F00100000006 /* BundleEditView+Capture.swift */; }; + BC00020F2F35F00200000003 /* BundleSearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC00020D2F35F00200000001 /* BundleSearchBar.swift */; }; + BC0002102F35F00200000004 /* KeyringEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC00020E2F35F00200000002 /* KeyringEmptyStateView.swift */; }; + BC0002132F35F00200000006 /* KeyringSelectionContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0002112F35F00200000005 /* KeyringSelectionContent.swift */; }; + BC0002152F35F00200000008 /* BundleKeyringCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0002142F35F00200000007 /* BundleKeyringCellView.swift */; }; C645AE9F2EB1055C004BFE69 /* CategoryTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = C645AE9E2EB1055C004BFE69 /* CategoryTabBar.swift */; }; C645AEA32EB1B8FC004BFE69 /* DataInitializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C645AEA22EB1B8FC004BFE69 /* DataInitializer.swift */; }; C665DDE82EAEFA8700CE4495 /* CoinChargeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C665DDE72EAEFA8700CE4495 /* CoinChargeView.swift */; }; @@ -568,9 +569,6 @@ 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 = ""; }; - BC00020D2F35F00200000001 /* BundleSearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleSearchBar.swift; sourceTree = ""; }; - BC00020E2F35F00200000002 /* KeyringEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyringEmptyStateView.swift; sourceTree = ""; }; - BC0002112F35F00200000005 /* KeyringSelectionContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyringSelectionContent.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 = ""; }; @@ -820,12 +818,6 @@ AA2146B42F15D8490048D40E /* KeyringCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyringCell.swift; sourceTree = ""; }; AA2146B62F15E5B60048D40E /* BundleEditView+SelectSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleEditView+SelectSheet.swift"; sourceTree = ""; }; AA2146BA2F161D0C0048D40E /* BundleEditView+Initialization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleEditView+Initialization.swift"; sourceTree = ""; }; - BC0001012F35F00100000001 /* BundleCreateView+SelectSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleCreateView+SelectSheet.swift"; sourceTree = ""; }; - BC0001022F35F00100000002 /* BundleCreateView+Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleCreateView+Alert.swift"; sourceTree = ""; }; - BC0001032F35F00100000003 /* BundleCreateView+Purchase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleCreateView+Purchase.swift"; sourceTree = ""; }; - BC0001042F35F00100000004 /* BundleCreateView+Initialization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleCreateView+Initialization.swift"; sourceTree = ""; }; - BC0001052F35F00100000005 /* BundleCreateView+Capture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleCreateView+Capture.swift"; sourceTree = ""; }; - BC0001062F35F00100000006 /* BundleEditView+Capture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleEditView+Capture.swift"; sourceTree = ""; }; AA3908F72EC8BF0400D87EEC /* BundleDetailView+SaveImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleDetailView+SaveImage.swift"; sourceTree = ""; }; AA3909452EC9F29500D87EEC /* UIApplication+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Extension.swift"; sourceTree = ""; }; AA39098D2ECA061700D87EEC /* GridItemSpacing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridItemSpacing.swift; sourceTree = ""; }; @@ -851,6 +843,16 @@ AABA4DBC2ED2D6CA00A7D062 /* festivalCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = festivalCard.swift; sourceTree = ""; }; AAEB46AC2EC1C893002B13E5 /* BundleMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleMenu.swift; sourceTree = ""; }; AAEB46AE2EC1D648002B13E5 /* BundleNameEditView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleNameEditView.swift; sourceTree = ""; }; + BC0001012F35F00100000001 /* BundleCreateView+SelectSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleCreateView+SelectSheet.swift"; sourceTree = ""; }; + BC0001022F35F00100000002 /* BundleCreateView+Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleCreateView+Alert.swift"; sourceTree = ""; }; + BC0001032F35F00100000003 /* BundleCreateView+Purchase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleCreateView+Purchase.swift"; sourceTree = ""; }; + BC0001042F35F00100000004 /* BundleCreateView+Initialization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleCreateView+Initialization.swift"; sourceTree = ""; }; + BC0001052F35F00100000005 /* BundleCreateView+Capture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleCreateView+Capture.swift"; sourceTree = ""; }; + BC0001062F35F00100000006 /* BundleEditView+Capture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleEditView+Capture.swift"; sourceTree = ""; }; + BC00020D2F35F00200000001 /* BundleSearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleSearchBar.swift; sourceTree = ""; }; + BC00020E2F35F00200000002 /* KeyringEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyringEmptyStateView.swift; sourceTree = ""; }; + BC0002112F35F00200000005 /* KeyringSelectionContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyringSelectionContent.swift; sourceTree = ""; }; + BC0002142F35F00200000007 /* BundleKeyringCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleKeyringCellView.swift; sourceTree = ""; }; C645AE9E2EB1055C004BFE69 /* CategoryTabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryTabBar.swift; sourceTree = ""; }; C645AEA22EB1B8FC004BFE69 /* DataInitializer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataInitializer.swift; sourceTree = ""; }; C665DDE72EAEFA8700CE4495 /* CoinChargeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinChargeView.swift; sourceTree = ""; }; @@ -1808,6 +1810,7 @@ 4CEC61F62EAE08DA0099ECEE /* BackgroundCell.swift */, AA9115072EB126A60026E9BC /* CarabinerCell.swift */, AA2146B42F15D8490048D40E /* KeyringCell.swift */, + BC0002142F35F00200000007 /* BundleKeyringCellView.swift */, AAA4467B2EC64C9900080AB1 /* SelectBackgroundSheet.swift */, AAA446812EC6519700080AB1 /* SelectCarabinerSheet.swift */, 4C2525D92F35B2A7003CC5AD /* BundleSheetFilterBar.swift */, @@ -2767,6 +2770,7 @@ BC00020F2F35F00200000003 /* BundleSearchBar.swift in Sources */, BC0002102F35F00200000004 /* KeyringEmptyStateView.swift in Sources */, BC0002132F35F00200000006 /* KeyringSelectionContent.swift in Sources */, + BC0002152F35F00200000008 /* BundleKeyringCellView.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/Core/DesignSystem/Colors/Gradient+Keychy.swift b/Keychy/Keychy/Core/DesignSystem/Colors/Gradient+Keychy.swift index cf7f8189c..f77188c13 100644 --- a/Keychy/Keychy/Core/DesignSystem/Colors/Gradient+Keychy.swift +++ b/Keychy/Keychy/Core/DesignSystem/Colors/Gradient+Keychy.swift @@ -13,6 +13,7 @@ import SwiftUI enum GradientStyle { case primary + case festivalPublished } extension GradientStyle { @@ -24,6 +25,12 @@ extension GradientStyle { startPoint: .topLeading, endPoint: .bottomTrailing ) + case .festivalPublished: + return LinearGradient( + colors: [.gradient3, .gradient4], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) } } } diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleKeyringCellView.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleKeyringCellView.swift new file mode 100644 index 000000000..eec131a76 --- /dev/null +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleKeyringCellView.swift @@ -0,0 +1,123 @@ +// +// BundleKeyringCellView.swift +// Keychy +// +// Created by 길지훈 on 2/6/26. +// + +import SwiftUI +import SpriteKit + +/// Bundle 전용 키링 셀 뷰 (단순화 버전) +/// - 캐시된 이미지 표시 +/// - 캐시 없으면 Scene fallback +/// - 상태 오버레이 +struct BundleKeyringCellView: View { + let keyring: Keyring + + @State private var isLoading = true + @State private var cachedImage: UIImage? + @State private var scene: KeyringCellScene? + + var body: some View { + ZStack { + Color.white100 + + // 콘텐츠 + if let image = cachedImage { + Image(uiImage: image) + .resizable() + .aspectRatio(contentMode: .fill) + } else if let scene = scene { + SpriteView(scene: scene) + } + + // 로딩 + if isLoading && cachedImage == nil && scene == nil { + LoadingAlert(type: .short40, message: nil) + } + + // 상태 오버레이 (포장중/출품중) + if let info = keyring.status.overlayInfo { + statusOverlay(info: info) + } + } + .onAppear { loadContent() } + .onDisappear { cleanupScene() } + } + + // MARK: - 로딩 + private func loadContent() { + // 1. 캐시 확인 + if let keyringID = keyring.documentId, + let imageData = KeyringImageCache.shared.load(for: keyringID, type: .thumbnail), + let image = UIImage(data: imageData) { + cachedImage = image + isLoading = false + return + } + + // 2. 캐시 없으면 Scene 생성 + createScene() + } + + private func createScene() { + let newScene = KeyringCellScene( + ringType: RingType.fromID(keyring.selectedRing), + chainType: ChainType.fromID(keyring.selectedChain), + bodyImage: keyring.bodyImage, + templateId: keyring.selectedTemplate, + targetSize: CGSize(width: 175, height: 233), + zoomScale: 2.0, + hookOffsetY: keyring.hookOffsetY, + chainLength: keyring.chainLength, + onLoadingComplete: { + DispatchQueue.main.async { + isLoading = false + } + } + ) + newScene.scaleMode = .aspectFill + scene = newScene + } + + private func cleanupScene() { + scene?.removeAllChildren() + scene?.removeAllActions() + scene?.physicsWorld.removeAllJoints() + scene?.view?.presentScene(nil) + scene = nil + } + + // MARK: - 상태 오버레이 + private func statusOverlay(info: String) -> some View { + RoundedRectangle(cornerRadius: 10) + .fill(.black50) + .overlay { + VStack { + Text(info) + .typography(.suit12M) + .foregroundColor(keyring.status == .packaged ? .white100 : .main500) + .padding(.vertical, 4) + .frame(maxWidth: .infinity) + .background { + if keyring.status == .packaged { + RoundedRectangle(cornerRadius: 20) + .fill(Color.black60) + .frame(height: 23) + } else { + RoundedRectangle(cornerRadius: 20) + .fill(.gradient(.festivalPublished)) + .frame(height: 23) + .overlay( + RoundedRectangle(cornerRadius: 20) + .strokeBorder(.main50, lineWidth: 1) + ) + } + } + Spacer() + } + .padding(6) + } + } +} diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Shared/KeyringCell.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/KeyringCell.swift index f6f252305..4d49e1684 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Shared/KeyringCell.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/KeyringCell.swift @@ -29,7 +29,7 @@ struct KeyringCell: View { ZStack(alignment: .bottomTrailing) { VStack(spacing: 6) { ZStack { - CollectionCellView(keyring: keyring) + BundleKeyringCellView(keyring: keyring) .frame(width: width, height: height) .cornerRadius(10) diff --git a/Keychy/Keychy/Resources/Assets.xcassets/01. Colors/5. Gradient/Contents.json b/Keychy/Keychy/Resources/Assets.xcassets/01. Colors/5. Gradient/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Keychy/Keychy/Resources/Assets.xcassets/01. Colors/5. Gradient/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Keychy/Keychy/Resources/Assets.xcassets/01. Colors/5. Gradient/Gradient3.colorset/Contents.json b/Keychy/Keychy/Resources/Assets.xcassets/01. Colors/5. Gradient/Gradient3.colorset/Contents.json new file mode 100644 index 000000000..ac2e387b6 --- /dev/null +++ b/Keychy/Keychy/Resources/Assets.xcassets/01. Colors/5. Gradient/Gradient3.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xE8", + "red" : "0xF9" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFE" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Keychy/Keychy/Resources/Assets.xcassets/01. Colors/5. Gradient/Gradient4.colorset/Contents.json b/Keychy/Keychy/Resources/Assets.xcassets/01. Colors/5. Gradient/Gradient4.colorset/Contents.json new file mode 100644 index 000000000..54e9c2829 --- /dev/null +++ b/Keychy/Keychy/Resources/Assets.xcassets/01. Colors/5. Gradient/Gradient4.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFE", + "green" : "0xFF", + "red" : "0xCE" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFE" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} From 52ebf740d6513bf7da4c1dd2f8c77cf52e52aa1e Mon Sep 17 00:00:00 2001 From: giljihun Date: Sat, 7 Feb 2026 20:29:30 +0900 Subject: [PATCH 06/10] =?UTF-8?q?style:=20=ED=82=A4=EB=A7=81=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=20=EC=8B=9C=ED=8A=B8=20=EC=B5=9C=EB=8C=80=20=EB=86=92?= =?UTF-8?q?=EC=9D=B4=20=ED=94=84=EB=9E=99=EC=85=98=200.95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Bundle/Views/Create/BundleCreateView+SelectSheet.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+SelectSheet.swift b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+SelectSheet.swift index eea53a3cc..29f655bda 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+SelectSheet.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+SelectSheet.swift @@ -130,7 +130,7 @@ extension BundleCreateView { } ) .padding(.horizontal, 20) - .presentationDetents([.fraction(0.45), .fraction(0.85)]) + .presentationDetents([.fraction(0.45), .fraction(0.95)]) .presentationDragIndicator(.visible) } From 8ed095efe8549ae0735d3df011276fbd0e458163 Mon Sep 17 00:00:00 2001 From: giljihun Date: Sat, 7 Feb 2026 20:47:24 +0900 Subject: [PATCH 07/10] =?UTF-8?q?feat:=20=EB=AD=89=EC=B9=98=20=EB=A7=8C?= =?UTF-8?q?=EB=93=A4=EA=B8=B0=20=ED=82=A4=EB=A7=81=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EC=8B=9C=ED=8A=B8=20-=20=ED=82=A4=EB=A7=81=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=20=EC=9B=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Views/Shared/BundleKeyringCellView.swift | 41 +++++++++++++++++- .../Bundle/Views/Shared/KeyringCell.swift | 2 +- .../checkMarkWhite.imageset/Contents.json | 12 +++++ .../checkMarkWhite.pdf | Bin 0 -> 4293 bytes 4 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Status/checkMarkWhite.imageset/Contents.json create mode 100644 Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Status/checkMarkWhite.imageset/checkMarkWhite.pdf diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleKeyringCellView.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleKeyringCellView.swift index eec131a76..86c0c8ed4 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleKeyringCellView.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleKeyringCellView.swift @@ -14,7 +14,8 @@ import SpriteKit /// - 상태 오버레이 struct BundleKeyringCellView: View { let keyring: Keyring - + let isSelected: Bool + @State private var isLoading = true @State private var cachedImage: UIImage? @State private var scene: KeyringCellScene? @@ -40,6 +41,9 @@ struct BundleKeyringCellView: View { // 상태 오버레이 (포장중/출품중) if let info = keyring.status.overlayInfo { statusOverlay(info: info) + } else { + // 선택 원 (선택 시 체크마크, 미선택 시 빈 원) + selectionCircle } } .onAppear { loadContent() } @@ -89,6 +93,41 @@ struct BundleKeyringCellView: View { scene = nil } + // MARK: - 선택 원 + private var selectionCircle: some View { + VStack { + Spacer() + HStack { + Spacer() + if isSelected { + Circle() + .fill(.main500) + .frame(width: 26.14, height: 26.14) + .overlay( + Image(.checkMarkWhite) + ) + .overlay( + Circle() + .strokeBorder(.white100, lineWidth: 1) + .shadow(color: Color.black.opacity(0.25), radius: 4, x: 0, y: 0) + ) + .shadow(color: Color.black.opacity(0.25), radius: 4, x: 0, y: 0) + } else { + Circle() + .fill(.clear) + .frame(width: 26.14, height: 26.14) + .overlay( + Circle() + .strokeBorder(.white100, lineWidth: 1) + .shadow(color: Color.black.opacity(0.25), radius: 4, x: 0, y: 0) + ) + .shadow(color: Color.black.opacity(0.25), radius: 4, x: 0, y: 0) + } + } + } + .padding(6) + } + // MARK: - 상태 오버레이 private func statusOverlay(info: String) -> some View { RoundedRectangle(cornerRadius: 10) diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Shared/KeyringCell.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/KeyringCell.swift index 4d49e1684..a0bb31c1a 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Shared/KeyringCell.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/KeyringCell.swift @@ -29,7 +29,7 @@ struct KeyringCell: View { ZStack(alignment: .bottomTrailing) { VStack(spacing: 6) { ZStack { - BundleKeyringCellView(keyring: keyring) + BundleKeyringCellView(keyring: keyring, isSelected: isSelectedHere) .frame(width: width, height: height) .cornerRadius(10) diff --git a/Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Status/checkMarkWhite.imageset/Contents.json b/Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Status/checkMarkWhite.imageset/Contents.json new file mode 100644 index 000000000..8a1f88aa2 --- /dev/null +++ b/Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Status/checkMarkWhite.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "checkMarkWhite.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Status/checkMarkWhite.imageset/checkMarkWhite.pdf b/Keychy/Keychy/Resources/Assets.xcassets/18. Icons/Status/checkMarkWhite.imageset/checkMarkWhite.pdf new file mode 100644 index 0000000000000000000000000000000000000000..f71b2c0bbd330f03a5f5a5a1051cbc50c07ecef4 GIT binary patch literal 4293 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 RyyGc)enLTq59^-N`#-BANF@LO literal 0 HcmV?d00001 From ebfafa7b90da14493f6e3a03175e693e3c0201d6 Mon Sep 17 00:00:00 2001 From: giljihun Date: Sat, 7 Feb 2026 21:09:40 +0900 Subject: [PATCH 08/10] =?UTF-8?q?feat:=20=EB=AD=89=EC=B9=98=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20-=20=ED=82=A4=EB=A7=81=EA=B1=B8=EA=B8=B0=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 1순위: 현재 위치에 장착된 키링 - 2순위: 다른 위치에 장착된 키링들 - 다른 장착된 키링 선택 시, 시트가 내려가지 않고 해제됨. - 현재 위치의 키링값이 바뀌어야 시트가 닫힌다. --- .../ViewModels/BundleViewModel+Edit.swift | 34 +++++++++---------- .../Create/BundleCreateView+SelectSheet.swift | 21 +++++++++--- .../Edit/BundleEditView+SelectSheet.swift | 22 ++++++++---- .../Bundle/Views/Shared/KeyringCell.swift | 15 ++------ 4 files changed, 51 insertions(+), 41 deletions(-) diff --git a/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Edit.swift b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Edit.swift index 96d3d4427..2f6c0f1dc 100644 --- a/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Edit.swift +++ b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Edit.swift @@ -192,41 +192,39 @@ extension BundleViewModel { /// 키링 선택 시트용 정렬된 키링 리스트 /// - 1순위: 현재 위치에 선택된 키링 - /// - 2순위: 일반 키링들 (선택되지 않고, published/packaged 아님) - /// - 3순위: 다른 위치에 장착된 키링들 + /// - 1순위: 현재 위치에 장착된 키링 + /// - 2순위: 다른 위치에 장착된 키링들 + /// - 3순위: 일반 키링들 (선택되지 않고, published/packaged 아님) /// - 4순위: published 또는 packaged 상태의 키링들 (맨 뒤) func sortedKeyringsForSelection(selectedKeyrings: [Int: Keyring], selectedPosition: Int) -> [Keyring] { - let selectedKeyring = selectedKeyrings[selectedPosition] + let currentKeyring = selectedKeyrings[selectedPosition] return keyring.sorted { keyring1, keyring2 in - let isKeyring1SelectedHere = keyring1.id == selectedKeyring?.id - let isKeyring2SelectedHere = keyring2.id == selectedKeyring?.id + let isKeyring1Current = keyring1.id == currentKeyring?.id + let isKeyring2Current = keyring2.id == currentKeyring?.id - let isKeyring1SelectedElsewhere = selectedKeyrings.values.contains { $0.id == keyring1.id } && !isKeyring1SelectedHere - let isKeyring2SelectedElsewhere = selectedKeyrings.values.contains { $0.id == keyring2.id } && !isKeyring2SelectedHere + let isKeyring1Elsewhere = selectedKeyrings.values.contains { $0.id == keyring1.id } && !isKeyring1Current + let isKeyring2Elsewhere = selectedKeyrings.values.contains { $0.id == keyring2.id } && !isKeyring2Current let isKeyring1Unavailable = keyring1.status == .published || keyring1.status == .packaged let isKeyring2Unavailable = keyring2.status == .published || keyring2.status == .packaged - if isKeyring1SelectedHere != isKeyring2SelectedHere { - return isKeyring1SelectedHere + // 1순위: 현재 위치 키링 + if isKeyring1Current != isKeyring2Current { + return isKeyring1Current } - let isKeyring1Normal = !isKeyring1SelectedElsewhere && !isKeyring1Unavailable - let isKeyring2Normal = !isKeyring2SelectedElsewhere && !isKeyring2Unavailable - - if isKeyring1Normal != isKeyring2Normal { - return isKeyring1Normal - } - - if isKeyring1SelectedElsewhere != isKeyring2SelectedElsewhere { - return isKeyring1SelectedElsewhere + // 2순위: 다른 위치 장착 키링 + if isKeyring1Elsewhere != isKeyring2Elsewhere { + return isKeyring1Elsewhere } + // 3순위: 일반 키링 (사용 불가 아닌 것) if isKeyring1Unavailable != isKeyring2Unavailable { return isKeyring2Unavailable } + // 동일 순위면 원래 순서 유지 guard let index1 = keyring.firstIndex(of: keyring1), let index2 = keyring.firstIndex(of: keyring2) else { return false diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+SelectSheet.swift b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+SelectSheet.swift index 29f655bda..340060f8b 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+SelectSheet.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView+SelectSheet.swift @@ -115,17 +115,28 @@ extension BundleCreateView { !(selectedKeyrings[selectedPosition]?.id == keyring.id) }, onTapSelect: { keyring in - if selectedKeyrings[selectedPosition] != nil { - keyringOrder.removeAll { $0 == selectedPosition } + // 다른 위치에 이미 장착된 키링인지 확인 + let existingPosition = selectedKeyrings.first { $0.value.id == keyring.id }?.key + + if let existingPos = existingPosition, existingPos != selectedPosition { + // 다른 위치에서 제거만 (현재 위치에 장착 X, 시트 유지) + selectedKeyrings[existingPos] = nil + keyringOrder.removeAll { $0 == existingPos } + } else { + // 새 키링 선택 → 현재 위치에 장착, 시트 닫기 + if selectedKeyrings[selectedPosition] != nil { + keyringOrder.removeAll { $0 == selectedPosition } + } + selectedKeyrings[selectedPosition] = keyring + keyringOrder.append(selectedPosition) + showKeyringSheet = false } - selectedKeyrings[selectedPosition] = keyring - keyringOrder.append(selectedPosition) - showKeyringSheet = false sceneRefreshId = UUID() }, onTapDeselect: { keyring in selectedKeyrings[selectedPosition] = nil keyringOrder.removeAll { $0 == selectedPosition } + showKeyringSheet = false // 시트 닫기 sceneRefreshId = UUID() } ) diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+SelectSheet.swift b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+SelectSheet.swift index 68b04570a..2105941e1 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+SelectSheet.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView+SelectSheet.swift @@ -83,18 +83,28 @@ extension BundleEditView { !(bundleVM.selectedKeyrings[selectedPosition]?.id == keyring.id) }, onTapSelect: { keyring in - if bundleVM.selectedKeyrings[selectedPosition] != nil { - bundleVM.keyringOrder.removeAll { $0 == selectedPosition } + // 다른 위치에 이미 장착된 키링인지 확인 + let existingPosition = bundleVM.selectedKeyrings.first { $0.value.id == keyring.id }?.key + + if let existingPos = existingPosition, existingPos != selectedPosition { + // 다른 위치에서 제거만 (현재 위치에 장착 X, 시트 유지) + bundleVM.selectedKeyrings[existingPos] = nil + bundleVM.keyringOrder.removeAll { $0 == existingPos } + } else { + // 새 키링 선택 → 현재 위치에 장착, 시트 닫기 + if bundleVM.selectedKeyrings[selectedPosition] != nil { + bundleVM.keyringOrder.removeAll { $0 == selectedPosition } + } + bundleVM.selectedKeyrings[selectedPosition] = keyring + bundleVM.keyringOrder.append(selectedPosition) + showSelectKeyringSheet = false } - bundleVM.selectedKeyrings[selectedPosition] = keyring - bundleVM.keyringOrder.append(selectedPosition) - showSelectKeyringSheet = false updateKeyringDataList() }, onTapDeselect: { keyring in bundleVM.selectedKeyrings[selectedPosition] = nil bundleVM.keyringOrder.removeAll { $0 == selectedPosition } - showSelectKeyringSheet = false + showSelectKeyringSheet = false // 시트 닫기 updateKeyringDataList() } ) diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Shared/KeyringCell.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/KeyringCell.swift index a0bb31c1a..0fbf063d0 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Shared/KeyringCell.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/KeyringCell.swift @@ -20,16 +20,14 @@ struct KeyringCell: View { Button { if isSelectedHere { onTapDeselect() - } else if !isSelectedElsewhere { - onTapSelect() } else { - // 중복 선택 방지: 아무 것도 하지 않음 + onTapSelect() } } label: { ZStack(alignment: .bottomTrailing) { VStack(spacing: 6) { ZStack { - BundleKeyringCellView(keyring: keyring, isSelected: isSelectedHere) + BundleKeyringCellView(keyring: keyring, isSelected: isSelectedHere || isSelectedElsewhere) .frame(width: width, height: height) .cornerRadius(10) @@ -37,13 +35,6 @@ struct KeyringCell: View { RoundedRectangle(cornerRadius: 10) .strokeBorder(isSelectedHere ? .main500 : .clear, lineWidth: 2) .frame(width: width, height: height) - - // 다른 위치에 장착된 경우 dim - if isSelectedElsewhere { - RoundedRectangle(cornerRadius: 10) - .fill(Color.black50) - .frame(width: width, height: height) - } } Text(keyring.name) @@ -75,7 +66,7 @@ struct KeyringCell: View { } } } - .disabled(keyring.status == .packaged || keyring.status == .published || isSelectedElsewhere) + .disabled(keyring.status == .packaged || keyring.status == .published) .opacity(1.0) } } From 8c57870dabe0539ffbd15ce971a537ea8d4155a9 Mon Sep 17 00:00:00 2001 From: giljihun Date: Sat, 7 Feb 2026 21:22:18 +0900 Subject: [PATCH 09/10] =?UTF-8?q?feat:=20=EB=B2=88=EB=93=A4=ED=82=A4?= =?UTF-8?q?=EB=A7=81=EB=8B=AC=EA=B8=B0=20-=20=EC=9D=B4=EB=A6=84=EB=85=B8?= =?UTF-8?q?=EC=9E=85=EB=A0=A5=20->=20=EB=B9=84=ED=99=9C=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Core/Components/View/Button/ToolbarButtons.swift | 4 +++- .../Bundle/Views/Create/BundleCreateView.swift | 10 +++++++++- .../Bundle/Views/Create/BundleNameInputView.swift | 12 ++++++------ 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/Keychy/Keychy/Core/Components/View/Button/ToolbarButtons.swift b/Keychy/Keychy/Core/Components/View/Button/ToolbarButtons.swift index 1b6a3c251..4dd85612f 100644 --- a/Keychy/Keychy/Core/Components/View/Button/ToolbarButtons.swift +++ b/Keychy/Keychy/Core/Components/View/Button/ToolbarButtons.swift @@ -136,6 +136,7 @@ struct PurchaseToolbarButton: View { // MARK: - Custom Text Toolbar Button struct TextToolbarButton: View { let title: String + var isDisabled: Bool = false let action: () -> Void var body: some View { @@ -143,9 +144,10 @@ struct TextToolbarButton: View { Text(title) .typography(.suit17B) .padding(4) - .foregroundStyle(.black100) + .foregroundStyle(isDisabled ? .gray300 : .main500) } .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 d1aae8783..a8a1b809a 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleCreateView.swift @@ -182,11 +182,19 @@ extension BundleCreateView { showPurchaseSheet = true } } else { - NextToolbarButton(isDisabled: isCapturing || selectedKeyrings.isEmpty) { + Button { Task { await captureAndSaveScene() } + } label: { + Text("다음") + .typography(.suit17B) + .padding(4) + .foregroundStyle(isCapturing || selectedKeyrings.isEmpty ? .gray300 : .main500) } + .frame(width: 62, height: 44) + .glassEffect(.regular.interactive(), in: .capsule) + .disabled(isCapturing || selectedKeyrings.isEmpty) } } } diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleNameInputView.swift b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleNameInputView.swift index 4a2008c59..33080d622 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleNameInputView.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Create/BundleNameInputView.swift @@ -160,14 +160,14 @@ extension BundleNameInputView { } center: { EmptyView() } trailing: { - TextToolbarButton(title: "완료") { + TextToolbarButton( + title: "완료", + isDisabled: isUploading || + bundleName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || + hasProfanity + ) { handleNextButtonTap() } - .disabled( - isUploading || - bundleName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || - hasProfanity - ) } } From 1e0ea452900200f2ddbf1ab01c5f40755b28b381 Mon Sep 17 00:00:00 2001 From: giljihun Date: Sat, 7 Feb 2026 21:27:45 +0900 Subject: [PATCH 10/10] =?UTF-8?q?=ED=81=B4=EB=A1=9C=EB=93=9C=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EB=B6=84=EB=A6=AC=20=EC=9E=98=ED=95=98=EB=84=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Presentation/Bundle/Views/Shared/BundleSearchBar.swift | 2 +- .../Bundle/Views/Shared/KeyringEmptyStateView.swift | 2 +- .../Bundle/Views/Shared/KeyringSelectionContent.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleSearchBar.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleSearchBar.swift index 3a7aba297..9ce7e73c9 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleSearchBar.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/BundleSearchBar.swift @@ -2,7 +2,7 @@ // BundleSearchBar.swift // Keychy // -// Created by Claude on 2/6/26. +// Created by 길지훈 on 2/6/26. // import SwiftUI diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Shared/KeyringEmptyStateView.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/KeyringEmptyStateView.swift index 69ebf873a..4ec3d1c23 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Shared/KeyringEmptyStateView.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/KeyringEmptyStateView.swift @@ -2,7 +2,7 @@ // KeyringEmptyStateView.swift // Keychy // -// Created by Claude on 2/6/26. +// Created by 길지훈 on 2/6/26. // import SwiftUI diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Shared/KeyringSelectionContent.swift b/Keychy/Keychy/Presentation/Bundle/Views/Shared/KeyringSelectionContent.swift index 59a1de510..6c9922d98 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Shared/KeyringSelectionContent.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Shared/KeyringSelectionContent.swift @@ -2,7 +2,7 @@ // KeyringSelectionContent.swift // Keychy // -// Created by Claude on 2/6/26. +// Created by 길지훈 on 2/6/26. // import SwiftUI