From 8bb3724d3a063e90e7d85e9788de5fc34962247f Mon Sep 17 00:00:00 2001 From: giljihun Date: Sun, 25 Jan 2026 12:41:11 +0900 Subject: [PATCH 01/11] =?UTF-8?q?feat:=20WorkshopViewModel=20-=20=ED=82=A4?= =?UTF-8?q?=EB=A7=81/=EB=AD=89=EC=B9=98=20=ED=83=AD=20=EC=B9=B4=ED=85=8C?= =?UTF-8?q?=EA=B3=A0=EB=A6=AC=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - workshopToggle 프로퍼티 여기로 이전 - 키링카테고리, 번들 카테고리 분류 - fetchDataForCategory 카테고리 -> 데이터타입으로 변경 --- .../ViewModels/WorkshopViewModel.swift | 65 ++++++++++++++----- 1 file changed, 48 insertions(+), 17 deletions(-) diff --git a/Keychy/Keychy/Presentation/Workshop/ViewModels/WorkshopViewModel.swift b/Keychy/Keychy/Presentation/Workshop/ViewModels/WorkshopViewModel.swift index e3f3c552..e33f483a 100644 --- a/Keychy/Keychy/Presentation/Workshop/ViewModels/WorkshopViewModel.swift +++ b/Keychy/Keychy/Presentation/Workshop/ViewModels/WorkshopViewModel.swift @@ -82,8 +82,30 @@ extension Sound: WorkshopItem { @MainActor @Observable class WorkshopViewModel { + // MARK: - Tab & Category Properties + + /// 키링/뭉치 탭 토글 (true = 키링, false = 뭉치) + var workshopToggle: Bool = true { + didSet { + // 탭 전환 시 카테고리 초기화 + selectedCategory = currentCategories.first ?? "" + resetFilters() + } + } + + /// 키링 탭 카테고리 + let keyringCategories = ["전체", "이미지", "텍스트", "드로잉"] + + /// 뭉치 탭 카테고리 + let bundleCategories = ["카라비너", "배경"] + + /// 현재 탭에 따른 카테고리 목록 + var currentCategories: [String] { + workshopToggle ? keyringCategories : bundleCategories + } + // MARK: - Published Properties - var selectedCategory: String = "템플릿" + var selectedCategory: String = "전체" var selectedTemplateFilter: TemplateFilterType? = nil var selectedCommonFilter: String? = nil var selectedEffectFilter: EffectFilterType? = .sound @@ -212,15 +234,16 @@ class WorkshopViewModel { // MARK: - Firebase Methods (통합) /// 특정 카테고리의 데이터만 가져오기 func fetchDataForCategory(_ category: String) async { - // 이미 로드된 카테고리는 스킵 - guard !loadedCategories.contains(category) else { return } + // 데이터 타입별로 로드 여부 확인 + let dataType = categoryToDataType(category) + guard !loadedCategories.contains(dataType) else { return } isLoading = true errorMessage = nil defer { isLoading = false } - switch category { + switch dataType { case "템플릿": await dataManager.fetchTemplatesIfNeeded() loadedCategories.insert("템플릿") @@ -232,24 +255,32 @@ class WorkshopViewModel { await dataManager.fetchCarabinersIfNeeded() extractAvailableTags() loadedCategories.insert("카라비너") - case "이펙트": - await dataManager.fetchParticlesIfNeeded() - await dataManager.fetchSoundsIfNeeded() - loadedCategories.insert("이펙트") default: break } } - /// 나머지 카테고리들을 백그라운드에서 프리페칭 - func prefetchRemainingData() async { - let allCategories = ["템플릿", "배경", "카라비너", "이펙트"] + /// 카테고리 → 데이터 타입 매핑 + private func categoryToDataType(_ category: String) -> String { + switch category { + // 키링 탭 카테고리 → 템플릿 + case "전체", "이미지", "텍스트", "드로잉": + return "템플릿" + // 뭉치 탭 카테고리 → 그대로 + case "카라비너", "배경": + return category + default: + return category + } + } - for category in allCategories { - // 이미 로드된 카테고리는 스킵 - guard !loadedCategories.contains(category) else { continue } + /// 나머지 데이터를 백그라운드에서 프리페칭 + func prefetchRemainingData() async { + let allDataTypes = ["템플릿", "배경", "카라비너"] - await fetchDataForCategory(category) + for dataType in allDataTypes { + guard !loadedCategories.contains(dataType) else { continue } + await fetchDataForCategory(dataType) } } @@ -263,8 +294,8 @@ class WorkshopViewModel { // WorkshopDataManager를 통해 캐싱된 데이터 가져오기 await dataManager.fetchAllDataIfNeeded() - // 모든 카테고리를 로드된 것으로 표시 - loadedCategories = ["템플릿", "배경", "카라비너", "이펙트"] + // 모든 데이터 타입을 로드된 것으로 표시 + loadedCategories = ["템플릿", "배경", "카라비너"] // 데이터를 가져온 후 사용 가능한 태그 추출 extractAvailableTags() From 295db447998ddd8bccd953042ee62141b95f8eb2 Mon Sep 17 00:00:00 2001 From: giljihun Date: Sun, 25 Jan 2026 12:41:40 +0900 Subject: [PATCH 02/11] =?UTF-8?q?chore:=20=EB=B6=88=ED=95=84=EC=9A=94=20?= =?UTF-8?q?=ED=94=84=EB=A6=AC=EB=B7=B0=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Keychy/Core/Components/CategoryTabBar.swift | 17 ----------------- .../Views/Keyring/WorkshopRecentTemplate.swift | 16 ---------------- 2 files changed, 33 deletions(-) diff --git a/Keychy/Keychy/Core/Components/CategoryTabBar.swift b/Keychy/Keychy/Core/Components/CategoryTabBar.swift index 1d1b278a..83bed095 100644 --- a/Keychy/Keychy/Core/Components/CategoryTabBar.swift +++ b/Keychy/Keychy/Core/Components/CategoryTabBar.swift @@ -77,20 +77,3 @@ private struct CategoryTabButton: View { } } } - -// MARK: - Preview - -#Preview("예시 프리뷰") { - @Previewable @State var selectedCategory = "키링" - let categories = ["키링", "카라비너", "이펫트", "배경"] - - VStack() { - CategoryTabBar( - categories: categories, - selectedCategory: $selectedCategory - ) - - Spacer() - } - .padding() -} diff --git a/Keychy/Keychy/Presentation/Workshop/Views/Keyring/WorkshopRecentTemplate.swift b/Keychy/Keychy/Presentation/Workshop/Views/Keyring/WorkshopRecentTemplate.swift index 0a635354..cf81bef7 100644 --- a/Keychy/Keychy/Presentation/Workshop/Views/Keyring/WorkshopRecentTemplate.swift +++ b/Keychy/Keychy/Presentation/Workshop/Views/Keyring/WorkshopRecentTemplate.swift @@ -130,19 +130,3 @@ private struct RecentTemplateCard: View { .buttonStyle(.plain) } } - -// MARK: - Preview - -#Preview("로딩 상태") { - WorkshopRecentTemplate( - templates: [], - isLoading: true - ) -} - -#Preview("빈 상태") { - WorkshopRecentTemplate( - templates: [], - isLoading: false - ) -} From 7a6d5471c41b31ea515e99ff71290d03e8ed3fe1 Mon Sep 17 00:00:00 2001 From: giljihun Date: Sun, 25 Jan 2026 12:42:12 +0900 Subject: [PATCH 03/11] =?UTF-8?q?refactor:=20=ED=82=A4=EB=A7=81/=EB=AD=89?= =?UTF-8?q?=EC=B9=98=20=ED=83=AD=EB=B3=84=20=EA=B7=B8=EB=A6=AC=EB=93=9C=20?= =?UTF-8?q?=EB=B7=B0=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - WorkshopBundle/KeyringGridView.swift --- .../Views/Bundle/WorkshopBundleGridView.swift | 102 +++++++++++++++++ .../Keyring/WorkshopKeyringGridView.swift | 104 ++++++++++++++++++ 2 files changed, 206 insertions(+) create mode 100644 Keychy/Keychy/Presentation/Workshop/Views/Bundle/WorkshopBundleGridView.swift create mode 100644 Keychy/Keychy/Presentation/Workshop/Views/Keyring/WorkshopKeyringGridView.swift diff --git a/Keychy/Keychy/Presentation/Workshop/Views/Bundle/WorkshopBundleGridView.swift b/Keychy/Keychy/Presentation/Workshop/Views/Bundle/WorkshopBundleGridView.swift new file mode 100644 index 00000000..a29e64b5 --- /dev/null +++ b/Keychy/Keychy/Presentation/Workshop/Views/Bundle/WorkshopBundleGridView.swift @@ -0,0 +1,102 @@ +// +// WorkshopBundleGridView.swift +// Keychy +// +// Created by 길지훈 on 1/22/26. +// + +import SwiftUI + +/// 뭉치 탭 그리드 뷰 (카라비너, 배경 표시) +/// 카테고리: 카라비너, 배경 +struct WorkshopBundleGridView: View { + @Bindable var viewModel: WorkshopViewModel + @Bindable var router: NavigationRouter + + var body: some View { + VStack { + if viewModel.isLoading { + loadingView + } else { + if let errorMessage = viewModel.errorMessage { + errorView(message: errorMessage) + } else { + bundleGridContent + } + } + } + .background(.white100) + } + + /// 뭉치 그리드 콘텐츠 (카테고리별) + @ViewBuilder + private var bundleGridContent: some View { + switch viewModel.selectedCategory { + case "카라비너": + WorkshopGridBuilder.itemGridView( + items: viewModel.filteredCarabiners, + isOwnedCheck: viewModel.isCarabinerOwned, + router: router, + viewModel: viewModel, + emptyView: emptyContentView + ) + case "배경": + WorkshopGridBuilder.itemGridView( + items: viewModel.filteredBackgrounds, + isOwnedCheck: viewModel.isBackgroundOwned, + router: router, + viewModel: viewModel, + emptyView: emptyContentView + ) + default: + emptyContentView + } + } + + /// 로딩 뷰 (스켈레톤) + private var loadingView: some View { + HStack(spacing: 11) { + WorkshopSkeletonBox(width: twoGridCellWidth, height: twoSquareGridCellSize) + WorkshopSkeletonBox(width: twoGridCellWidth, height: twoSquareGridCellSize) + } + .padding(.horizontal, 16) + .padding(.vertical, 92) + } + + /// 빈 콘텐츠 뷰 + private var emptyContentView: some View { + VStack(spacing: 12) { + Image(.emptyViewIcon) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 90) + + Text("준비중이에요") + .typography(.suit14SB18) + .foregroundColor(.gray500) + } + .frame(maxWidth: .infinity, minHeight: 300) + .padding(.top, 50) + } + + /// 에러 뷰 + private func errorView(message: String) -> some View { + VStack(spacing: 16) { + Image(systemName: "exclamationmark.triangle") + .font(.system(size: 50)) + .foregroundStyle(.secondary) + + Text(message) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + + Button("다시 시도") { + Task { + await viewModel.fetchAllData() + } + } + .buttonStyle(.borderedProminent) + } + .padding(.top, 100) + } +} diff --git a/Keychy/Keychy/Presentation/Workshop/Views/Keyring/WorkshopKeyringGridView.swift b/Keychy/Keychy/Presentation/Workshop/Views/Keyring/WorkshopKeyringGridView.swift new file mode 100644 index 00000000..a168f515 --- /dev/null +++ b/Keychy/Keychy/Presentation/Workshop/Views/Keyring/WorkshopKeyringGridView.swift @@ -0,0 +1,104 @@ +// +// WorkshopKeyringGridView.swift +// Keychy +// +// Created by 길지훈 on 1/22/26. +// + +import SwiftUI + +/// 키링 탭 그리드 뷰 (템플릿 표시) +/// 카테고리: 전체, 이미지, 텍스트, 드로잉 +struct WorkshopKeyringGridView: View { + @Bindable var viewModel: WorkshopViewModel + @Bindable var router: NavigationRouter + + var body: some View { + VStack { + if viewModel.isLoading { + loadingView + } else { + if let errorMessage = viewModel.errorMessage { + errorView(message: errorMessage) + } else { + templateGridContent + } + } + } + .background(.white100) + } + + /// 필터링된 템플릿 목록 + private var filteredTemplates: [KeyringTemplate] { + switch viewModel.selectedCategory { + case "전체": + return viewModel.filteredTemplates + case "이미지": + return viewModel.templates.filter { $0.tags.contains("이미지") } + case "텍스트": + return viewModel.templates.filter { $0.tags.contains("텍스트") } + case "드로잉": + return viewModel.templates.filter { $0.tags.contains("드로잉") } + default: + return viewModel.filteredTemplates + } + } + + /// 템플릿 그리드 콘텐츠 + private var templateGridContent: some View { + WorkshopGridBuilder.itemGridView( + items: filteredTemplates, + isOwnedCheck: viewModel.isTemplateOwned, + router: router, + viewModel: viewModel, + emptyView: emptyContentView + ) + } + + /// 로딩 뷰 (스켈레톤) + private var loadingView: some View { + HStack(spacing: 11) { + WorkshopSkeletonBox(width: twoGridCellWidth, height: twoGridCellHeight) + WorkshopSkeletonBox(width: twoGridCellWidth, height: twoGridCellHeight) + } + .padding(.horizontal, 16) + .padding(.vertical, 92) + } + + /// 빈 콘텐츠 뷰 + private var emptyContentView: some View { + VStack(spacing: 12) { + Image(.emptyViewIcon) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 90) + + Text("준비중이에요") + .typography(.suit14SB18) + .foregroundColor(.gray500) + } + .frame(maxWidth: .infinity, minHeight: 300) + .padding(.top, 50) + } + + /// 에러 뷰 + private func errorView(message: String) -> some View { + VStack(spacing: 16) { + Image(systemName: "exclamationmark.triangle") + .font(.system(size: 50)) + .foregroundStyle(.secondary) + + Text(message) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + + Button("다시 시도") { + Task { + await viewModel.fetchAllData() + } + } + .buttonStyle(.borderedProminent) + } + .padding(.top, 100) + } +} From 9b16745c3c640bfdaebc76219049a949cd438f22 Mon Sep 17 00:00:00 2001 From: giljihun Date: Sun, 25 Jan 2026 12:42:44 +0900 Subject: [PATCH 04/11] =?UTF-8?q?refactor:=20=EC=9C=84=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84=20=ED=9B=84,=20=EA=B8=B0?= =?UTF-8?q?=EC=A1=B4=20Workshop=20=EC=9D=B5=EC=8A=A4=ED=85=90=EC=85=98=20?= =?UTF-8?q?=EB=82=B4=20=EC=BD=94=EB=93=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Keychy/Keychy.xcodeproj/project.pbxproj | 8 ++ .../Views/Components/WorkshopFilterBar.swift | 71 ++++------ .../Views/Main/WorkshopView+MainContent.swift | 131 +----------------- .../Main/WorkshopView+NetworkError.swift | 2 +- .../Main/WorkshopView+StickyHeader.swift | 4 +- .../Views/Main/WorkshopView+TopSection.swift | 39 +----- .../Workshop/Views/Main/WorkshopView.swift | 3 - 7 files changed, 52 insertions(+), 206 deletions(-) diff --git a/Keychy/Keychy.xcodeproj/project.pbxproj b/Keychy/Keychy.xcodeproj/project.pbxproj index 1b6744b6..68036c54 100644 --- a/Keychy/Keychy.xcodeproj/project.pbxproj +++ b/Keychy/Keychy.xcodeproj/project.pbxproj @@ -243,6 +243,8 @@ 4C8426602ED3585A0050B6FE /* gulimche-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 4C84265F2ED3585A0050B6FE /* gulimche-Regular.ttf */; }; 4C8426642ED375840050B6FE /* ColorPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8426632ED375840050B6FE /* ColorPalette.swift */; }; 4C84A1602EB134BD008FFE57 /* ProfileSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A596832EAFEAA20003D712 /* ProfileSetupView.swift */; }; + 4C86A6122F25C0B10023AA2D /* WorkshopBundleGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C86A6112F25C0B10023AA2D /* WorkshopBundleGridView.swift */; }; + 4C86A6142F25C0BA0023AA2D /* WorkshopKeyringGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C86A6132F25C0BA0023AA2D /* WorkshopKeyringGridView.swift */; }; 4CA9C6A62EC9D11600CA546B /* CustomNavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA9C6A42EC9D11600CA546B /* CustomNavigationBar.swift */; }; 4CA9C6A82EC9DB5300CA546B /* View+SafeAreaBottom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA9C6A72EC9DB5300CA546B /* View+SafeAreaBottom.swift */; }; 4CA9C6D62ECB7AEA00CA546B /* BadWords.json in Resources */ = {isa = PBXBuildFile; fileRef = 4CA9C6D52ECB7AEA00CA546B /* BadWords.json */; }; @@ -681,6 +683,8 @@ 4C77753C2EB1343600981C3E /* IntroViewModel+Signup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IntroViewModel+Signup.swift"; sourceTree = ""; }; 4C84265F2ED3585A0050B6FE /* gulimche-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = text; path = "gulimche-Regular.ttf"; sourceTree = ""; }; 4C8426632ED375840050B6FE /* ColorPalette.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPalette.swift; sourceTree = ""; }; + 4C86A6112F25C0B10023AA2D /* WorkshopBundleGridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkshopBundleGridView.swift; sourceTree = ""; }; + 4C86A6132F25C0BA0023AA2D /* WorkshopKeyringGridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkshopKeyringGridView.swift; sourceTree = ""; }; 4CA9C6A42EC9D11600CA546B /* CustomNavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomNavigationBar.swift; sourceTree = ""; }; 4CA9C6A72EC9DB5300CA546B /* View+SafeAreaBottom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+SafeAreaBottom.swift"; sourceTree = ""; }; 4CA9C6D52ECB7AEA00CA546B /* BadWords.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = BadWords.json; sourceTree = ""; }; @@ -1434,6 +1438,7 @@ isa = PBXGroup; children = ( 4C4733E42F20FE34005D2376 /* WorkshopRecentTemplate.swift */, + 4C86A6132F25C0BA0023AA2D /* WorkshopKeyringGridView.swift */, ); path = Keyring; sourceTree = ""; @@ -1442,6 +1447,7 @@ isa = PBXGroup; children = ( 4C47331A2F1FA2AB005D2376 /* WorkshopBundleBanner.swift */, + 4C86A6112F25C0B10023AA2D /* WorkshopBundleGridView.swift */, ); path = Bundle; sourceTree = ""; @@ -2617,6 +2623,7 @@ 4C4733272F1FA2AB005D2376 /* WorkshopBundleBanner.swift in Sources */, 4C4733282F1FA2AB005D2376 /* WorkshopTemplatesView.swift in Sources */, 4C47332D2F1FA2AB005D2376 /* WorkshopViewModel.swift in Sources */, + 4C86A6142F25C0BA0023AA2D /* WorkshopKeyringGridView.swift in Sources */, 386B17642ECD142600CCCC23 /* String+Extension.swift in Sources */, 4CEBB1652EFBA54200CF53E2 /* RootViewModel.swift in Sources */, 4CC3D37F2EC464D70009D376 /* IntroAppGuidingView.swift in Sources */, @@ -2650,6 +2657,7 @@ 38C147C72EB1F57F00A8E511 /* StorageManager.swift in Sources */, 4CEC629F2EAE09990099ECEE /* BundleDetailView.swift in Sources */, 4CEBB1692EFBDCE900CF53E2 /* NetworkManager.swift in Sources */, + 4C86A6122F25C0B10023AA2D /* WorkshopBundleGridView.swift in Sources */, 4CEC62A02EAE09990099ECEE /* BundleInventoryView.swift in Sources */, 38283A812EBF511C00BE45A5 /* PackagePopup.swift in Sources */, 4CC8D00B2EEFC36900317467 /* AlarmViewModel.swift in Sources */, diff --git a/Keychy/Keychy/Presentation/Workshop/Views/Components/WorkshopFilterBar.swift b/Keychy/Keychy/Presentation/Workshop/Views/Components/WorkshopFilterBar.swift index 197c39f7..621218d9 100644 --- a/Keychy/Keychy/Presentation/Workshop/Views/Components/WorkshopFilterBar.swift +++ b/Keychy/Keychy/Presentation/Workshop/Views/Components/WorkshopFilterBar.swift @@ -53,56 +53,39 @@ struct WorkshopFilterBar: View { } /// 카테고리별 필터 옵션 + @ViewBuilder private var categorySpecificFilters: some View { - Group { - switch viewModel.selectedCategory { - case "템플릿": - ForEach(TemplateFilterType.allCases, id: \.self) { filter in - WorkshopFilterChip( - title: filter.rawValue, - isSelected: viewModel.selectedTemplateFilter == filter - ) { - viewModel.selectedTemplateFilter = - viewModel.selectedTemplateFilter == filter ? nil : filter - } - } - - case "이펙트": - ForEach(EffectFilterType.allCases, id: \.self) { filter in - WorkshopFilterChip( - title: filter.rawValue, - isSelected: viewModel.selectedEffectFilter == filter - ) { - viewModel.selectedEffectFilter = - viewModel.selectedEffectFilter == filter ? nil : filter - } - } + switch viewModel.selectedCategory { + // 키링 탭: 전체/이미지/텍스트/드로잉은 카테고리 자체가 필터 → 추가 필터 없음 + case "전체", "이미지", "텍스트", "드로잉": + EmptyView() - case "카라비너": - ForEach(viewModel.availableCarabinerTags, id: \.self) { tag in - WorkshopFilterChip( - title: tag, - isSelected: viewModel.selectedCommonFilter == tag - ) { - viewModel.selectedCommonFilter = - viewModel.selectedCommonFilter == tag ? nil : tag - } + // 뭉치 탭: 카라비너 태그 필터 + case "카라비너": + ForEach(viewModel.availableCarabinerTags, id: \.self) { tag in + WorkshopFilterChip( + title: tag, + isSelected: viewModel.selectedCommonFilter == tag + ) { + viewModel.selectedCommonFilter = + viewModel.selectedCommonFilter == tag ? nil : tag } + } - case "배경": - ForEach(viewModel.availableBackgroundTags, id: \.self) { tag in - WorkshopFilterChip( - title: tag, - isSelected: viewModel.selectedCommonFilter == tag - ) { - viewModel.selectedCommonFilter = - viewModel.selectedCommonFilter == tag ? nil : tag - } + // 뭉치 탭: 배경 태그 필터 + case "배경": + ForEach(viewModel.availableBackgroundTags, id: \.self) { tag in + WorkshopFilterChip( + title: tag, + isSelected: viewModel.selectedCommonFilter == tag + ) { + viewModel.selectedCommonFilter = + viewModel.selectedCommonFilter == tag ? nil : tag } - - default: - EmptyView() } + + default: + EmptyView() } } } diff --git a/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView+MainContent.swift b/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView+MainContent.swift index af2f5461..8a4d5a29 100644 --- a/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView+MainContent.swift +++ b/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView+MainContent.swift @@ -10,136 +10,19 @@ import SwiftUI // MARK: - Main Content Section extension WorkshopView { - /// 메인 콘텐츠 영역 (카테고리별 그리드) + /// 메인 콘텐츠 영역 (키링/뭉치 탭에 따른 그리드) var mainContentSection: some View { - VStack { - if viewModel.isLoading { - loadingView - } else { - if let errorMessage = viewModel.errorMessage { - errorView(message: errorMessage) - } else { - categoryContent - } - } - } - .background(.white100) - } - - /// 로딩 뷰 (스켈레톤 애니메이션) - var loadingView: some View { - HStack(spacing: 11){ - WorkshopSkeletonBox(width: twoGridCellWidth, height: twoGridCellHeight) - WorkshopSkeletonBox(width: twoGridCellWidth, height: twoGridCellHeight) - } - .padding(.horizontal, 16) - .padding(.vertical, 92) - } - - - /// 카테고리별 콘텐츠 - var categoryContent: some View { Group { - switch viewModel.selectedCategory { - - //case "KEYCHY!": keychyContentView - - case "템플릿": - itemGridView(items: viewModel.filteredTemplates, - isOwnedCheck: viewModel.isTemplateOwned) - case "배경": - itemGridView(items: viewModel.filteredBackgrounds, - isOwnedCheck: viewModel.isBackgroundOwned) - case "카라비너": - itemGridView(items: viewModel.filteredCarabiners, - isOwnedCheck: viewModel.isCarabinerOwned) - case "이펙트": - effectContentView - default: - emptyContentView + if viewModel.workshopToggle { + // 키링 탭: 템플릿 그리드 + WorkshopKeyringGridView(viewModel: viewModel, router: router) + } else { + // 뭉치 탭: 카라비너/배경 그리드 + WorkshopBundleGridView(viewModel: viewModel, router: router) } } } - /// 이펙트 전용 콘텐츠 (사운드 + 파티클) - var effectContentView: some View { - WorkshopGridBuilder.effectGridView( - items: viewModel.filteredEffects, - isSoundOwned: viewModel.isSoundOwned, - isParticleOwned: viewModel.isParticleOwned, - router: router, - viewModel: viewModel, - emptyView: emptyContentView - ) - } - - /// 통합 아이템 그리드 뷰 - func itemGridView( - items: [T], - isOwnedCheck: @escaping (T) -> Bool - ) -> some View { - WorkshopGridBuilder.itemGridView( - items: items, - isOwnedCheck: isOwnedCheck, - router: router, - viewModel: viewModel, - emptyView: emptyContentView - ) - } - - // MARK: - KEYCHY! 탭 (향후 추가 시 사용) - // /// KEYCHY! 전용 콘텐츠 (준비 중) - // var keychyContentView: some View { - // VStack(spacing: 12) { - // Image(systemName: "sparkles") - // .font(.system(size: 20, weight: .semibold)) - // .foregroundStyle(.purple).opacity(0.6) - // - // Text("KEYCHY! 디자이너 열일중..") - // .typography(.suit14SB18) - // .foregroundColor(.gray500) - // .multilineTextAlignment(.center) - // } - // .frame(maxWidth: .infinity, minHeight: 300) - // .padding(.top, 50) - // } - - /// 빈 콘텐츠 뷰 - var emptyContentView: some View { - VStack(spacing: 12) { - Image(.emptyViewIcon) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 90) - - Text("준비중이에요") - .typography(.suit14SB18) - .foregroundColor(.gray500) - } - .frame(maxWidth: .infinity, minHeight: 300) - .padding(.top, 50) - } - - /// 에러 뷰 - func errorView(message: String) -> some View { - VStack(spacing: 16) { - Image(systemName: "exclamationmark.triangle") - .font(.system(size: 50)) - .foregroundStyle(.secondary) - - Text(message) - .foregroundStyle(.secondary) - .multilineTextAlignment(.center) - - Button("다시 시도") { - Task { - await viewModel.fetchAllData() - } - } - .buttonStyle(.borderedProminent) - } - .padding(.top, 100) - } } diff --git a/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView+NetworkError.swift b/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView+NetworkError.swift index 2ea28752..d6f3f376 100644 --- a/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView+NetworkError.swift +++ b/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView+NetworkError.swift @@ -24,7 +24,7 @@ extension WorkshopView { HStack { titleView Spacer() - myItemBtn + makeBtn } .padding(.top, 60) .padding(.horizontal, 20) diff --git a/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView+StickyHeader.swift b/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView+StickyHeader.swift index 18ffbdd0..5a8579e6 100644 --- a/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView+StickyHeader.swift +++ b/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView+StickyHeader.swift @@ -13,9 +13,9 @@ extension WorkshopView { /// 스티키 헤더 (카테고리 + 필터) var stickyHeaderSection: some View { VStack(spacing: 0) { - // 카테고리 탭바 + // 카테고리 탭바 (키링/뭉치 탭에 따라 동적 변경) CategoryTabBar( - categories: categories, + categories: viewModel.currentCategories, selectedCategory: $viewModel.selectedCategory ) .padding(.top, 12) diff --git a/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView+TopSection.swift b/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView+TopSection.swift index c69a9060..ae203e7a 100644 --- a/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView+TopSection.swift +++ b/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView+TopSection.swift @@ -26,7 +26,7 @@ extension WorkshopView { HStack { titleView Spacer() - myItemBtn + makeBtn } .padding(.top, WorkshopLayout.topPadding) .padding(.horizontal, 20) @@ -35,52 +35,27 @@ extension WorkshopView { .opacity(viewModel.mainContentOffset - WorkshopLayout.titleBarOpacityThreshold < WorkshopLayout.titleBarOpacityRange ? 1 : 0) } - /// 타이틀 뷰 + /// 타이틀 뷰 (키링/뭉치 탭 전환) var titleView: some View { HStack(spacing: 10) { Button { - // TODO: - 공방 탭 액션 - workshopToggle = true + viewModel.workshopToggle = true } label: { Text("키링") .typography(.nanum24EB) - .foregroundStyle(workshopToggle ? .black100 : .gray100) + .foregroundStyle(viewModel.workshopToggle ? .black100 : .gray100) } - + Button { - // TODO: - 뭉치 탭 액션 - workshopToggle = false + viewModel.workshopToggle = false } label: { Text("뭉치") .typography(.nanum24EB) - .foregroundStyle(workshopToggle ? .gray100 : .black100) + .foregroundStyle(viewModel.workshopToggle ? .gray100 : .black100) } } } - /// 내 아이템 버튼 - var myItemBtn: some View { - Button { - router.push(.myItems) - } label: { - HStack(spacing: 0) { - Image(.myItem) - .resizable() - .scaledToFit() - - Spacer() - - Text("내 아이템") - .typography(.suit17B) - .foregroundColor(.black) - } - } - .frame(minWidth: 80) - .frame(height: 44) - .fixedSize(horizontal: true, vertical: true) - .buttonStyle(.glass) - } - /// 만들기 메뉴 버튼 var makeBtn: some View { Button { diff --git a/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView.swift b/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView.swift index e7106450..0bd6423f 100644 --- a/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView.swift +++ b/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView.swift @@ -31,14 +31,11 @@ struct WorkshopView: View { @State var viewModel: WorkshopViewModel @State private var hasInitialized = false @State private var isTabBarVisible = true - @State var workshopToggle: Bool = true // 만들기 메뉴 상태 @State var showMakeMenu: Bool = false @State var makeMenuPosition: CGRect = .zero - let categories = ["템플릿", "카라비너", "이펙트", "배경"] - /// WorkshopTab에서 생성된 viewModel을 받아서 사용 init( router: NavigationRouter, From ba1930b8b10224dbb7df2738a76ac85215b03ea4 Mon Sep 17 00:00:00 2001 From: giljihun Date: Sun, 25 Jan 2026 18:29:41 +0900 Subject: [PATCH 05/11] =?UTF-8?q?refactor:=20=EB=A7=8C=EB=93=A4=EA=B8=B0?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=EC=9D=B4=20=EC=95=84=EB=9E=98=EB=A1=9C=20?= =?UTF-8?q?=EC=8A=A4=ED=81=AC=EB=A1=A4=ED=95=B4=EB=8F=84=20=EB=B3=B4?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=95=8A=EB=8D=98=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 스크롤한 경우를 대응하지 않았었음. --- .../Workshop/Views/Components/WorkshopMakeMenu.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Keychy/Keychy/Presentation/Workshop/Views/Components/WorkshopMakeMenu.swift b/Keychy/Keychy/Presentation/Workshop/Views/Components/WorkshopMakeMenu.swift index a2db4c41..ddbe71ea 100644 --- a/Keychy/Keychy/Presentation/Workshop/Views/Components/WorkshopMakeMenu.swift +++ b/Keychy/Keychy/Presentation/Workshop/Views/Components/WorkshopMakeMenu.swift @@ -61,7 +61,7 @@ struct WorkshopMakeMenu: View { .opacity(isAppearing ? 1.0 : 0.0) .position( x: geometry.size.width - menuWidth / 2 - 16, - y: position.maxY - geometry.safeAreaInsets.top + 5 + menuHeight / 2 + y: geometry.safeAreaInsets.top + menuHeight / 2 - 5 // (맨 뒤에 상수값을 조정해서 위치 조정 가능) ) } } From cc8bc9ee5c99e4b03e37af24588fc0f8cb88889e Mon Sep 17 00:00:00 2001 From: giljihun Date: Sun, 25 Jan 2026 18:56:15 +0900 Subject: [PATCH 06/11] =?UTF-8?q?style:=20CategoryTabBar=20-=20=ED=84=B0?= =?UTF-8?q?=EC=B9=98=EC=98=81=EC=97=AD=20=EC=82=AC=EC=9A=A9=EC=84=B1=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0,=20=EC=96=B8=EB=8D=94=EB=B0=94=20=ED=81=AC?= =?UTF-8?q?=EA=B8=B0=20=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 - 기존엔 텍스트 영역 -> 정확히 카테고리 갯수에 나눠서 맞춰지는 영역 크기 --- Keychy/Keychy/Core/Components/CategoryTabBar.swift | 6 +++--- .../Workshop/Views/Components/WorkshopItemCard.swift | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Keychy/Keychy/Core/Components/CategoryTabBar.swift b/Keychy/Keychy/Core/Components/CategoryTabBar.swift index 83bed095..fa64390f 100644 --- a/Keychy/Keychy/Core/Components/CategoryTabBar.swift +++ b/Keychy/Keychy/Core/Components/CategoryTabBar.swift @@ -62,13 +62,13 @@ private struct CategoryTabButton: View { Text(title) .typography(isSelected ? .suit15B25 : .suit15SB25) .foregroundStyle(isSelected ? Color.main500 : Color.black100) - + Rectangle() .fill(isSelected ? Color.main500 : Color.clear) .frame(height: 2) - .padding(.horizontal, -4) // 좌우로 2pt씩 확장 + .padding(.horizontal, 10) } - .fixedSize(horizontal: true, vertical: false) + .contentShape(Rectangle()) } .frame(maxWidth: .infinity) .buttonStyle(.plain) diff --git a/Keychy/Keychy/Presentation/Workshop/Views/Components/WorkshopItemCard.swift b/Keychy/Keychy/Presentation/Workshop/Views/Components/WorkshopItemCard.swift index d9f52956..696fe5a7 100644 --- a/Keychy/Keychy/Presentation/Workshop/Views/Components/WorkshopItemCard.swift +++ b/Keychy/Keychy/Presentation/Workshop/Views/Components/WorkshopItemCard.swift @@ -155,11 +155,11 @@ struct WorkshopPriceOverlay: View { // 유료 아이콘 VStack { HStack { - Image(.paidIcon) + Image(.myCoin) Spacer() } - .padding(.top, 7) - .padding(.leading, 10) + .padding(.top, 9.84) + .padding(.leading, 9.35) Spacer() } .opacity(isFree ? 0 : 1) From 59c18dbc8d48360359005cdeba3eff978c46101b Mon Sep 17 00:00:00 2001 From: giljihun Date: Sun, 25 Jan 2026 19:24:11 +0900 Subject: [PATCH 07/11] =?UTF-8?q?style:=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20=ED=83=AD=EB=B0=94=20=EB=89=B4=ED=95=98=EC=9D=B4?= =?UTF-8?q?=ED=8C=8C=EC=9D=B4=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 주요: 최신순 정렬버튼 바텀과 떨어진 거리 --- Keychy/Keychy/Core/Components/CategoryTabBar.swift | 6 +++--- .../Views/Components/WorkshopFilterBar.swift | 1 - .../Views/Components/WorkshopGridBuilder.swift | 3 ++- .../Views/Keyring/WorkshopKeyringGridView.swift | 3 ++- .../Views/Main/WorkshopView+StickyHeader.swift | 13 ++++--------- 5 files changed, 11 insertions(+), 15 deletions(-) diff --git a/Keychy/Keychy/Core/Components/CategoryTabBar.swift b/Keychy/Keychy/Core/Components/CategoryTabBar.swift index fa64390f..ce3e886a 100644 --- a/Keychy/Keychy/Core/Components/CategoryTabBar.swift +++ b/Keychy/Keychy/Core/Components/CategoryTabBar.swift @@ -60,11 +60,11 @@ private struct CategoryTabButton: View { Button(action: action) { VStack(spacing: Spacing.sm) { Text(title) - .typography(isSelected ? .suit15B25 : .suit15SB25) - .foregroundStyle(isSelected ? Color.main500 : Color.black100) + .typography(.suit15SB25) + .foregroundStyle(isSelected ? .main500 : .black100) Rectangle() - .fill(isSelected ? Color.main500 : Color.clear) + .fill(isSelected ? .main500 : .clear) .frame(height: 2) .padding(.horizontal, 10) } diff --git a/Keychy/Keychy/Presentation/Workshop/Views/Components/WorkshopFilterBar.swift b/Keychy/Keychy/Presentation/Workshop/Views/Components/WorkshopFilterBar.swift index 621218d9..9b0ae891 100644 --- a/Keychy/Keychy/Presentation/Workshop/Views/Components/WorkshopFilterBar.swift +++ b/Keychy/Keychy/Presentation/Workshop/Views/Components/WorkshopFilterBar.swift @@ -25,7 +25,6 @@ struct WorkshopFilterBar: View { } } } - .padding(.top, 12) } /// 정렬 버튼 diff --git a/Keychy/Keychy/Presentation/Workshop/Views/Components/WorkshopGridBuilder.swift b/Keychy/Keychy/Presentation/Workshop/Views/Components/WorkshopGridBuilder.swift index b6f8b7c9..e146d990 100644 --- a/Keychy/Keychy/Presentation/Workshop/Views/Components/WorkshopGridBuilder.swift +++ b/Keychy/Keychy/Presentation/Workshop/Views/Components/WorkshopGridBuilder.swift @@ -37,7 +37,8 @@ struct WorkshopGridBuilder { } } .padding(.horizontal, 16) - .padding(.vertical, 92) + .padding(.top, 80) + .padding(.bottom, 92) } } } diff --git a/Keychy/Keychy/Presentation/Workshop/Views/Keyring/WorkshopKeyringGridView.swift b/Keychy/Keychy/Presentation/Workshop/Views/Keyring/WorkshopKeyringGridView.swift index a168f515..d63b8349 100644 --- a/Keychy/Keychy/Presentation/Workshop/Views/Keyring/WorkshopKeyringGridView.swift +++ b/Keychy/Keychy/Presentation/Workshop/Views/Keyring/WorkshopKeyringGridView.swift @@ -62,7 +62,8 @@ struct WorkshopKeyringGridView: View { WorkshopSkeletonBox(width: twoGridCellWidth, height: twoGridCellHeight) } .padding(.horizontal, 16) - .padding(.vertical, 92) + .padding(.top, 80) + .padding(.bottom, 92) } /// 빈 콘텐츠 뷰 diff --git a/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView+StickyHeader.swift b/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView+StickyHeader.swift index 5a8579e6..54184e85 100644 --- a/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView+StickyHeader.swift +++ b/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView+StickyHeader.swift @@ -18,22 +18,17 @@ extension WorkshopView { categories: viewModel.currentCategories, selectedCategory: $viewModel.selectedCategory ) - .padding(.top, 12) - + .padding(.bottom, 12) + // 필터바 - filterBar + WorkshopFilterBar(viewModel: $viewModel) } .padding(.horizontal, 20) - .padding(.bottom, 20) + .padding(.vertical, 12) .background(.white) .clipShape(.rect(cornerRadii: .init(topLeading: 20, topTrailing: 20))) .offset(y: max(WorkshopLayout.stickyHeaderMinOffset, min(WorkshopLayout.stickyHeaderMaxOffset, viewModel.mainContentOffset - WorkshopLayout.stickyHeaderOffsetAdjust))) } - - /// 필터바 - var filterBar: some View { - WorkshopFilterBar(viewModel: $viewModel) - } } From be89956f8e60fd3043772a6ee1b42192cc5200e4 Mon Sep 17 00:00:00 2001 From: giljihun Date: Sun, 25 Jan 2026 20:10:03 +0900 Subject: [PATCH 08/11] =?UTF-8?q?style:=20=ED=80=B5=ED=95=84=ED=84=B0=20(?= =?UTF-8?q?=EB=AC=B4=EB=A3=8C,=20=EC=86=8C=EC=9C=A0)=20=EC=95=84=EC=9D=B4?= =?UTF-8?q?=EC=BD=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../quickFilterChecked.imageset/Contents.json | 12 ++++++++++++ .../quickFilterChecked.pdf | Bin 0 -> 4582 bytes .../quickFilterOwned.imageset/Contents.json | 12 ++++++++++++ .../quickFilterOwned.pdf | Bin 0 -> 6522 bytes 4 files changed, 24 insertions(+) create mode 100644 Keychy/Keychy/Resources/Assets.xcassets/02. WorkShop/quickFilterChecked.imageset/Contents.json create mode 100644 Keychy/Keychy/Resources/Assets.xcassets/02. WorkShop/quickFilterChecked.imageset/quickFilterChecked.pdf create mode 100644 Keychy/Keychy/Resources/Assets.xcassets/02. WorkShop/quickFilterOwned.imageset/Contents.json create mode 100644 Keychy/Keychy/Resources/Assets.xcassets/02. WorkShop/quickFilterOwned.imageset/quickFilterOwned.pdf diff --git a/Keychy/Keychy/Resources/Assets.xcassets/02. WorkShop/quickFilterChecked.imageset/Contents.json b/Keychy/Keychy/Resources/Assets.xcassets/02. WorkShop/quickFilterChecked.imageset/Contents.json new file mode 100644 index 00000000..a23cee3f --- /dev/null +++ b/Keychy/Keychy/Resources/Assets.xcassets/02. WorkShop/quickFilterChecked.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "quickFilterChecked.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Keychy/Keychy/Resources/Assets.xcassets/02. WorkShop/quickFilterChecked.imageset/quickFilterChecked.pdf b/Keychy/Keychy/Resources/Assets.xcassets/02. WorkShop/quickFilterChecked.imageset/quickFilterChecked.pdf new file mode 100644 index 0000000000000000000000000000000000000000..1c23f32f9072ffad67ce15a9541a85c8c7780b1f GIT binary patch literal 4582 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 JRr=cv{trS;(DeWS literal 0 HcmV?d00001 diff --git a/Keychy/Keychy/Resources/Assets.xcassets/02. WorkShop/quickFilterOwned.imageset/Contents.json b/Keychy/Keychy/Resources/Assets.xcassets/02. WorkShop/quickFilterOwned.imageset/Contents.json new file mode 100644 index 00000000..5d2280c3 --- /dev/null +++ b/Keychy/Keychy/Resources/Assets.xcassets/02. WorkShop/quickFilterOwned.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "quickFilterOwned.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Keychy/Keychy/Resources/Assets.xcassets/02. WorkShop/quickFilterOwned.imageset/quickFilterOwned.pdf b/Keychy/Keychy/Resources/Assets.xcassets/02. WorkShop/quickFilterOwned.imageset/quickFilterOwned.pdf new file mode 100644 index 0000000000000000000000000000000000000000..f85f7e9dc25f36418a2fa93d835dd61d3450a963 GIT binary patch literal 6522 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 Date: Sun, 25 Jan 2026 20:10:41 +0900 Subject: [PATCH 09/11] =?UTF-8?q?feat:=20=ED=80=B5=ED=95=84=ED=84=B0=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=20(=EB=AC=B4=EB=A3=8C,=20=EC=86=8C=EC=9C=A0)?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 케이스를 정의해서, 다른 버튼이 생기거나 수정 소요가 있어도 쉽습니다. --- .../ViewModels/WorkshopViewModel.swift | 25 ++++++ .../Views/Bundle/WorkshopBundleGridView.swift | 32 +++++++- .../Views/Components/WorkshopFilterBar.swift | 78 +++++++++++++++++-- .../Keyring/WorkshopKeyringGridView.swift | 23 ++++-- 4 files changed, 143 insertions(+), 15 deletions(-) diff --git a/Keychy/Keychy/Presentation/Workshop/ViewModels/WorkshopViewModel.swift b/Keychy/Keychy/Presentation/Workshop/ViewModels/WorkshopViewModel.swift index e33f483a..6512ded4 100644 --- a/Keychy/Keychy/Presentation/Workshop/ViewModels/WorkshopViewModel.swift +++ b/Keychy/Keychy/Presentation/Workshop/ViewModels/WorkshopViewModel.swift @@ -20,6 +20,27 @@ enum EffectFilterType: String, CaseIterable { case sound = "사운드" case particle = "파티클" } + +/// 퀵 필터 타입 (무료, 마이) +enum QuickFilter: CaseIterable { + case free + case owned + + var title: String { + switch self { + case .free: return "무료" + case .owned: return "마이" + } + } + + var icon: ImageResource? { + switch self { + case .free: return nil + case .owned: return .quickFilterOwned + } + } + +} // MARK: - WorkshopItem Protocol /// 공방에서 판매되는 모든 아이템이 준수해야 하는 프로토콜 @@ -111,6 +132,10 @@ class WorkshopViewModel { var selectedEffectFilter: EffectFilterType? = .sound var sortOrder: String = "최신순" var showFilterSheet: Bool = false + + // MARK: - Quick Filter Properties + var showFreeOnly: Bool = false + var showOwnedOnly: Bool = false var mainContentOffset: CGFloat = 439 private var refreshTrigger: Bool = false diff --git a/Keychy/Keychy/Presentation/Workshop/Views/Bundle/WorkshopBundleGridView.swift b/Keychy/Keychy/Presentation/Workshop/Views/Bundle/WorkshopBundleGridView.swift index a29e64b5..a77af3da 100644 --- a/Keychy/Keychy/Presentation/Workshop/Views/Bundle/WorkshopBundleGridView.swift +++ b/Keychy/Keychy/Presentation/Workshop/Views/Bundle/WorkshopBundleGridView.swift @@ -28,13 +28,41 @@ struct WorkshopBundleGridView: View { .background(.white100) } + /// 필터링된 카라비너 목록 + private var filteredCarabiners: [Carabiner] { + var result = viewModel.filteredCarabiners + + if viewModel.showFreeOnly { + result = result.filter { $0.isFree } + } + if viewModel.showOwnedOnly { + result = result.filter { viewModel.isCarabinerOwned($0) } + } + + return result + } + + /// 필터링된 배경 목록 + private var filteredBackgrounds: [Background] { + var result = viewModel.filteredBackgrounds + + if viewModel.showFreeOnly { + result = result.filter { $0.isFree } + } + if viewModel.showOwnedOnly { + result = result.filter { viewModel.isBackgroundOwned($0) } + } + + return result + } + /// 뭉치 그리드 콘텐츠 (카테고리별) @ViewBuilder private var bundleGridContent: some View { switch viewModel.selectedCategory { case "카라비너": WorkshopGridBuilder.itemGridView( - items: viewModel.filteredCarabiners, + items: filteredCarabiners, isOwnedCheck: viewModel.isCarabinerOwned, router: router, viewModel: viewModel, @@ -42,7 +70,7 @@ struct WorkshopBundleGridView: View { ) case "배경": WorkshopGridBuilder.itemGridView( - items: viewModel.filteredBackgrounds, + items: filteredBackgrounds, isOwnedCheck: viewModel.isBackgroundOwned, router: router, viewModel: viewModel, diff --git a/Keychy/Keychy/Presentation/Workshop/Views/Components/WorkshopFilterBar.swift b/Keychy/Keychy/Presentation/Workshop/Views/Components/WorkshopFilterBar.swift index 9b0ae891..a10affed 100644 --- a/Keychy/Keychy/Presentation/Workshop/Views/Components/WorkshopFilterBar.swift +++ b/Keychy/Keychy/Presentation/Workshop/Views/Components/WorkshopFilterBar.swift @@ -14,16 +14,14 @@ struct WorkshopFilterBar: View { @Binding var viewModel: WorkshopViewModel var body: some View { - HStack(spacing: 8) { + HStack(spacing: 0) { // 정렬 버튼 (고정) sortButton - // 카테고리별 필터 (스크롤 가능) - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 8) { - categorySpecificFilters - } - } + Spacer() + + // 퀵 필터 버튼 (무료, 마이) + quickFilters } } @@ -51,7 +49,71 @@ struct WorkshopFilterBar: View { .buttonStyle(PlainButtonStyle()) } - /// 카테고리별 필터 옵션 + // MARK: - 퀵 필터 + /// 퀵 필터 버튼 그룹 + private var quickFilters: some View { + HStack(spacing: 15) { + ForEach(QuickFilter.allCases, id: \.self) { filter in + quickFilterButton(filter) + } + } + } + + /// 퀵 필터 선택 여부 + private func isSelected(_ filter: QuickFilter) -> Bool { + switch filter { + case .free: return viewModel.showFreeOnly + case .owned: return viewModel.showOwnedOnly + } + } + + /// 퀵 필터 토글 (라디오 버튼 스타일 - 하나만 선택 가능) + private func toggle(_ filter: QuickFilter) { + switch filter { + case .free: + viewModel.showFreeOnly.toggle() + if viewModel.showFreeOnly { + viewModel.showOwnedOnly = false + } + case .owned: + viewModel.showOwnedOnly.toggle() + if viewModel.showOwnedOnly { + viewModel.showFreeOnly = false + } + } + } + + /// 퀵 필터 버튼 + private func quickFilterButton(_ filter: QuickFilter) -> some View { + Button { + toggle(filter) + } label: { + HStack(spacing: 4) { + if let icon = filter.icon { + Image(icon) + } + + HStack(spacing: 5) { + Text(filter.title) + .typography(.suit14SB) + .foregroundColor(.gray500) + + Circle() + .fill(isSelected(filter) ? Color.main500 : Color.clear) + .stroke(.gray100, lineWidth: 1) + .frame(width: 16, height: 16) + .overlay { + if isSelected(filter) { + Image(.quickFilterChecked) + } + } + } + } + } + .buttonStyle(.plain) + } + + // MARK: - 카테고리별 필터 옵션, 현재 미사용 @ViewBuilder private var categorySpecificFilters: some View { switch viewModel.selectedCategory { diff --git a/Keychy/Keychy/Presentation/Workshop/Views/Keyring/WorkshopKeyringGridView.swift b/Keychy/Keychy/Presentation/Workshop/Views/Keyring/WorkshopKeyringGridView.swift index d63b8349..2aaa10fc 100644 --- a/Keychy/Keychy/Presentation/Workshop/Views/Keyring/WorkshopKeyringGridView.swift +++ b/Keychy/Keychy/Presentation/Workshop/Views/Keyring/WorkshopKeyringGridView.swift @@ -30,18 +30,31 @@ struct WorkshopKeyringGridView: View { /// 필터링된 템플릿 목록 private var filteredTemplates: [KeyringTemplate] { + var result: [KeyringTemplate] + + // 1. 카테고리 필터 switch viewModel.selectedCategory { case "전체": - return viewModel.filteredTemplates + result = viewModel.filteredTemplates case "이미지": - return viewModel.templates.filter { $0.tags.contains("이미지") } + result = viewModel.templates.filter { $0.tags.contains("이미지") } case "텍스트": - return viewModel.templates.filter { $0.tags.contains("텍스트") } + result = viewModel.templates.filter { $0.tags.contains("텍스트") } case "드로잉": - return viewModel.templates.filter { $0.tags.contains("드로잉") } + result = viewModel.templates.filter { $0.tags.contains("드로잉") } default: - return viewModel.filteredTemplates + result = viewModel.filteredTemplates + } + + // 2. 퀵 필터 적용 + if viewModel.showFreeOnly { + result = result.filter { $0.isFree } } + if viewModel.showOwnedOnly { + result = result.filter { viewModel.isTemplateOwned($0) } + } + + return result } /// 템플릿 그리드 콘텐츠 From e826b9a167f2d5c792eadd26d4d2c0152d7cbe14 Mon Sep 17 00:00:00 2001 From: giljihun Date: Sun, 25 Jan 2026 20:43:20 +0900 Subject: [PATCH 10/11] =?UTF-8?q?style:=20WorkshopView=20-=20=ED=82=A4?= =?UTF-8?q?=EB=A7=81=20=ED=83=AD=EC=9D=BC=EB=95=8C=EB=A7=8C=20=EC=B5=9C?= =?UTF-8?q?=EA=B7=BC=20=EC=82=AC=EC=9A=A9=20=ED=85=9C=ED=94=8C=EB=A6=BF=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Presentation/Workshop/Views/Main/WorkshopView.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView.swift b/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView.swift index 0bd6423f..c12da529 100644 --- a/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView.swift +++ b/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView.swift @@ -30,7 +30,6 @@ struct WorkshopView: View { @Environment(UserManager.self) var userManager @State var viewModel: WorkshopViewModel @State private var hasInitialized = false - @State private var isTabBarVisible = true // 만들기 메뉴 상태 @State var showMakeMenu: Bool = false @@ -71,7 +70,6 @@ struct WorkshopView: View { } } .ignoresSafeArea() - .toolbar(isTabBarVisible ? .visible : .hidden, for: .tabBar) .sheet(isPresented: $viewModel.showFilterSheet) { sortSheet } @@ -134,8 +132,10 @@ struct WorkshopView: View { Spacer() .frame(height: WorkshopLayout.recentTemplateTopSpacing) - // 최근 사용 템플릿 - recentTemplateSection + // 키링 탭일 때만 최근 사용 템플릿 표시 + if viewModel.workshopToggle { + recentTemplateSection + } Spacer() .frame(height: WorkshopLayout.mainContentTopSpacing) From b5b38f23d7583b518da44c6339c566b021f1ef7d Mon Sep 17 00:00:00 2001 From: giljihun Date: Mon, 26 Jan 2026 00:23:49 +0900 Subject: [PATCH 11/11] =?UTF-8?q?style:=20=ED=83=80=EC=9D=B4=ED=8B=80=20?= =?UTF-8?q?=EA=B7=B8=EB=A0=88=EC=9D=B4=20100=20->=20200?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Workshop/Views/Main/WorkshopView+TopSection.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView+TopSection.swift b/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView+TopSection.swift index ae203e7a..b6152782 100644 --- a/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView+TopSection.swift +++ b/Keychy/Keychy/Presentation/Workshop/Views/Main/WorkshopView+TopSection.swift @@ -43,7 +43,7 @@ extension WorkshopView { } label: { Text("키링") .typography(.nanum24EB) - .foregroundStyle(viewModel.workshopToggle ? .black100 : .gray100) + .foregroundStyle(viewModel.workshopToggle ? .black100 : .gray200) } Button { @@ -51,7 +51,7 @@ extension WorkshopView { } label: { Text("뭉치") .typography(.nanum24EB) - .foregroundStyle(viewModel.workshopToggle ? .gray100 : .black100) + .foregroundStyle(viewModel.workshopToggle ? .gray200 : .black100) } } }