From 32ddf08eb1e8d5995c402c318be34ff2bd3107b8 Mon Sep 17 00:00:00 2001 From: Jini Date: Sat, 7 Feb 2026 23:44:44 +0900 Subject: [PATCH 1/8] =?UTF-8?q?chore:=20=ED=83=AD=20=ED=86=A0=EA=B8=80?= =?UTF-8?q?=EC=9A=A9=20=EB=B3=80=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Collection/ViewModels/CollectionViewModel.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Keychy/Keychy/Presentation/Collection/ViewModels/CollectionViewModel.swift b/Keychy/Keychy/Presentation/Collection/ViewModels/CollectionViewModel.swift index bd04e4de4..4b45c0753 100644 --- a/Keychy/Keychy/Presentation/Collection/ViewModels/CollectionViewModel.swift +++ b/Keychy/Keychy/Presentation/Collection/ViewModels/CollectionViewModel.swift @@ -26,6 +26,17 @@ class CollectionViewModel { var selectedKeyrings: [Keyring] = [] var hasNetworkError: Bool = false + // MARK: - 탭 토글 (true = 키링, false = 뭉치) + var collectionToggle: Bool = true { + didSet { + // 탭 전환 시 초기화 + if !collectionToggle { + // 뭉치 탭으로 전환 시 + selectedSort = "최신순" + } + } + } + // Firestore 문서 ID 매핑: 로컬 Keyring(UUID) -> Firestore 문서 ID(String) var keyringDocumentIdByLocalId: [UUID: String] = [:] From 17dd5bf01e6d9b3999b4e4c398fd6648f16dc27a Mon Sep 17 00:00:00 2001 From: Jini Date: Sat, 7 Feb 2026 23:45:27 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20=ED=83=AD=20=ED=86=A0=EA=B8=80=20?= =?UTF-8?q?=EC=8B=9C=20UI=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Views/Main/CollectionView+Handlers.swift | 12 + .../Main/CollectionView+NormalMode.swift | 313 +++++++++++++----- .../Views/Main/CollectionView.swift | 3 + .../Tab/Views/CollectionTab.swift | 2 +- 4 files changed, 248 insertions(+), 82 deletions(-) diff --git a/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView+Handlers.swift b/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView+Handlers.swift index 3e2787250..ae713b078 100644 --- a/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView+Handlers.swift +++ b/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView+Handlers.swift @@ -21,6 +21,18 @@ extension CollectionView { } } + // 뭉치 데이터 로드 + func fetchBundleData() { + let uid = UserManager.shared.userUID + guard !uid.isEmpty else { return } + + bundleViewModel.fetchAllBundles(uid: uid) { success in + if !success { + print("뭉치 로드 실패") + } + } + } + // 키링 로드 func fetchUserKeyrings(uid: String) { collectionViewModel.fetchUserKeyrings(uid: uid) { success in diff --git a/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView+NormalMode.swift b/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView+NormalMode.swift index 5d4d9b7af..ef3819570 100644 --- a/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView+NormalMode.swift +++ b/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView+NormalMode.swift @@ -10,77 +10,123 @@ import SwiftUI // MARK: - Normal Mode View extension CollectionView { /// 오버레이 헤더 높이 (headerSection + tagSection + collectionHeader) - /// - headerSection: 60(top padding) + ~40(buttons) + 2(padding) ≈ 102pt + /// - headerSection: 60(top padding) + ~40(buttons) + 12(padding) ≈ 112pt /// - tagSection: 4(Spacing.xs) + 35(TabBar) ≈ 39pt /// - collectionHeader: ~35pt (sortButton + spacing) - /// - 총합: ~185pt - private var overlayHeaderHeight: CGFloat { 185 } + /// - 총합: ~195pt + private var overlayHeaderHeight: CGFloat { + collectionViewModel.collectionToggle ? 195 : 165 + } // MARK: - Normal Mode View var normalModeView: some View { Group { if collectionViewModel.hasNetworkError { // 네트워크 에러: 오버레이 형태 - ZStack(alignment: .top) { - Color.white - .ignoresSafeArea() - - NoInternetView(topPadding: getSafeAreaTop() + 90, onRetry: { - Task { - guard let uid = UserDefaults.standard.string(forKey: "userUID") else { - print("UID를 찾을 수 없습니다") - return - } - await collectionViewModel.retryFetchData(userId: uid) - } - }) - .ignoresSafeArea() - - VStack { - VStack { - headerSection - .padding(.horizontal, Spacing.margin) - .padding(.top, 2) - - tagSection - .padding(.horizontal, Spacing.xs) - } - .background(Color.white) + networkErrorView + } else { + // 정상 상태: ZStack 오버레이 형태 (iOS 빌트인 탭 스크롤 지원) + if collectionViewModel.collectionToggle { + keyringTabView + } else { + bundleTabView + } + } + } + } + + private var networkErrorView: some View { + ZStack(alignment: .top) { + Color.white + .ignoresSafeArea() - Spacer() + NoInternetView(topPadding: getSafeAreaTop() + 90, onRetry: { + Task { + guard let uid = UserDefaults.standard.string(forKey: "userUID") else { + print("UID를 찾을 수 없습니다") + return } + await collectionViewModel.retryFetchData(userId: uid) } - } else { - // 정상 상태: ZStack 오버레이 형태 (iOS 빌트인 탭 스크롤 지원) - ZStack(alignment: .top) { - // 전체 화면 ScrollView (pullToRefresh가 생성) - normalCollectionSection + }) + .ignoresSafeArea() - // 고정 오버레이 헤더 - VStack(spacing: 0) { - headerSection - .padding(.horizontal, Spacing.margin) - .padding(.top, 2) + VStack { + VStack { + headerSection + .padding(.horizontal, Spacing.margin) + .padding(.top, 2) + if collectionViewModel.collectionToggle { tagSection .padding(.horizontal, Spacing.xs) - - collectionHeader - .padding(.horizontal, Spacing.padding) - .padding(.top, 10) - .padding(.bottom, 12) } - .background(Color.white) } - .contentShape(Rectangle()) - .onTapGesture { - if showSearchBar && !isSearching { - isSearchFieldFocused = false + .background(Color.white) - withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { - showSearchBar = false - } - } + Spacer() + } + } + } + + private var keyringTabView: some View { + ZStack(alignment: .top) { + // 전체 화면 ScrollView (pullToRefresh가 생성) + normalCollectionSection + + // 고정 오버레이 헤더 + VStack(spacing: 0) { + headerSection + .padding(.horizontal, Spacing.margin) + .padding(.top, 2) + .padding(.bottom, 10) + + tagSection + .padding(.horizontal, Spacing.xs) + + collectionHeader + .padding(.horizontal, Spacing.padding) + .padding(.top, 10) + .padding(.bottom, 12) + } + .background(Color.white) + } + .contentShape(Rectangle()) + .onTapGesture { + if showSearchBar && !isSearching { + isSearchFieldFocused = false + + withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { + showSearchBar = false + } + } + } + } + + private var bundleTabView: some View { + ZStack(alignment: .top) { + bundleGridSection + + VStack(spacing: 0) { + headerSection + .padding(.horizontal, Spacing.margin) + .padding(.top, 2) + .padding(.bottom, 10) + + collectionHeader + .padding(.horizontal, Spacing.padding) + .padding(.top, 10) + .padding(.bottom, 16) + } + .background(Color.white) + } + .contentShape(Rectangle()) + .onTapGesture { + if showSearchBar && !isSearching { + isSearchFieldFocused = false + + withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { + showSearchBar = false } } } @@ -88,8 +134,30 @@ extension CollectionView { private var headerSection: some View { HStack(spacing: 0) { - Text("보관함") - .typography(.nanum32EB) + // 탭 토글 버튼 + HStack(spacing: 10) { + Button { + withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { + collectionViewModel.collectionToggle = true + } + } label: { + Text("키링") + .typography(.nanum24EB) + .foregroundColor(collectionViewModel.collectionToggle ? .black100 : .gray100) + } + .buttonStyle(.plain) + + Button { + withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { + collectionViewModel.collectionToggle = false + } + } label: { + Text("뭉치") + .typography(.nanum24EB) + .foregroundColor(collectionViewModel.collectionToggle ? .gray100 : .black100) + } + .buttonStyle(.plain) + } Spacer() @@ -111,16 +179,6 @@ extension CollectionView { } .padding(.trailing, 10) #endif - -// CircleGlassButton(imageName: "Widget", -// action: { -// isSearchFieldFocused = false -// showSearchBar = false -// -// router.push(.widgetSettingView) -// } -// ) -// .padding(.trailing, 10) CircleGlassButton(imageName: "BundleIcon", action: { @@ -196,32 +254,125 @@ extension CollectionView { } ) } + + // MARK: - 뭉치 그리드 섹션 + private var bundleGridSection: some View { + VStack(spacing: 0) { + Spacer() + .frame(height: overlayHeaderHeight) + if bundleViewModel.sortedBundles.isEmpty { + bundleEmptyView + } else { + bundleGrid + .padding(.horizontal, Spacing.xs) + .padding(.top, 20) + } + } + .pullToRefresh(topPadding: overlayHeaderHeight) { + try? await Task.sleep(for: .seconds(1)) + fetchBundleData() + } + .simultaneousGesture( + DragGesture().onChanged { _ in + if showSearchBar { + isSearchFieldFocused = false + + if !isSearching { + withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { + showSearchBar = false + } + } + } + } + ) + } + + private var bundleGrid: some View { + LazyVGrid(columns: columns, spacing: 11) { + ForEach(bundleViewModel.sortedBundles, id: \.documentId) { bundle in + Button { + guard NetworkManager.shared.isConnected else { + ToastManager.shared.show() + return + } + + bundleViewModel.selectedBundle = bundle + bundleViewModel.selectedBackground = bundleViewModel.resolveBackground(from: bundle.selectedBackground) + bundleViewModel.selectedCarabiner = bundleViewModel.resolveCarabiner(from: bundle.selectedCarabiner) + + router.push(.bundleDetailView) + } label: { + BundleGridItem(bundle: bundle) + } + .buttonStyle(PlainButtonStyle()) + } + } + .padding(.bottom, 90) + } + + private var bundleEmptyView: some View { + VStack { + Spacer() + .frame(height: 180) + + Image(.emptyViewIcon) + .resizable() + .frame(width: 124, height: 111) + + Text("공방에서 뭉치를 만들어봐요") + .typography(.suit15R) + .padding(.top, 15) + + Spacer() + } + .padding(.top, 10) + .scrollIndicators(.hidden) + } + var collectionHeader: some View { HStack(spacing: 0) { sortButton Spacer() - // 보유한 키링 개수 - Text("\(collectionViewModel.keyring.count) / \(collectionViewModel.maxKeyringCount)") - .typography(.suit14SB18) - .foregroundColor(.black100) - .padding(.trailing, 8) + if collectionViewModel.collectionToggle { + // 보유한 키링 개수 + Text("\(collectionViewModel.keyring.count) / \(collectionViewModel.maxKeyringCount)") + .typography(.suit14SB18) + .foregroundColor(.black100) + .padding(.trailing, 8) - Button(action: { - withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { - isSearchFieldFocused = false - showSearchBar = false - - showInvenExpandAlert = true + Button(action: { + withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { + isSearchFieldFocused = false + showSearchBar = false + + showInvenExpandAlert = true + } + }) { + Image(.invenPlus) + .resizable() + .scaledToFit() + .frame(width: 21, height: 21) } - }) { - Image(.invenPlus) - .resizable() - .scaledToFit() - .frame(width: 21, height: 21) + } else { + // 뭉치 탭: 뭉치 만들기 버튼 + Button(action: { + router.push(.bundleCreateView) + }) { + HStack(spacing: 8) { + Image(.makingIcon) + + Text("뭉치 만들기") + .typography(.suit17B) + .foregroundColor(.black100) + } + } + .frame(width: 129, height: 37) + .glassEffect(.regular.interactive(), in: .capsule) } + } } diff --git a/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView.swift b/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView.swift index 9e62d4722..2b678c264 100644 --- a/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView.swift +++ b/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView.swift @@ -11,6 +11,7 @@ import SpriteKit struct CollectionView: View { @Bindable var router: NavigationRouter @State var collectionViewModel: CollectionViewModel + @State var bundleViewModel: BundleViewModel @Binding var shouldRefresh: Bool @State var userManager = UserManager.shared @@ -113,6 +114,8 @@ struct CollectionView: View { TabBarManager.show() } fetchUserData() + fetchBundleData() + setupNotifications() // 백그라운드에서 캐시 없는 키링들 사전 캡처 diff --git a/Keychy/Keychy/Presentation/Tab/Views/CollectionTab.swift b/Keychy/Keychy/Presentation/Tab/Views/CollectionTab.swift index 1a6f3969b..20d78f716 100644 --- a/Keychy/Keychy/Presentation/Tab/Views/CollectionTab.swift +++ b/Keychy/Keychy/Presentation/Tab/Views/CollectionTab.swift @@ -15,7 +15,7 @@ struct CollectionTab: View { var body: some View { NavigationStack(path: $router.path) { - CollectionView(router: router, collectionViewModel: collectionViewModel, shouldRefresh: $shouldRefresh) + CollectionView(router: router, collectionViewModel: collectionViewModel, bundleViewModel: bundleViewModel, shouldRefresh: $shouldRefresh) .navigationDestination(for: CollectionRoute.self) { route in switch route { From 1ca2ad3dcc3b3bba1ca05c5dc16e5f66a402fee7 Mon Sep 17 00:00:00 2001 From: Jini Date: Sat, 7 Feb 2026 23:47:06 +0900 Subject: [PATCH 3/8] =?UTF-8?q?style:=20=EB=AD=89=EC=B9=98=ED=95=A8=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=20=EC=9E=84=EC=8B=9C=20=EC=A3=BC=EC=84=9D?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Views/Main/CollectionView+NormalMode.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView+NormalMode.swift b/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView+NormalMode.swift index ef3819570..c50d37e93 100644 --- a/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView+NormalMode.swift +++ b/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView+NormalMode.swift @@ -180,15 +180,15 @@ extension CollectionView { .padding(.trailing, 10) #endif - CircleGlassButton(imageName: "BundleIcon", - action: { - isSearchFieldFocused = false - showSearchBar = false - - router.push(.bundleInventoryView) - } - ) - .padding(.trailing, 10) +// CircleGlassButton(imageName: "BundleIcon", +// action: { +// isSearchFieldFocused = false +// showSearchBar = false +// +// router.push(.bundleInventoryView) +// } +// ) +// .padding(.trailing, 10) CircleGlassButton( imageName: "Search", From 4c519a850d9541fe9fbeba43d56179c4c85dca3a Mon Sep 17 00:00:00 2001 From: Jini Date: Sun, 8 Feb 2026 00:44:00 +0900 Subject: [PATCH 4/8] =?UTF-8?q?style:=20=ED=8E=98=EC=8A=A4=ED=8B=B0?= =?UTF-8?q?=EB=B2=8C=20=EC=B6=9C=ED=92=88=20=EC=83=81=ED=83=9C=20=EC=98=A4?= =?UTF-8?q?=EB=B2=84=EB=A0=88=EC=9D=B4=20=EC=8A=A4=ED=83=80=EC=9D=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DesignSystem/Typography/Typography.swift | 1 + .../Views/Main/CollectionCellView.swift | 53 +++++++++++++++---- 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/Keychy/Keychy/Core/DesignSystem/Typography/Typography.swift b/Keychy/Keychy/Core/DesignSystem/Typography/Typography.swift index 001ae59c3..56cc57f4d 100644 --- a/Keychy/Keychy/Core/DesignSystem/Typography/Typography.swift +++ b/Keychy/Keychy/Core/DesignSystem/Typography/Typography.swift @@ -54,6 +54,7 @@ struct Typography { static let suit14R18 = Typography(font: .custom(.suitRegular, size: 14), lineSpacing: 4) /// 13 + static let suit13B = Typography(font: .custom(.suitBold, size: 13), lineSpacing: 0) static let suit13SB = Typography(font: .custom(.suitSemiBold, size: 13), lineSpacing: 0) static let suit13M = Typography(font: .custom(.suitMedium, size: 13), lineSpacing: 0) diff --git a/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionCellView.swift b/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionCellView.swift index c5c030ba9..7d6690623 100644 --- a/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionCellView.swift +++ b/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionCellView.swift @@ -28,8 +28,8 @@ struct CollectionCellView: View { } // 비활성 상태 오버레이 (포장중, 출품중) - if let info = keyring.status.overlayInfo { - statusOverlay(info: info) + if let status = keyring.status.overlayInfo { + statusOverlay(status: keyring.status) } } .onAppear { @@ -146,13 +146,19 @@ struct CollectionCellView: View { } // MARK: - 상태 오버레이 - private func statusOverlay(info: String) -> some View { - RoundedRectangle(cornerRadius: 10) - .fill(.black50) - .overlay { - VStack { - Text(info) - .typography(.suit13M) + private func statusOverlay(status: KeyringStatus) -> some View { + ZStack { + // 어두운 배경 + RoundedRectangle(cornerRadius: 10) + .fill(.black20) + + // 상태별 UI + VStack { + switch status { + case .packaged: + // 포장중: 텍스트만 + Text(status.overlayInfo ?? "") + .typography(.suit13B) .foregroundColor(.white100) .padding(.vertical, 4) .frame(maxWidth: .infinity) @@ -162,10 +168,35 @@ struct CollectionCellView: View { .frame(height: 26) ) - Spacer() + case .published: + // 출품중: 그라데이션 카드 디자인 + Text(status.overlayInfo ?? "") + .typography(.suit13B) + .foregroundColor(.main500) + .padding(.vertical, 4) + .frame(maxWidth: .infinity) + .background( + RoundedRectangle(cornerRadius: 20) + .fill(LinearGradient( + colors: [.gradient3, .gradient4], + startPoint: .leading, + endPoint: .trailing)) + .frame(height: 26) + ) + .overlay( + RoundedRectangle(cornerRadius: 20) + .stroke(.main50, lineWidth:1) + ) + + + default: + EmptyView() } - .padding(5) + + Spacer() } + .padding(10) + } } // MARK: - 위젯 메타데이터 동기화 From 6d3b9289a79443a7ff7273f8d8390720cdd2f5b0 Mon Sep 17 00:00:00 2001 From: Jini Date: Sun, 8 Feb 2026 02:13:59 +0900 Subject: [PATCH 5/8] =?UTF-8?q?style:=20new=20=ED=91=9C=EC=8B=9C=20?= =?UTF-8?q?=EB=94=94=EC=9E=90=EC=9D=B8=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DesignSystem/Typography/Typography.swift | 1 + .../Views/Main/CollectionCellView.swift | 59 +++++++++++++++++++ .../Views/Main/CollectionView.swift | 27 +++++++-- 3 files changed, 81 insertions(+), 6 deletions(-) diff --git a/Keychy/Keychy/Core/DesignSystem/Typography/Typography.swift b/Keychy/Keychy/Core/DesignSystem/Typography/Typography.swift index 56cc57f4d..d84457d9b 100644 --- a/Keychy/Keychy/Core/DesignSystem/Typography/Typography.swift +++ b/Keychy/Keychy/Core/DesignSystem/Typography/Typography.swift @@ -66,6 +66,7 @@ struct Typography { /// 10 static let suit10R = Typography(font: .custom(.suitRegular, size: 10), lineSpacing: 0) static let suit10SB = Typography(font: .custom(.suitSemiBold, size: 10), lineSpacing: 0) + static let suit10H = Typography(font: .custom(.suitHeavy, size: 10), lineSpacing: 0) /// 9 static let suit9B = Typography(font: .custom(.suitBold, size: 9), lineSpacing: 0) diff --git a/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionCellView.swift b/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionCellView.swift index 7d6690623..de84b1298 100644 --- a/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionCellView.swift +++ b/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionCellView.swift @@ -13,6 +13,11 @@ struct CollectionCellView: View { @State private var isLoading: Bool = true @State private var cachedImage: UIImage? @State private var scene: KeyringCellScene? + @State private var rotation: CGFloat = 0.0 + + // 카드 스타일 + private let radius: CGFloat = 10 + private let lineWidth: CGFloat = 2 var body: some View { ZStack { @@ -26,14 +31,27 @@ struct CollectionCellView: View { LoadingAlert(type: .short40, message: nil) } } + + // NEW 키링 그라데이션 보더 + if keyring.isNew { + newKeyringBorder + } // 비활성 상태 오버레이 (포장중, 출품중) if let status = keyring.status.overlayInfo { statusOverlay(status: keyring.status) } } + .cornerRadius(radius) .onAppear { loadContent() + + // NEW 키링의 회전 애니메이션 시작 + if keyring.isNew { + withAnimation(.linear(duration: 4).repeatForever(autoreverses: false)) { + rotation = 360 + } + } } .onDisappear { if let keyringID = keyring.documentId { @@ -65,6 +83,39 @@ struct CollectionCellView: View { } } + // MARK: - NEW 키링 그라데이션 보더 + private var newKeyringBorder: some View { + ZStack { + // 기본 보더 + RoundedRectangle(cornerRadius: radius, style: .continuous) + .strokeBorder(NewIndicatorColor.main, lineWidth: lineWidth) + .overlay { + RoundedRectangle(cornerRadius: radius, style: .continuous) + .stroke(Color.black.opacity(0.25), lineWidth: lineWidth + 2) + .blur(radius: 1) + .mask( + RoundedRectangle(cornerRadius: radius, style: .continuous) + ) + } + + // 회전하는 그라데이션 보더 + AngularGradient( + gradient: Gradient(colors: [ + NewIndicatorColor.main, + NewIndicatorColor.light, + NewIndicatorColor.sub, + NewIndicatorColor.main + ]), + center: .center, + angle: .degrees(rotation) + ) + .mask { + RoundedRectangle(cornerRadius: radius, style: .continuous) + .strokeBorder(lineWidth: lineWidth) + } + } + } + // 컨텐츠 로딩 private func loadContent() { guard let keyringID = keyring.documentId else { @@ -359,3 +410,11 @@ struct CollectionCellView: View { } } } + +// MARK: - NewColor 정의 +enum NewIndicatorColor { + // Main + static let main = Color(hex: "#A72CFF") + static let light = Color(hex: "#C2FEFF") + static let sub = Color(hex: "#A863FF") +} diff --git a/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView.swift b/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView.swift index 2b678c264..d1f87745e 100644 --- a/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView.swift +++ b/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView.swift @@ -220,13 +220,28 @@ struct CollectionView: View { .frame(width: twoGridCellWidth, height: twoGridCellHeight) .cornerRadius(10) - HStack(spacing: 3) { + HStack(spacing: 4) { if keyring.isNew { - Circle() - .fill(.pink) - .frame(width: 9, height: 9) - .padding(.vertical, 5) - .padding(.horizontal, 1.5) + // NEW 뱃지 + Text("NEW!") + .typography(.suit10H) + .foregroundColor(.main500) + .padding(.horizontal, 6) + .padding(.vertical, 4) + .background( + RoundedRectangle(cornerRadius: 30) + .fill( + LinearGradient( + colors: [ + NewIndicatorColor.light.opacity(0.6), + NewIndicatorColor.main.opacity(0.25), + ], + startPoint: .leading, + endPoint: .trailing + ) + ) + + ) } // 검색 모드일 때 하이라이트 적용 From f26908b417202f4fe008a9dc8445006636404e6d Mon Sep 17 00:00:00 2001 From: Jini Date: Sun, 8 Feb 2026 02:25:21 +0900 Subject: [PATCH 6/8] =?UTF-8?q?fix:=20alert=20=EB=B7=B0=20=EC=A2=85?= =?UTF-8?q?=EB=A3=8C=20=EC=8B=9C=20=EB=92=B7=20=EB=B7=B0=EC=99=80=20?= =?UTF-8?q?=EA=B2=B9=EC=B9=A8=20=ED=98=84=EC=83=81=20=ED=94=BD=EC=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Collection/Views/Main/CollectionView+Alerts.swift | 4 ++++ .../Presentation/Collection/Views/Main/CollectionView.swift | 2 ++ 2 files changed, 6 insertions(+) diff --git a/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView+Alerts.swift b/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView+Alerts.swift index d981e49e9..d66f5e6ac 100644 --- a/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView+Alerts.swift +++ b/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView+Alerts.swift @@ -13,24 +13,28 @@ extension CollectionView { var alertOverlays: some View { if let menuCategory = showingMenuFor { categoryMenuView(menuCategory: menuCategory) + .zIndex(300) } if showRenameAlert { renameAlertOverlay .frame(maxWidth: .infinity, maxHeight: .infinity) .ignoresSafeArea() + .zIndex(301) } if showDeleteAlert || showDeleteCompleteAlert { deleteAlertOverlay .frame(maxWidth: .infinity, maxHeight: .infinity) .ignoresSafeArea() + .zIndex(302) } if showInvenExpandAlert || showPurchaseSuccessAlert || showPurchaseFailAlert { invenAlertOverlay .frame(maxWidth: .infinity, maxHeight: .infinity) .ignoresSafeArea() + .zIndex(303) } } diff --git a/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView.swift b/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView.swift index d1f87745e..65d8b5366 100644 --- a/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView.swift +++ b/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView.swift @@ -67,6 +67,7 @@ struct CollectionView: View { var body: some View { ZStack { mainContent + .zIndex(0) // 검색바 if showSearchBar { @@ -95,6 +96,7 @@ struct CollectionView: View { } alertOverlays + .zIndex(300) } .toolbar(isSearching ? .hidden : .visible, for: .tabBar) From 50249c48dbccb8010c7ec54f9b043cc160dd3481 Mon Sep 17 00:00:00 2001 From: Jini Date: Sun, 8 Feb 2026 02:45:26 +0900 Subject: [PATCH 7/8] =?UTF-8?q?style:=20=EB=AD=89=EC=B9=98=20=EC=97=A0?= =?UTF-8?q?=ED=8B=B0=EB=B7=B0=20=EB=9D=BC=EC=9D=B4=ED=8C=85=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Collection/Views/Main/CollectionView+NormalMode.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView+NormalMode.swift b/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView+NormalMode.swift index c50d37e93..7d68c8cea 100644 --- a/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView+NormalMode.swift +++ b/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView+NormalMode.swift @@ -320,7 +320,7 @@ extension CollectionView { .resizable() .frame(width: 124, height: 111) - Text("공방에서 뭉치를 만들어봐요") + Text("새로운 뭉치를 만들어주세요") .typography(.suit15R) .padding(.top, 15) From 7e34f619883d938ac7a5c07b107a043ba36b06c5 Mon Sep 17 00:00:00 2001 From: Jini Date: Sun, 8 Feb 2026 02:54:41 +0900 Subject: [PATCH 8/8] =?UTF-8?q?style:=20=ED=82=A4=EB=A7=81=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=88=98=EC=A0=95=20=EC=99=84=EB=A3=8C=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EB=94=94=EC=9E=90=EC=9D=B8=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Views/Detail/KeyringEditView.swift | 64 ++++++++----------- 1 file changed, 26 insertions(+), 38 deletions(-) diff --git a/Keychy/Keychy/Presentation/Collection/Views/Detail/KeyringEditView.swift b/Keychy/Keychy/Presentation/Collection/Views/Detail/KeyringEditView.swift index 528cabf36..adff7a9f1 100644 --- a/Keychy/Keychy/Presentation/Collection/Views/Detail/KeyringEditView.swift +++ b/Keychy/Keychy/Presentation/Collection/Views/Detail/KeyringEditView.swift @@ -143,48 +143,36 @@ extension KeyringEditView { private var completeToolbarItem: some ToolbarContent { ToolbarItem(placement: .topBarTrailing) { - if isCompleteEnabled { - Button(role: .confirm, action: { - // 네트워크 체크 - guard NetworkManager.shared.isConnected else { - ToastManager.shared.show() - return - } - - viewModel.updateKeyring( - keyring: keyring, - name: editedName, - memo: editedMemo, - tags: editedTags - ) { success in - if success { - router.reset() - TabBarManager.show() - } - } - }) { - Image(.recCheck) - .resizable() - .renderingMode(.template) - .foregroundStyle(.white100) - .frame(width: 32, height: 32) - + Button { + guard isCompleteEnabled else { return } + + // 네트워크 체크 + guard NetworkManager.shared.isConnected else { + ToastManager.shared.show() + return } - } - else { - Button(action: { - // disabled - }) { - Image(.recCheck) - .resizable() - .renderingMode(.template) - .foregroundStyle(.gray300) - .frame(width: 32, height: 32) + viewModel.updateKeyring( + keyring: keyring, + name: editedName, + memo: editedMemo, + tags: editedTags + ) { success in + if success { + router.reset() + TabBarManager.show() + + //TODO: 수정완료 후 pop (데이터 새로고침 로직 추가 예정) + //router.pop() + } } - .disabled(true) + } label: { + Text("완료") + .typography(.suit17B) + .foregroundStyle(isCompleteEnabled ? .main500 : .gray200) } - + .frame(width: 56, height: 44) + .disabled(!isCompleteEnabled) } } }