From 8dd7d52a5f880220deafd9823f53b2fe98d5e1ce Mon Sep 17 00:00:00 2001 From: giljihun Date: Wed, 11 Feb 2026 00:58:58 +0900 Subject: [PATCH 1/7] =?UTF-8?q?style:=20=EC=83=81=EB=8B=A8=20=ED=95=98?= =?UTF-8?q?=EC=96=80=20=EA=B7=B8=EB=9D=BC=EB=8D=B0=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Views/Main/WorkshopView+TopSection.swift | 20 ------------------- .../Workshop/Views/Main/WorkshopView.swift | 4 ---- 2 files changed, 24 deletions(-) diff --git a/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView+TopSection.swift b/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView+TopSection.swift index 074941d4..8819ba83 100644 --- a/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView+TopSection.swift +++ b/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView+TopSection.swift @@ -86,26 +86,6 @@ extension WorkshopView { ) } - /// 상단 그라데이션 오버레이 - var topGradientOverlay: some View { - VStack { - LinearGradient( - colors: [ - Color.white.opacity(0.8), - Color.white.opacity(0.6), - Color.white.opacity(0.3), - Color.clear - ], - startPoint: .top, - endPoint: .bottom - ) - .frame(height: WorkshopLayout.gradientHeight) - .ignoresSafeArea(edges: .top) - Spacer() - } - .allowsHitTesting(false) - } - /// 최근 사용 템플릿 섹션 var recentTemplateSection: some View { WorkshopRecentTemplate( diff --git a/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView.swift b/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView.swift index f0bf60cf..6fad2891 100644 --- a/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView.swift +++ b/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView.swift @@ -15,7 +15,6 @@ enum WorkshopLayout { static let recentTemplateTopSpacing: CGFloat = 116 static let bundleBannerTopSpacing: CGFloat = 20 static let mainContentTopSpacing: CGFloat = 43 - static let gradientHeight: CGFloat = 100 static let stickyHeaderMinOffset: CGFloat = 120 static let stickyHeaderMaxOffset: CGFloat = 730 static let stickyHeaderOffsetAdjust: CGFloat = 20 @@ -62,9 +61,6 @@ struct WorkshopView: View { // 스티키 헤더 (카테고리 탭 + 필터) stickyHeaderSection - - // 상단 그라데이션 블러 오버레이 - topGradientOverlay } .background { Image(viewModel.workshopToggle ? .workshopKeyringBGB : .workshopBundleBGB) From 614ded6c5df54c9a6b7860e478744370b942e470 Mon Sep 17 00:00:00 2001 From: giljihun Date: Wed, 11 Feb 2026 01:54:55 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20=ED=83=AD=EB=B0=94=20=EC=8A=A4?= =?UTF-8?q?=EC=99=80=EC=9D=B4=ED=94=84=20=EB=B0=B1=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EB=93=9C=EC=9D=B8=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 크림앱처럼 스와이프 백 시 탭바가 서서히 나타나는 기능 구현 [구조 변경] - TabBarManager 3개 파일로 분리 - TabBarManager.swift: show/hide/setAlpha API - TabBarSwipeObserver.swift: 제스처 진행도 감지 - UIViewController+Find.swift: VC 탐색 유틸 [동작 방식] - hide() 호출 시 스와이프 제스처 옵저버 자동 연결 - 스와이프 진행도(0~1) → 탭바 alpha 실시간 반영 - 완료 판정: 50% 이상 or 속도 500pt/s 이상 [버그 수정] - WorkshopView onAppear에서 show() 제거 → 스와이프 중 탭바 즉시 등장 버그 해결 --- Keychy/Keychy.xcodeproj/project.pbxproj | 26 ++++-- .../Navigation/TabBar/TabBarManager.swift | 72 +++++++++++++++ .../TabBar/TabBarSwipeObserver.swift | 90 +++++++++++++++++++ .../TabBar/UIViewController+Find.swift | 50 +++++++++++ .../Core/Navigation/TabBarManager.swift | 69 -------------- .../Workshop/Views/Main/WorkshopView.swift | 3 - 6 files changed, 233 insertions(+), 77 deletions(-) create mode 100644 Keychy/Keychy/Core/Navigation/TabBar/TabBarManager.swift create mode 100644 Keychy/Keychy/Core/Navigation/TabBar/TabBarSwipeObserver.swift create mode 100644 Keychy/Keychy/Core/Navigation/TabBar/UIViewController+Find.swift delete mode 100644 Keychy/Keychy/Core/Navigation/TabBarManager.swift diff --git a/Keychy/Keychy.xcodeproj/project.pbxproj b/Keychy/Keychy.xcodeproj/project.pbxproj index 7135d896..90cd5734 100644 --- a/Keychy/Keychy.xcodeproj/project.pbxproj +++ b/Keychy/Keychy.xcodeproj/project.pbxproj @@ -86,7 +86,6 @@ 38F832CD2EC90DEF00D3A248 /* WidgetOnboardingStepView+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F832CC2EC90DEF00D3A248 /* WidgetOnboardingStepView+Helpers.swift */; }; 38F832CF2EC914C900D3A248 /* InvenExpandPopup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F832CE2EC914C900D3A248 /* InvenExpandPopup.swift */; }; 40WF8CXMLHGD9B5S521VX89Y /* BundleViewModel+Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = DTQCH5OWZZJ8N03KVZERHJKR /* BundleViewModel+Cache.swift */; }; - 4C004F912F164D5500D9063E /* TabBarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C004F902F164D5500D9063E /* TabBarManager.swift */; }; 4C004FA12F177C4600D9063E /* BundleVideoGenerator+Rendering.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C004F982F177C4600D9063E /* BundleVideoGenerator+Rendering.swift */; }; 4C004FA22F177C4600D9063E /* KeyringVideoGenerator+Setup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C004F9E2F177C4600D9063E /* KeyringVideoGenerator+Setup.swift */; }; 4C004FA32F177C4600D9063E /* BundleVideoGenerator+Metal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C004F972F177C4600D9063E /* BundleVideoGenerator+Metal.swift */; }; @@ -130,6 +129,9 @@ 4C2526232F3B290A003CC5AD /* KeyringScene+Setup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2526172F3B290A003CC5AD /* KeyringScene+Setup.swift */; }; 4C2526242F3B290A003CC5AD /* KeyringCellScene+Capture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C25260F2F3B290A003CC5AD /* KeyringCellScene+Capture.swift */; }; 4C2526262F3B2BBE003CC5AD /* KeyringScale.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2526252F3B2BBE003CC5AD /* KeyringScale.swift */; }; + 4C25262B2F3B97D6003CC5AD /* UIViewController+Find.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2526292F3B97D6003CC5AD /* UIViewController+Find.swift */; }; + 4C25262C2F3B97D6003CC5AD /* TabBarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2526272F3B97D6003CC5AD /* TabBarManager.swift */; }; + 4C25262D2F3B97D6003CC5AD /* TabBarSwipeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2526282F3B97D6003CC5AD /* TabBarSwipeObserver.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 */; }; @@ -549,7 +551,6 @@ 38F832CC2EC90DEF00D3A248 /* WidgetOnboardingStepView+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WidgetOnboardingStepView+Helpers.swift"; sourceTree = ""; }; 38F832CE2EC914C900D3A248 /* InvenExpandPopup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvenExpandPopup.swift; sourceTree = ""; }; 40QZ1H4Y8EH2YZZUOT7WN7MX /* BundleViewModel+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleViewModel+Helpers.swift"; sourceTree = ""; }; - 4C004F902F164D5500D9063E /* TabBarManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarManager.swift; sourceTree = ""; }; 4C004F962F177C4600D9063E /* BundleVideoGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleVideoGenerator.swift; sourceTree = ""; }; 4C004F972F177C4600D9063E /* BundleVideoGenerator+Metal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleVideoGenerator+Metal.swift"; sourceTree = ""; }; 4C004F982F177C4600D9063E /* BundleVideoGenerator+Rendering.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleVideoGenerator+Rendering.swift"; sourceTree = ""; }; @@ -590,6 +591,9 @@ 4C2526182F3B290A003CC5AD /* KeyringScene+Swipe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KeyringScene+Swipe.swift"; sourceTree = ""; }; 4C2526192F3B290A003CC5AD /* KeyringScene+Touch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KeyringScene+Touch.swift"; sourceTree = ""; }; 4C2526252F3B2BBE003CC5AD /* KeyringScale.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyringScale.swift; sourceTree = ""; }; + 4C2526272F3B97D6003CC5AD /* TabBarManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarManager.swift; sourceTree = ""; }; + 4C2526282F3B97D6003CC5AD /* TabBarSwipeObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarSwipeObserver.swift; sourceTree = ""; }; + 4C2526292F3B97D6003CC5AD /* UIViewController+Find.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Find.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 = ""; }; @@ -1212,6 +1216,16 @@ path = KeyringScene; sourceTree = ""; }; + 4C25262A2F3B97D6003CC5AD /* TabBar */ = { + isa = PBXGroup; + children = ( + 4C2526272F3B97D6003CC5AD /* TabBarManager.swift */, + 4C2526282F3B97D6003CC5AD /* TabBarSwipeObserver.swift */, + 4C2526292F3B97D6003CC5AD /* UIViewController+Find.swift */, + ); + path = TabBar; + sourceTree = ""; + }; 4C3687F52EBFA86B00C64E75 /* pretendard */ = { isa = PBXGroup; children = ( @@ -1958,9 +1972,9 @@ 4CEC620D2EAE08DA0099ECEE /* Navigation */ = { isa = PBXGroup; children = ( - 4CEC620B2EAE08DA0099ECEE /* Routes */, 4CEC620C2EAE08DA0099ECEE /* NavigationRouter.swift */, - 4C004F902F164D5500D9063E /* TabBarManager.swift */, + 4C25262A2F3B97D6003CC5AD /* TabBar */, + 4CEC620B2EAE08DA0099ECEE /* Routes */, ); path = Navigation; sourceTree = ""; @@ -2809,6 +2823,9 @@ 38173D0A2EB8AD7900E36F7E /* CategoryTabBarWithLongPress.swift in Sources */, 4CEBB1552EFACFA900CF53E2 /* HomeTab.swift in Sources */, 4CEBB1572EFACFA900CF53E2 /* CollectionTab.swift in Sources */, + 4C25262B2F3B97D6003CC5AD /* UIViewController+Find.swift in Sources */, + 4C25262C2F3B97D6003CC5AD /* TabBarManager.swift in Sources */, + 4C25262D2F3B97D6003CC5AD /* TabBarSwipeObserver.swift in Sources */, 4CEBB1592EFACFA900CF53E2 /* WorkshopTab.swift in Sources */, 38F832CD2EC90DEF00D3A248 /* WidgetOnboardingStepView+Helpers.swift in Sources */, 38A22A9D2EC27AC400B4C7C5 /* PackagedKeyringView+SaveImage.swift in Sources */, @@ -2850,7 +2867,6 @@ 4C6530502EBB2A90000F8154 /* LoadingAlert.swift in Sources */, 4C6530722EBCFD10000F8154 /* BangmarkAlert.swift in Sources */, 389080192ED3F32700D7A49F /* FestivalKeyringDetailView+Sheet.swift in Sources */, - 4C004F912F164D5500D9063E /* TabBarManager.swift in Sources */, C665DDF02EAF08D000CE4495 /* PurchaseManager.swift in Sources */, 4CEC62332EAE08DA0099ECEE /* BundleGridItem.swift in Sources */, AA69DD242F14C56F00C0A41C /* BundleViewModel+CRUD.swift in Sources */, diff --git a/Keychy/Keychy/Core/Navigation/TabBar/TabBarManager.swift b/Keychy/Keychy/Core/Navigation/TabBar/TabBarManager.swift new file mode 100644 index 00000000..feece235 --- /dev/null +++ b/Keychy/Keychy/Core/Navigation/TabBar/TabBarManager.swift @@ -0,0 +1,72 @@ +// +// TabBarManager.swift +// Keychy +// +// Created by 길지훈 on 2/11/26. +// +// 탭바 표시/숨김을 관리하는 매니저 +// - show(): 탭바 표시 (애니메이션) +// - hide(): 탭바 숨김 + 스와이프 백 제스처 연동 설정 +// - setAlpha(): 탭바 투명도 직접 설정 +// + +import UIKit + +enum TabBarManager { + + // MARK: - Tab Index + + enum Tab: Int { + case home = 0 + case workshop = 1 + case collection = 2 + case festival = 3 + } + + // MARK: - Public Methods + + /// 탭바 표시 (fade in 애니메이션) + static func show() { + animate(toAlpha: 1.0) + } + + /// 탭바 숨김 (fade out) + 스와이프 백 제스처 연동 + static func hide() { + animate(toAlpha: 0.0) + setupSwipeGestureObserver() + } + + /// 탭바 투명도 직접 설정 (애니메이션 없음) + static func setAlpha(_ alpha: CGFloat) { + tabBar?.alpha = alpha + } + + /// 특정 탭으로 전환 + static func switchTo(_ tab: Tab) { + tabBarController?.selectedIndex = tab.rawValue + } + + // MARK: - Private 변/함 + + private static var tabBarController: UITabBarController? { + UIApplication.shared.rootViewController?.findTabBarController() + } + + private static var tabBar: UITabBar? { + tabBarController?.tabBar + } + + private static func animate(toAlpha alpha: CGFloat) { + UIView.animate(withDuration: alpha > 0 ? 0.25 : 0.2) { + tabBar?.alpha = alpha + } + } + + private static func setupSwipeGestureObserver() { + guard let selectedVC = tabBarController?.selectedViewController, + let navController = selectedVC.findNavigationController(), + let gesture = navController.interactivePopGestureRecognizer else { return } + + TabBarSwipeObserver.shared.attach(to: gesture) + } +} diff --git a/Keychy/Keychy/Core/Navigation/TabBar/TabBarSwipeObserver.swift b/Keychy/Keychy/Core/Navigation/TabBar/TabBarSwipeObserver.swift new file mode 100644 index 00000000..8955bf75 --- /dev/null +++ b/Keychy/Keychy/Core/Navigation/TabBar/TabBarSwipeObserver.swift @@ -0,0 +1,90 @@ +// +// TabBarSwipeObserver.swift +// Keychy +// +// Created by 길지훈 on 2/11/26. +// +// 스와이프 백 제스처 진행도를 탭바 투명도에 연동 +// +// [동작 흐름] +// 1. TabBarManager.hide() 호출 시 attach(to:) 실행 +// 2. 스와이프 시작 → handleGesture 호출 +// 3. 스와이프 진행 → 진행도(0~1)를 탭바 alpha에 반영 +// 4. 스와이프 완료(50%↑ or 빠른 스와이프) → TabBarManager.show() +// 5. 스와이프 취소 → TabBarManager.hide() +// + +import UIKit + +final class TabBarSwipeObserver: NSObject { + + static let shared = TabBarSwipeObserver() + + // MARK: - Constants + + private enum Threshold { + static let completeProgress: CGFloat = 0.5 // 50% 이상이면 완료 + static let fastVelocity: CGFloat = 500 // 빠른 스와이프 판정 속도 + } + + // MARK: - Properties + + private weak var currentGesture: UIGestureRecognizer? + + // MARK: - Init + + private override init() { + super.init() + } + + // MARK: - Public + + /// 제스처에 옵저버 연결 + func attach(to gesture: UIGestureRecognizer) { + guard currentGesture !== gesture else { return } + + detach() + currentGesture = gesture + gesture.addTarget(self, action: #selector(handleGesture(_:))) + } + + /// 현재 제스처 연결 해제 + func detach() { + currentGesture?.removeTarget(self, action: #selector(handleGesture(_:))) + currentGesture = nil + } + + // MARK: - Private + + private func calculateProgress(from gesture: UIPanGestureRecognizer) -> CGFloat { + let translationX = gesture.translation(in: gesture.view).x + let screenWidth = gesture.view?.window?.screen.bounds.width ?? gesture.view?.bounds.width ?? 393 + return min(max(translationX / screenWidth, 0), 1) + } + + private func shouldComplete(progress: CGFloat, velocity: CGFloat) -> Bool { + progress > Threshold.completeProgress || velocity > Threshold.fastVelocity + } + + // MARK: - Gesture Handler + + @objc private func handleGesture(_ gesture: UIPanGestureRecognizer) { + let progress = calculateProgress(from: gesture) + + switch gesture.state { + case .changed: + TabBarManager.setAlpha(progress) + + case .ended, .cancelled: + let velocity = gesture.velocity(in: gesture.view).x + if shouldComplete(progress: progress, velocity: velocity) { + TabBarManager.show() + } else { + TabBarManager.hide() + } + + default: + break + } + } +} diff --git a/Keychy/Keychy/Core/Navigation/TabBar/UIViewController+Find.swift b/Keychy/Keychy/Core/Navigation/TabBar/UIViewController+Find.swift new file mode 100644 index 00000000..1be07372 --- /dev/null +++ b/Keychy/Keychy/Core/Navigation/TabBar/UIViewController+Find.swift @@ -0,0 +1,50 @@ +// +// UIViewController+Find.swift +// Keychy +// +// Created by 길지훈 on 2/11/26. +// +// UIViewController 계층에서 특정 컨트롤러 탐색 +// + +import UIKit + +extension UIViewController { + + /// 하위 계층에서 UITabBarController 탐색 + func findTabBarController() -> UITabBarController? { + if let tabBar = self as? UITabBarController { return tabBar } + + for child in children { + if let found = child.findTabBarController() { return found } + } + + return parent?.findTabBarController() + } + + /// 하위 계층에서 UINavigationController 탐색 + func findNavigationController() -> UINavigationController? { + if let nav = self as? UINavigationController { return nav } + + for child in children { + if let found = child.findNavigationController() { return found } + } + + return nil + } +} + +// MARK: - UIApplication Extension + +extension UIApplication { + + /// 현재 활성 윈도우의 rootViewController + var rootViewController: UIViewController? { + connectedScenes + .compactMap { $0 as? UIWindowScene } + .first? + .windows + .first? + .rootViewController + } +} diff --git a/Keychy/Keychy/Core/Navigation/TabBarManager.swift b/Keychy/Keychy/Core/Navigation/TabBarManager.swift deleted file mode 100644 index dfebf2e3..00000000 --- a/Keychy/Keychy/Core/Navigation/TabBarManager.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// TabBarManager.swift -// Keychy -// -// Created by 길지훈 on 1/13/26. -// - -import SwiftUI -import UIKit - -/// 탭바 표시/숨김 전역 관리 -enum TabBarManager { - /// 탭 인덱스 - enum TabIndex: Int { - case home = 0 - case workshop = 1 - case collection = 2 - case festival = 3 - } - - /// 탭바 숨기기 - static func hide() { - guard let tabBarController = findTabBarController() else { return } - tabBarController.tabBar.isHidden = true - } - - /// 탭바 보이기 - static func show() { - guard let tabBarController = findTabBarController() else { return } - UIView.animate(withDuration: 0.3) { - tabBarController.tabBar.isHidden = false - } - } - - /// 특정 탭으로 전환 - static func switchTo(_ tab: TabIndex) { - guard let tabBarController = findTabBarController() else { return } - tabBarController.selectedIndex = tab.rawValue - } - - /// TabBarController 찾기 - private static func findTabBarController() -> UITabBarController? { - guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let window = windowScene.windows.first, - let rootViewController = window.rootViewController else { - return nil - } - - return rootViewController.findTabBarController() - } -} - -// MARK: - UIViewController Extension -extension UIViewController { - /// UITabBarController 재귀 탐색 - func findTabBarController() -> UITabBarController? { - if let tabBarController = self as? UITabBarController { - return tabBarController - } - - for child in children { - if let tabBarController = child.findTabBarController() { - return tabBarController - } - } - - return parent?.findTabBarController() - } -} diff --git a/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView.swift b/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView.swift index 6fad2891..2ee2f142 100644 --- a/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView.swift +++ b/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView.swift @@ -119,9 +119,6 @@ struct WorkshopView: View { } } .withToast(position: .tabbar) - .onAppear { - TabBarManager.show() - } .sheet(isPresented: $showTemplateSelectSheet) { WorkshopTemplateSelectSheet( isPresented: $showTemplateSelectSheet, From 843320cd481c31447d6dd801cca2f969484931de Mon Sep 17 00:00:00 2001 From: giljihun Date: Wed, 11 Feb 2026 02:05:10 +0900 Subject: [PATCH 3/7] =?UTF-8?q?fix:=20=EB=AD=89=EC=B9=98=EC=99=84=EC=84=B1?= =?UTF-8?q?=EB=B7=B0=EC=97=90=EC=84=9C=20X,=20=EB=B3=B4=EA=B4=80=ED=95=A8?= =?UTF-8?q?=20=EB=B2=84=ED=8A=BC=20=EB=9D=BC=EC=9A=B0=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 --- .../Bundle/Views/Complete/BundleCompleteView.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Complete/BundleCompleteView.swift b/Keychy/Keychy/Presentation/Bundle/Views/Complete/BundleCompleteView.swift index 203bd554..2d653270 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Complete/BundleCompleteView.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Complete/BundleCompleteView.swift @@ -279,6 +279,7 @@ extension BundleCompleteView { ToolbarItem(placement: .topBarLeading) { Button { cleanupCachedVideo() + TabBarManager.switchTo(.workshop) TabBarManager.show() router.reset() } label: { @@ -314,9 +315,10 @@ extension BundleCompleteView { private func navigateToInventory() { cleanupCachedVideo() + CollectionViewModel.shouldStartWithBundleTab = true + TabBarManager.switchTo(.collection) TabBarManager.show() router.reset() - router.push(.bundleInventoryView) } } From 916265bece6f9e8dcabd2d8a3b09af0c80a6d1e3 Mon Sep 17 00:00:00 2001 From: giljihun Date: Wed, 11 Feb 2026 02:05:30 +0900 Subject: [PATCH 4/7] =?UTF-8?q?feat:=20CollectionView=EC=97=90=20=EB=AD=89?= =?UTF-8?q?=EC=B9=98=ED=83=AD=20=EC=8B=9C=EC=9E=91=20=EC=97=AC=EB=B6=80=20?= =?UTF-8?q?=ED=94=8C=EB=9E=98=EA=B7=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Collection/ViewModels/CollectionViewModel.swift | 3 +++ .../Collection/Views/Main/CollectionView.swift | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/Keychy/Keychy/Presentation/Collection/ViewModels/CollectionViewModel.swift b/Keychy/Keychy/Presentation/Collection/ViewModels/CollectionViewModel.swift index 383ea360..19968321 100644 --- a/Keychy/Keychy/Presentation/Collection/ViewModels/CollectionViewModel.swift +++ b/Keychy/Keychy/Presentation/Collection/ViewModels/CollectionViewModel.swift @@ -32,6 +32,9 @@ class CollectionViewModel { // 탭 전환해도 정렬 상태는 유지 (각 탭이 독립적으로 정렬방식 유지) } } + + /// 다른 화면에서 보관함 이동 시 뭉치 탭으로 시작할지 여부 + static var shouldStartWithBundleTab: Bool = false // Firestore 문서 ID 매핑: 로컬 Keyring(UUID) -> Firestore 문서 ID(String) var keyringDocumentIdByLocalId: [UUID: String] = [:] diff --git a/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView.swift b/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView.swift index ed7a29d0..3332fcf7 100644 --- a/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView.swift +++ b/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView.swift @@ -126,6 +126,13 @@ struct CollectionView: View { if !isSearching && !showSearchBar { TabBarManager.show() } + + // 다른 화면에서 뭉치 탭으로 이동 요청 시 + if CollectionViewModel.shouldStartWithBundleTab { + collectionViewModel.collectionToggle = false + CollectionViewModel.shouldStartWithBundleTab = false + } + fetchUserData() fetchBundleData() From 95b3cf379828a39aa7cf74ddffc490258b92d313 Mon Sep 17 00:00:00 2001 From: giljihun Date: Wed, 11 Feb 2026 02:14:42 +0900 Subject: [PATCH 5/7] =?UTF-8?q?fix:=20=ED=99=88=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=8C=80=ED=91=9C=EB=AD=89=EC=B9=98=20=EC=84=A4=EC=A0=95=20->?= =?UTF-8?q?=20=EB=B9=88=20=EB=AD=89=EC=B9=98=EB=A9=B4=20=EB=AC=B4=ED=95=9C?= =?UTF-8?q?=EB=A1=9C=EB=94=A9=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 키링 없으면 즉시 완료 콜백 ㄱㄱ 혓 --- .../Core/KeyringBundle/View/MultiKeyringSceneView.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Keychy/Keychy/Core/KeyringBundle/View/MultiKeyringSceneView.swift b/Keychy/Keychy/Core/KeyringBundle/View/MultiKeyringSceneView.swift index 0970d043..cd4447de 100644 --- a/Keychy/Keychy/Core/KeyringBundle/View/MultiKeyringSceneView.swift +++ b/Keychy/Keychy/Core/KeyringBundle/View/MultiKeyringSceneView.swift @@ -85,6 +85,11 @@ struct MultiKeyringSceneView: View { loadBackgroundImage() setupScene() } + + // 키링이 없으면 즉시 준비 완료 콜백 호출 + if keyringDataList.isEmpty { + onAllKeyringsReady?() + } } .onChange(of: backgroundImageURL) { _, _ in loadBackgroundImage() From 2554619f57290e0fd0736362e8e1f22ec0880750 Mon Sep 17 00:00:00 2001 From: giljihun Date: Wed, 11 Feb 2026 02:14:58 +0900 Subject: [PATCH 6/7] =?UTF-8?q?feat:=20=EB=AD=89=EC=B9=98=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EC=A0=80=EC=9E=A5=20-=20=ED=88=AC?= =?UTF-8?q?=EB=AA=85=20=EB=B0=B0=EA=B2=BD=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 디테일, 완성뷰 둘다 적용 --- .../Bundle/Views/Complete/BundleCompleteView+SaveImage.swift | 4 ++-- .../Bundle/Views/Detail/BundleDetailView+SaveImage.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Complete/BundleCompleteView+SaveImage.swift b/Keychy/Keychy/Presentation/Bundle/Views/Complete/BundleCompleteView+SaveImage.swift index 87799f7c..cb61c9e5 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Complete/BundleCompleteView+SaveImage.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Complete/BundleCompleteView+SaveImage.swift @@ -111,10 +111,10 @@ extension BundleCompleteView { carabinerFrontURL = nil } - // 배경 포함 캡쳐 + // 투명 배경으로 캡쳐 guard let fullImageData = await MultiKeyringCaptureScene.captureBundleImage( keyringDataList: captureKeyringDataList, - backgroundImageURL: background.backgroundImage, + backgroundImageURL: nil, carabinerBackImageURL: carabinerBackURL, carabinerFrontImageURL: carabinerFrontURL, carabinerType: carabinerType, diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Detail/BundleDetailView+SaveImage.swift b/Keychy/Keychy/Presentation/Bundle/Views/Detail/BundleDetailView+SaveImage.swift index 23af003a..f31e5115 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Detail/BundleDetailView+SaveImage.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Detail/BundleDetailView+SaveImage.swift @@ -126,10 +126,10 @@ extension BundleDetailView { carabinerFrontURL = nil } - // 1. 배경 포함 캡쳐 (앱용) + // 1. 투명 배경으로 캡쳐 (앨범 저장용) guard let fullImageData = await MultiKeyringCaptureScene.captureBundleImage( keyringDataList: keyringDataList, - backgroundImageURL: bg.backgroundImage, + backgroundImageURL: nil, carabinerBackImageURL: carabinerBackURL, carabinerFrontImageURL: carabinerFrontURL, carabinerType: carabinerType, From e48b692173c21d0829ab215ff5bfc00f9e710ba1 Mon Sep 17 00:00:00 2001 From: giljihun Date: Wed, 11 Feb 2026 02:25:53 +0900 Subject: [PATCH 7/7] =?UTF-8?q?fix:=20=ED=99=88=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EB=A6=AC=ED=94=84=EB=A0=88=EC=8B=9C=20-=20=EB=AD=89=EC=B9=98/?= =?UTF-8?q?=ED=82=A4=EB=A7=81=20=EC=88=98=EC=A0=95=20=ED=9B=84=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8D=98=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - HomeViewModel에 needsRefresh 플래그 추가 - 키링 편집 저장 시 플래그 설정 (saveBundleChanges) - 대표뭉치 변경 시 플래그 설정 (updateBundleMainStatus) - 첫 뭉치 생성(isMain) 시 플래그 설정 (createBundle) - loadMainBundle에서 플래그 확인 후 캐시 무효화 --- .../Bundle/ViewModels/BundleViewModel+CRUD.swift | 8 ++++++++ .../Bundle/ViewModels/BundleViewModel+Edit.swift | 3 +++ .../Presentation/Home/ViewModels/HomeViewModel.swift | 11 ++++++++++- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+CRUD.swift b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+CRUD.swift index da425d1e..30c6b160 100644 --- a/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+CRUD.swift +++ b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+CRUD.swift @@ -66,6 +66,11 @@ extension BundleViewModel { backgroundId: selectedBackground ) + // 메인 뭉치로 생성된 경우 홈 화면 리프레시 필요 + if isMain { + HomeViewModel.needsRefresh = true + } + // 첫 뭉치 생성 체크 (기본 1개 → 사용자가 만든 첫 뭉치는 2개일 때) let isFirstUserBundle = self.bundles.count == 2 ReviewManager.shared.checkFirstBundle(isFirstBundle: isFirstUserBundle) @@ -213,6 +218,9 @@ extension BundleViewModel { if self.selectedBundle?.documentId == bundle.documentId { self.selectedBundle?.isMain = isMain } + + // 대표 뭉치 변경 시 홈 화면 리프레시 필요 + HomeViewModel.needsRefresh = true } completion(true) } diff --git a/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Edit.swift b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Edit.swift index 2f6c0f1d..b29f68f0 100644 --- a/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Edit.swift +++ b/Keychy/Keychy/Presentation/Bundle/ViewModels/BundleViewModel+Edit.swift @@ -176,6 +176,9 @@ extension BundleViewModel { } BundleImageCache.shared.delete(for: documentId) + + // 홈 화면 리프레시 필요 플래그 설정 + HomeViewModel.needsRefresh = true } } catch { diff --git a/Keychy/Keychy/Presentation/Home/ViewModels/HomeViewModel.swift b/Keychy/Keychy/Presentation/Home/ViewModels/HomeViewModel.swift index 56a7888a..13c100d9 100644 --- a/Keychy/Keychy/Presentation/Home/ViewModels/HomeViewModel.swift +++ b/Keychy/Keychy/Presentation/Home/ViewModels/HomeViewModel.swift @@ -17,12 +17,15 @@ class HomeViewModel { /// 씬 준비 완료 여부 var isSceneReady = false - + /// 데이터 로드 완료 여부 var isDataLoaded = false /// 마지막으로 로드한 뭉치 ID (뭉치 변경 감지용) private var lastLoadedBundleId: String? + + /// 다른 화면에서 키링/뭉치 수정 후 홈 리프레시 필요 여부 + static var needsRefresh: Bool = false /// 네트워크 에러 발생 여부 var hasNetworkError: Bool = false @@ -40,6 +43,12 @@ class HomeViewModel { @MainActor func loadMainBundle(collectionViewModel: CollectionViewModel, bundleViewModel: BundleViewModel, onBackgroundLoaded: (() -> Void)?) async { + // 리프레시 필요 시 캐시 무효화 + if Self.needsRefresh { + Self.needsRefresh = false + lastLoadedBundleId = nil + } + // 이미 데이터가 로드되었고, 같은 뭉치가 선택된 상태면 스킵 (탭 전환 후 돌아올 때) if isDataLoaded, let currentBundle = bundleViewModel.selectedBundle,