From f9d448c25d43a5ab697327d4f09bea3d3c8c5bb4 Mon Sep 17 00:00:00 2001 From: Roy Date: Sun, 17 May 2026 22:18:19 +0900 Subject: [PATCH 01/21] =?UTF-8?q?feat:=20#4=20=EC=82=AC=EC=A0=84=20?= =?UTF-8?q?=ED=88=AC=ED=91=9C=EC=B0=BD=20=EB=8F=84=EC=9E=85=20=E2=80=94=20?= =?UTF-8?q?Entity=20=C2=B7=20Feature/View=20=C2=B7=20=EC=BD=94=EB=94=94?= =?UTF-8?q?=EB=84=A4=EC=9D=B4=ED=84=B0=20=EB=9D=BC=EC=9A=B0=ED=8C=85=20+?= =?UTF-8?q?=20=EA=B3=B5=ED=86=B5=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=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 - PreVoteBattle / PreVoteOption / PhilosopherAvatar Entity + .mock - PreVote 폴더 (Reducer · View) — 2지선다 옵션 카드 + VS 뱃지 + Pretendard 24pt 타이틀 + 그라데이션 콘텐츠 영역, 탭바 hidden - HomeCoordinator 라우터에 .preVote 푸시 / dismiss·submit → backAction 위임 - DesignSystem: 철학자 아바타 자산 (플라톤·사르트르·순자) + ImageAsset case 추가 - DesignSystem 공통 컴포넌트: PickeNavigationBar (좌측 back · 가운데 옵션 · 우측 ViewBuilder), 시스템 공유용 ShareSheet (UIActivityViewController wrapper), CustomButton height override (CTAButtonSize 기본값 + 명시적 height) - AGENTS.md: Coordinator 의 `extension X { @Reducer public enum XScreen { ... } }` 구조 절대 건드리지 말 것 규칙 명시 --- AGENTS.md | 35 +++ .../Entity/Sources/Home/PreVoteBattle.swift | 79 ++++++ .../Coordinator/Reducer/HomeCoordinator.swift | 18 +- .../View/HomeCoordinatorView.swift | 3 + .../Sources/Vote/Reducer/PreVoteFeature.swift | 141 ++++++++++ .../Home/Sources/Vote/View/PreVoteView.swift | 248 ++++++++++++++++++ .../ImageAssets.xcassets/Avatar/Contents.json | 6 + .../Avatar/avatarPlato.imageset/Contents.json | 12 + .../avatarPlato.imageset/avatarPlato.png | Bin 0 -> 7570 bytes .../avatarSartre.imageset/Contents.json | 12 + .../avatarSartre.imageset/avatarSartre.png | Bin 0 -> 8614 bytes .../Avatar/avatarSunja.imageset/Contents.json | 21 ++ .../avatarSunja.imageset/avatarSunja.png | Bin 0 -> 7408 bytes .../GNB/tabExplore.imageset/Contents.json | 5 +- .../GNB/tabExplore.imageset/Union.png | Bin 722 -> 0 bytes .../GNB/tabExplore.imageset/tabExplore.svg | 3 + .../tabExploreActive.imageset/Contents.json | 15 ++ .../tabExploreActive.svg | 3 + .../GNB/tabHome.imageset/Contents.json | 5 +- .../GNB/tabHome.imageset/Icon.svg | 3 - .../GNB/tabHome.imageset/tabHome.svg | 3 + .../GNB/tabHomeActive.imageset/Contents.json | 15 ++ .../tabHomeActive.imageset/tabHomeActive.svg | 3 + .../GNB/tabMyPage.imageset/Contents.json | 5 +- .../GNB/tabMyPage.imageset/Group 27.svg | 4 - .../GNB/tabMyPage.imageset/tabMyPage.svg | 4 + .../tabMyPageActive.imageset/Contents.json | 15 ++ .../tabMyPageActive.svg | 4 + .../GNB/tabQuickBattle.imageset/Contents.json | 5 +- .../tabQuickBattle.svg | 3 + .../Contents.json | 15 ++ .../tabQuickBattleActive.svg} | 0 .../onboarding1.imageset/Contents.json | 2 +- .../\354\235\264\353\257\270\354\247\200.png" | Bin 0 -> 22336 bytes .../\354\235\264\353\257\270\354\247\200.svg" | 61 ----- .../onboarding3.imageset/Contents.json | 2 +- ...354\235\264\353\257\270\354\247\200 1.png" | Bin 0 -> 10573 bytes .../\354\235\264\353\257\270\354\247\200.svg" | 40 --- .../onboarding4.imageset/Contents.json | 2 +- .../\354\235\264\353\257\270\354\247\200.png" | Bin 0 -> 14538 bytes .../\354\235\264\353\257\270\354\247\200.svg" | 45 ---- .../Sources/Image/ImageAsset.swift | 18 +- .../UI/Button/CustomButtonConfig.swift | 10 +- .../UI/Navigaion/PickeNavigationBar.swift | 76 ++++++ .../Sources/UI/Share/ShareSheet.swift | 23 ++ 45 files changed, 795 insertions(+), 169 deletions(-) create mode 100644 Projects/Domain/Entity/Sources/Home/PreVoteBattle.swift create mode 100644 Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift create mode 100644 Projects/Presentation/Home/Sources/Vote/View/PreVoteView.swift create mode 100644 Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/Avatar/Contents.json create mode 100644 Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/Avatar/avatarPlato.imageset/Contents.json create mode 100644 Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/Avatar/avatarPlato.imageset/avatarPlato.png create mode 100644 Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/Avatar/avatarSartre.imageset/Contents.json create mode 100644 Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/Avatar/avatarSartre.imageset/avatarSartre.png create mode 100644 Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/Avatar/avatarSunja.imageset/Contents.json create mode 100644 Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/Avatar/avatarSunja.imageset/avatarSunja.png delete mode 100644 Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabExplore.imageset/Union.png create mode 100644 Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabExplore.imageset/tabExplore.svg create mode 100644 Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabExploreActive.imageset/Contents.json create mode 100644 Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabExploreActive.imageset/tabExploreActive.svg delete mode 100644 Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabHome.imageset/Icon.svg create mode 100644 Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabHome.imageset/tabHome.svg create mode 100644 Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabHomeActive.imageset/Contents.json create mode 100644 Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabHomeActive.imageset/tabHomeActive.svg delete mode 100644 Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabMyPage.imageset/Group 27.svg create mode 100644 Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabMyPage.imageset/tabMyPage.svg create mode 100644 Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabMyPageActive.imageset/Contents.json create mode 100644 Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabMyPageActive.imageset/tabMyPageActive.svg create mode 100644 Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabQuickBattle.imageset/tabQuickBattle.svg create mode 100644 Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabQuickBattleActive.imageset/Contents.json rename Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/{tabQuickBattle.imageset/Icon.svg => tabQuickBattleActive.imageset/tabQuickBattleActive.svg} (100%) create mode 100644 "Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/OnBoarding/onboarding1.imageset/\354\235\264\353\257\270\354\247\200.png" delete mode 100644 "Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/OnBoarding/onboarding1.imageset/\354\235\264\353\257\270\354\247\200.svg" create mode 100644 "Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/OnBoarding/onboarding3.imageset/\354\235\264\353\257\270\354\247\200 1.png" delete mode 100644 "Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/OnBoarding/onboarding3.imageset/\354\235\264\353\257\270\354\247\200.svg" create mode 100644 "Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/OnBoarding/onboarding4.imageset/\354\235\264\353\257\270\354\247\200.png" delete mode 100644 "Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/OnBoarding/onboarding4.imageset/\354\235\264\353\257\270\354\247\200.svg" create mode 100644 Projects/Shared/DesignSystem/Sources/UI/Navigaion/PickeNavigationBar.swift create mode 100644 Projects/Shared/DesignSystem/Sources/UI/Share/ShareSheet.swift diff --git a/AGENTS.md b/AGENTS.md index c6522af..7ae0966 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -414,6 +414,41 @@ public init( - `MoyaProviderPool` 은 더 이상 RepositoryImpl 에서 직접 호출하지 않는다 (필요 시 풀 자체에서 내부적으로 캐시 처리) - 레퍼런스: `HomeRepositoryImpl`, `AuthRepositoryImpl`, AsyncMoya `MoyaProvider+Factory.default`, `Extension+MoyaProvider+Auth.authorized` +#### 🧭 Coordinator — `extension X { @Reducer public enum XScreen { ... } }` 구조 절대 건드리지 말 것 + +TCAFlow 기반 Coordinator 의 `XScreen` 정의는 반드시 **별도 extension 의 `@Reducer public enum`** 형태를 유지한다. 마이그레이션 / 리팩터링 / 자동 포맷터 어떤 이유로도 이 구조를 본체 안으로 끌어들이거나 `enum`을 `struct`/`Reducer` 로 바꾸지 말 것. + +```swift +// ✅ 올바른 패턴 — extension + @Reducer + public enum + State: Equatable 보조 extension +@FlowCoordinator(screen: "HomeScreen", navigation: true) +public struct HomeCoordinator { + // … State / Action / handleRoute … +} + +extension HomeCoordinator { + @Reducer + public enum HomeScreen { + case home(HomeFeature) + case preVote(PreVoteFeature) + } +} + +extension HomeCoordinator.HomeScreen.State: Equatable {} + +// ❌ 금지 — Coordinator 본체 안에 enum 을 인라인 선언 +public struct HomeCoordinator { + @Reducer + public enum HomeScreen { ... } // 안 됨 (매크로 인식 / Route 추론 깨짐) +} +``` + +규칙: +- `XScreen` 은 `@Reducer public enum`. struct 로 바꾸지 말 것 +- 본체와 분리된 **별도 extension** 안에 선언 +- `extension Coordinator.XScreen.State: Equatable {}` 보조 conformance 도 같이 유지 (Route diff 비교 필요) +- 라우터 핸들러 (`routerAction`) 안에서 `state.routes.push/pop/goBack` 직접 호출은 OK, 단 `dismiss`/`submit` 같이 반복되는 종료 액션은 `.send(.view(.backAction))` 으로 일원화 +- 레퍼런스: `HomeCoordinator`, `AuthCoordinator`, `MainTabCoordinator` + ### 📏 Swift 코딩 규칙 (`docs/agent/swift-coding-rules.md`) - Swift 스타일 가이드 - 에러 처리 패턴 diff --git a/Projects/Domain/Entity/Sources/Home/PreVoteBattle.swift b/Projects/Domain/Entity/Sources/Home/PreVoteBattle.swift new file mode 100644 index 0000000..4fa840f --- /dev/null +++ b/Projects/Domain/Entity/Sources/Home/PreVoteBattle.swift @@ -0,0 +1,79 @@ +// +// PreVoteBattle.swift +// Entity +// +// Created by Wonji Suh on 5/17/26. +// + +import Foundation + +/// 사전 투표창 (.pen `U5WO4`) 에 표시되는 배틀 모델. +/// 홈 카드의 `VoteQuestion` 과 달리 2지선다 + 철학자 아바타 기반. +public struct PreVoteBattle: Equatable, Identifiable { + public let battleId: Int + public let backgroundImageURL: String? + public let tags: [String] + public let titleLine1: String + public let titleLine2: String + public let summary: String + public let leftOption: PreVoteOption + public let rightOption: PreVoteOption + + public var id: Int { battleId } + + public init( + battleId: Int, + backgroundImageURL: String?, + tags: [String], + titleLine1: String, + titleLine2: String, + summary: String, + leftOption: PreVoteOption, + rightOption: PreVoteOption + ) { + self.battleId = battleId + self.backgroundImageURL = backgroundImageURL + self.tags = tags + self.titleLine1 = titleLine1 + self.titleLine2 = titleLine2 + self.summary = summary + self.leftOption = leftOption + self.rightOption = rightOption + } +} + +public struct PreVoteOption: Equatable, Identifiable, Hashable { + public let philosopher: PhilosopherAvatar + public let stance: String + + public var id: String { philosopher.rawValue } + + public init(philosopher: PhilosopherAvatar, stance: String) { + self.philosopher = philosopher + self.stance = stance + } +} + +/// 사전 투표창 / 새로운 배틀 카드에서 사용되는 철학자 아바타. raw value 는 화면 표시 이름. +public enum PhilosopherAvatar: String, CaseIterable, Equatable, Hashable { + case plato = "플라톤" + case sartre = "사르트르" + case sunja = "순자" +} + +public extension PreVoteBattle { + static let mock = PreVoteBattle( + battleId: 41, + backgroundImageURL: "https://picsum.photos/seed/picke-prevote/750/1024", + tags: ["#예술", "#현대미술"], + titleLine1: "뒤샹의 변기,", + titleLine2: "예술인가 도발인가", + summary: """ + 누군가는 이것을 화장실의 부속품이라 부르고, + 누군가는 현대 미술의 혁명이라고 부릅니다. + 과연 이 변기의 '진짜 모습'은 무엇일까요? + """, + leftOption: .init(philosopher: .plato, stance: "변기는 변기다"), + rightOption: .init(philosopher: .sartre, stance: "예술이다") + ) +} diff --git a/Projects/Presentation/Home/Sources/Coordinator/Reducer/HomeCoordinator.swift b/Projects/Presentation/Home/Sources/Coordinator/Reducer/HomeCoordinator.swift index 12d2f8a..61bee05 100644 --- a/Projects/Presentation/Home/Sources/Coordinator/Reducer/HomeCoordinator.swift +++ b/Projects/Presentation/Home/Sources/Coordinator/Reducer/HomeCoordinator.swift @@ -56,10 +56,21 @@ public struct HomeCoordinator { extension HomeCoordinator { private func routerAction( - state _: inout State, - action _: IndexedRouterActionOf + state: inout State, + action: IndexedRouterActionOf ) -> Effect { - .none + switch action { + case .routeAction(_, action: .home(.delegate(.presentPreVote))): + state.routes.push(.preVote(.init())) + return .none + + case .routeAction(_, action: .preVote(.delegate(.dismiss))), + .routeAction(_, action: .preVote(.delegate(.submit))): + return .send(.view(.backAction)) + + default: + return .none + } } private func handleViewAction( @@ -81,6 +92,7 @@ extension HomeCoordinator { @Reducer public enum HomeScreen { case home(HomeFeature) + case preVote(PreVoteFeature) } } diff --git a/Projects/Presentation/Home/Sources/Coordinator/View/HomeCoordinatorView.swift b/Projects/Presentation/Home/Sources/Coordinator/View/HomeCoordinatorView.swift index d95542f..a109a6d 100644 --- a/Projects/Presentation/Home/Sources/Coordinator/View/HomeCoordinatorView.swift +++ b/Projects/Presentation/Home/Sources/Coordinator/View/HomeCoordinatorView.swift @@ -24,6 +24,9 @@ public struct HomeCoordinatorView: View { switch screen.case { case let .home(homeStore): HomeView(store: homeStore) + case let .preVote(preVoteStore): + PreVoteView(store: preVoteStore) + .toolbar(.hidden, for: .tabBar) } } } diff --git a/Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift b/Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift new file mode 100644 index 0000000..489ccd3 --- /dev/null +++ b/Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift @@ -0,0 +1,141 @@ +// +// PreVoteFeature.swift +// Home +// +// Created by Wonji Suh on 5/16/26. +// + +import Foundation + +import ComposableArchitecture +import Entity + +@Reducer +public struct PreVoteFeature { + public init() {} + + @ObservableState + public struct State: Equatable { + public var battle: PreVoteBattle = .mock + public var selectedSide: PhilosopherAvatar? + public var isSubmitting: Bool = false + public var shareItem: ShareItem? + + public var isPrimaryButtonEnabled: Bool { + selectedSide != nil && !isSubmitting + } + + public init() {} + } + + /// 공유 시트 트리거. `.sheet(item:)` 에 바로 바인딩. + public struct ShareItem: Equatable, Identifiable { + public let id: UUID + public let items: [String] + + public init(id: UUID = UUID(), items: [String]) { + self.id = id + self.items = items + } + } + + public enum Action: ViewAction, BindableAction { + case binding(BindingAction) + case view(View) + case async(AsyncAction) + case inner(InnerAction) + case delegate(DelegateAction) + } + + @CasePathable + public enum View { + case backButtonTapped + case shareTapped + case optionTapped(PhilosopherAvatar) + case primaryButtonTapped + } + + public enum AsyncAction: Equatable {} + public enum InnerAction: Equatable {} + + public enum DelegateAction: Equatable { + case dismiss + case submit(battleId: Int, side: PhilosopherAvatar) + } + + public var body: some Reducer { + BindingReducer() + Reduce { state, action in + switch action { + case .binding: + .none + + case let .view(viewAction): + handleViewAction(state: &state, action: viewAction) + + case let .async(asyncAction): + handleAsyncAction(state: &state, action: asyncAction) + + case let .inner(innerAction): + handleInnerAction(state: &state, action: innerAction) + + case let .delegate(delegateAction): + handleDelegateAction(state: &state, action: delegateAction) + } + } + } +} + +extension PreVoteFeature { + private func handleViewAction( + state: inout State, + action: View + ) -> Effect { + switch action { + case .backButtonTapped: + return .send(.delegate(.dismiss)) + + case .shareTapped: + state.shareItem = ShareItem( + items: [ + "\(state.battle.titleLine1) \(state.battle.titleLine2)", + "https://picke.store/battle/\(state.battle.battleId)", + ] + ) + return .none + + case let .optionTapped(side): + state.selectedSide = (state.selectedSide == side) ? nil : side + return .none + + case .primaryButtonTapped: + guard let side = state.selectedSide else { return .none } + state.isSubmitting = true + return .send(.delegate(.submit(battleId: state.battle.battleId, side: side))) + } + } + + private func handleAsyncAction( + state _: inout State, + action: AsyncAction + ) -> Effect { + switch action {} + } + + private func handleInnerAction( + state _: inout State, + action: InnerAction + ) -> Effect { + switch action {} + } + + private func handleDelegateAction( + state _: inout State, + action: DelegateAction + ) -> Effect { + switch action { + case .dismiss, .submit: + .none + } + } +} diff --git a/Projects/Presentation/Home/Sources/Vote/View/PreVoteView.swift b/Projects/Presentation/Home/Sources/Vote/View/PreVoteView.swift new file mode 100644 index 0000000..0f5ed92 --- /dev/null +++ b/Projects/Presentation/Home/Sources/Vote/View/PreVoteView.swift @@ -0,0 +1,248 @@ +// +// PreVoteView.swift +// Home +// +// Created by Wonji Suh on 5/16/26. +// + +import SwiftUI + +import ComposableArchitecture +import DesignSystem +import Entity +import Kingfisher + +@ViewAction(for: PreVoteFeature.self) +public struct PreVoteView: View { + @Bindable public var store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + ZStack(alignment: .top) { + backgroundImage + + VStack(spacing: 0) { + navigationBar + Spacer(minLength: 0) + contentArea + } + } + .background(Color.beige50.ignoresSafeArea()) + .navigationBarHidden(true) + .toolbar(.hidden, for: .navigationBar) + .toolbar(.hidden, for: .tabBar) + .sheet(item: $store.shareItem) { item in + ShareSheet(items: item.items) + .presentationDetents([.fraction(0.6)]) + .toolbar(.hidden, for: .navigationBar) + } + } +} + +// MARK: - Background + +extension PreVoteView { + private var backgroundImage: some View { + ZStack { + if let urlString = store.battle.backgroundImageURL, + let url = URL(string: urlString) + { + KFImage(url) + .placeholder { Color.neutral200 } + .resizable() + .scaledToFill() + } else { + Color.neutral200 + } + + Color.black.opacity(0.4) + } + .frame(height: 512) + .clipped() + .frame(maxWidth: .infinity, alignment: .top) + .ignoresSafeArea(edges: .top) + } +} + +// MARK: - Navigation bar + +extension PreVoteView { + private var navigationBar: some View { + PickeNavigationBar( + onBack: { send(.backButtonTapped) } + ) { + Button { send(.shareTapped) } label: { + Image(systemName: "square.and.arrow.up") + .font(.system(size: 16, weight: .semibold)) + .frame(width: 24, height: 24) + } + .buttonStyle(.plain) + } + .foregroundStyle(.beige50) + } +} + +// MARK: - Content (gradient + 카피 + 선택지 + CTA) + +extension PreVoteView { + private var contentArea: some View { + VStack(spacing: 40) { + contentSection + optionSection + primaryButton + } + .padding(.horizontal, 16) + .padding(.top, 80) + .padding(.bottom, 40) + .background( + LinearGradient( + stops: [ + .init(color: Color.beige50.opacity(0), location: 0), + .init(color: .beige50, location: 0.35), + .init(color: .beige50, location: 1), + ], + startPoint: .top, + endPoint: .bottom + ) + ) + .ignoresSafeArea(edges: .bottom) + } + + private var contentSection: some View { + VStack(alignment: .leading, spacing: 12) { + VStack(alignment: .leading, spacing: 20) { + tagsRow + titleText + } + summaryText + } + .frame(maxWidth: .infinity, alignment: .leading) + } + + private var tagsRow: some View { + HStack(spacing: 9) { + ForEach(store.battle.tags, id: \.self) { tag in + Text(tag) + .pretendardFont(family: .SemiBold, size: 12) + .foregroundStyle(.primary500) + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background(.beige600, in: RoundedRectangle(cornerRadius: 2)) + } + } + } + + private var titleText: some View { + Text("\(store.battle.titleLine1)\n\(store.battle.titleLine2)") + .pretendardFont(family: .Bold, size: 24) + .foregroundStyle(.neutral500) + .kerning(-0.6) + .lineSpacing(24 * 0.4) + .fixedSize(horizontal: false, vertical: true) + .frame(maxWidth: .infinity, alignment: .leading) + } + + private var summaryText: some View { + Text(store.battle.summary) + .pretendardFont(family: .Regular, size: 13) + .foregroundStyle(.neutral400) + .lineSpacing(13 * 0.4) + .fixedSize(horizontal: false, vertical: true) + .frame(maxWidth: .infinity, alignment: .leading) + } +} + +// MARK: - 선택지 + +extension PreVoteView { + private var optionSection: some View { + ZStack { + HStack(spacing: 8) { + optionCard(store.battle.leftOption) + optionCard(store.battle.rightOption) + } + vsBadge + } + } + + private func optionCard(_ option: PreVoteOption) -> some View { + let isSelected = store.selectedSide == option.philosopher + + return Button { + send(.optionTapped(option.philosopher)) + } label: { + VStack(spacing: 12) { + avatarView(option.philosopher) + + VStack(spacing: 2) { + Text(option.stance) + .pretendardFont(family: .SemiBold, size: 14) + .foregroundStyle(.neutral600) + .kerning(-0.35) + .multilineTextAlignment(.center) + + Text(option.philosopher.rawValue) + .pretendardFont(family: .Medium, size: 12) + .foregroundStyle(.neutral300) + .multilineTextAlignment(.center) + } + } + .frame(maxWidth: .infinity) + .padding(8) + .background(.beige300, in: RoundedRectangle(cornerRadius: 2)) + .overlay( + RoundedRectangle(cornerRadius: 2) + .stroke(isSelected ? .primary500 : .beige600, lineWidth: isSelected ? 1.5 : 1) + ) + .opacity(isSelected ? 1.0 : 0.88) + } + .buttonStyle(.plain) + } + + private func avatarView(_ philosopher: PhilosopherAvatar) -> some View { + let asset: ImageAsset = switch philosopher { + case .plato: .avatarPlato + case .sartre: .avatarSartre + case .sunja: .avatarSunja + } + + return Image(asset: asset) + .resizable() + .scaledToFit() + .frame(width: 40, height: 40) + .background(.beige600, in: Circle()) + } + + private var vsBadge: some View { + Text("VS") + .pretendardFont(family: .Bold, size: 11) + .foregroundStyle(.neutral800) + .frame(width: 28, height: 28) + .background(.secondary200, in: Circle()) + .overlay(Circle().stroke(.beige50, lineWidth: 1.5)) + } +} + +// MARK: - CTA + +extension PreVoteView { + private var primaryButton: some View { + CustomButton( + action: { send(.primaryButtonTapped) }, + title: "사전 투표하기", + config: CustomButtonConfig.primary(.large, height: 52), + isEnable: store.isPrimaryButtonEnabled + ) + } +} + +#Preview { + PreVoteView( + store: Store(initialState: PreVoteFeature.State()) { + PreVoteFeature() + } + ) +} diff --git a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/Avatar/Contents.json b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/Avatar/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/Avatar/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/Avatar/avatarPlato.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/Avatar/avatarPlato.imageset/Contents.json new file mode 100644 index 0000000..7659a81 --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/Avatar/avatarPlato.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "avatarPlato.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/Avatar/avatarPlato.imageset/avatarPlato.png b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/Avatar/avatarPlato.imageset/avatarPlato.png new file mode 100644 index 0000000000000000000000000000000000000000..ebc2951563a1f598267d7c0760109f7daea5ed13 GIT binary patch literal 7570 zcmWkz1yqx56dnSjyQC2$q`O0qknTq5Zs`iZbj0`r+6s&JeYx0p{|9JEMV((N$iAT0cZQ)j8b0d4$mTvpC zeQXYCupPd|r9Eoc4jj1-RL6OGRh$%A7~)#VMFuA=#xaOmIqY5|f6E!q96SO(Lgs<% z(#bRRxn|xlkbk8p6wG5SPQnz*`*Jg-MqFJRn%R#E1CfRB!41>t+ALQEUvD>%89;Pq zUp#fA9D*i=5=89bhB2#fk#xEdz61`@(?v{n%zGw$Y|mg^=O8c@njrFmb(r|VB-nnv z;jRFhgo%|G$Fk=bk_>eoPY5*2Wx0}0r{m(Tk*3JO3e{DT!=b~!dFk6fd8Lz>2&Ye{ zd!Cy7X668V(v1aYF=HBCOsahkCo#h~iBgG|5=bUsA_Cm;L~TVCSGE3

-vUf^k+* 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 N6y?-pVKQc6{{hll(P97q literal 0 HcmV?d00001 diff --git a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/Avatar/avatarSartre.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/Avatar/avatarSartre.imageset/Contents.json new file mode 100644 index 0000000..8be55fb --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/Avatar/avatarSartre.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "avatarSartre.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/Avatar/avatarSartre.imageset/avatarSartre.png b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/Avatar/avatarSartre.imageset/avatarSartre.png new file mode 100644 index 0000000000000000000000000000000000000000..54c35ec9188907c6c475a2566d72f1aaf9daa792 GIT binary patch literal 8614 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 aFB`9O^djHX_5lkmK=RTmQWX+rVgCc0MCT&_ literal 0 HcmV?d00001 diff --git a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/Avatar/avatarSunja.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/Avatar/avatarSunja.imageset/Contents.json new file mode 100644 index 0000000..2ff5c82 --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/Avatar/avatarSunja.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "avatarSunja.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/Avatar/avatarSunja.imageset/avatarSunja.png b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/Avatar/avatarSunja.imageset/avatarSunja.png new file mode 100644 index 0000000000000000000000000000000000000000..af91a7440b85215ba4a7d6d308153f0c1e156e29 GIT binary patch literal 7408 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 Ef;FK(761SM diff --git a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabExplore.imageset/tabExplore.svg b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabExplore.imageset/tabExplore.svg new file mode 100644 index 0000000..98e1613 --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabExplore.imageset/tabExplore.svg @@ -0,0 +1,3 @@ + + + diff --git a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabExploreActive.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabExploreActive.imageset/Contents.json new file mode 100644 index 0000000..8806eb6 --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabExploreActive.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "tabExploreActive.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabExploreActive.imageset/tabExploreActive.svg b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabExploreActive.imageset/tabExploreActive.svg new file mode 100644 index 0000000..e9174b2 --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabExploreActive.imageset/tabExploreActive.svg @@ -0,0 +1,3 @@ + + + diff --git a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabHome.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabHome.imageset/Contents.json index 1c2ff97..cab2f1b 100644 --- a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabHome.imageset/Contents.json +++ b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabHome.imageset/Contents.json @@ -1,12 +1,15 @@ { "images" : [ { - "filename" : "Icon.svg", + "filename" : "tabHome.svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true } } diff --git a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabHome.imageset/Icon.svg b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabHome.imageset/Icon.svg deleted file mode 100644 index 5523ab2..0000000 --- a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabHome.imageset/Icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabHome.imageset/tabHome.svg b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabHome.imageset/tabHome.svg new file mode 100644 index 0000000..0d2bc07 --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabHome.imageset/tabHome.svg @@ -0,0 +1,3 @@ + + + diff --git a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabHomeActive.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabHomeActive.imageset/Contents.json new file mode 100644 index 0000000..30d363c --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabHomeActive.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "tabHomeActive.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabHomeActive.imageset/tabHomeActive.svg b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabHomeActive.imageset/tabHomeActive.svg new file mode 100644 index 0000000..5e8ae77 --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabHomeActive.imageset/tabHomeActive.svg @@ -0,0 +1,3 @@ + + + diff --git a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabMyPage.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabMyPage.imageset/Contents.json index 5cf58fd..e115f5e 100644 --- a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabMyPage.imageset/Contents.json +++ b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabMyPage.imageset/Contents.json @@ -1,12 +1,15 @@ { "images" : [ { - "filename" : "Group 27.svg", + "filename" : "tabMyPage.svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true } } diff --git a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabMyPage.imageset/Group 27.svg b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabMyPage.imageset/Group 27.svg deleted file mode 100644 index 82f9d72..0000000 --- a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabMyPage.imageset/Group 27.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabMyPage.imageset/tabMyPage.svg b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabMyPage.imageset/tabMyPage.svg new file mode 100644 index 0000000..16ca7ef --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabMyPage.imageset/tabMyPage.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabMyPageActive.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabMyPageActive.imageset/Contents.json new file mode 100644 index 0000000..86a48ec --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabMyPageActive.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "tabMyPageActive.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabMyPageActive.imageset/tabMyPageActive.svg b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabMyPageActive.imageset/tabMyPageActive.svg new file mode 100644 index 0000000..2fb9b1c --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabMyPageActive.imageset/tabMyPageActive.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabQuickBattle.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabQuickBattle.imageset/Contents.json index 1c2ff97..8ff80d6 100644 --- a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabQuickBattle.imageset/Contents.json +++ b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabQuickBattle.imageset/Contents.json @@ -1,12 +1,15 @@ { "images" : [ { - "filename" : "Icon.svg", + "filename" : "tabQuickBattle.svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true } } diff --git a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabQuickBattle.imageset/tabQuickBattle.svg b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabQuickBattle.imageset/tabQuickBattle.svg new file mode 100644 index 0000000..27b376a --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabQuickBattle.imageset/tabQuickBattle.svg @@ -0,0 +1,3 @@ + + + diff --git a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabQuickBattleActive.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabQuickBattleActive.imageset/Contents.json new file mode 100644 index 0000000..e567445 --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabQuickBattleActive.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "tabQuickBattleActive.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabQuickBattle.imageset/Icon.svg b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabQuickBattleActive.imageset/tabQuickBattleActive.svg similarity index 100% rename from Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabQuickBattle.imageset/Icon.svg rename to Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabQuickBattleActive.imageset/tabQuickBattleActive.svg diff --git a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/OnBoarding/onboarding1.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/OnBoarding/onboarding1.imageset/Contents.json index a1a7abc..4955878 100644 --- a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/OnBoarding/onboarding1.imageset/Contents.json +++ b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/OnBoarding/onboarding1.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "이미지.svg", + "filename" : "이미지.png", "idiom" : "universal" } ], diff --git "a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/OnBoarding/onboarding1.imageset/\354\235\264\353\257\270\354\247\200.png" "b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/OnBoarding/onboarding1.imageset/\354\235\264\353\257\270\354\247\200.png" new file mode 100644 index 0000000000000000000000000000000000000000..4b99e808281f9633947d451138674df57af4c35b GIT binary patch literal 22336 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 DZuaC$ literal 0 HcmV?d00001 diff --git "a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/OnBoarding/onboarding1.imageset/\354\235\264\353\257\270\354\247\200.svg" "b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/OnBoarding/onboarding1.imageset/\354\235\264\353\257\270\354\247\200.svg" deleted file mode 100644 index 32f7c27..0000000 --- "a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/OnBoarding/onboarding1.imageset/\354\235\264\353\257\270\354\247\200.svg" +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/OnBoarding/onboarding3.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/OnBoarding/onboarding3.imageset/Contents.json index a1a7abc..31857c8 100644 --- a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/OnBoarding/onboarding3.imageset/Contents.json +++ b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/OnBoarding/onboarding3.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "이미지.svg", + "filename" : "이미지 1.png", "idiom" : "universal" } ], diff --git "a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/OnBoarding/onboarding3.imageset/\354\235\264\353\257\270\354\247\200 1.png" "b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/OnBoarding/onboarding3.imageset/\354\235\264\353\257\270\354\247\200 1.png" new file mode 100644 index 0000000000000000000000000000000000000000..4d1a99263d771dbebbb05a563a6769bb674d1cb6 GIT binary patch literal 10573 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 KDq8_I4g3!`9Bb_W literal 0 HcmV?d00001 diff --git "a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/OnBoarding/onboarding3.imageset/\354\235\264\353\257\270\354\247\200.svg" "b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/OnBoarding/onboarding3.imageset/\354\235\264\353\257\270\354\247\200.svg" deleted file mode 100644 index 8c0375e..0000000 --- "a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/OnBoarding/onboarding3.imageset/\354\235\264\353\257\270\354\247\200.svg" +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/OnBoarding/onboarding4.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/OnBoarding/onboarding4.imageset/Contents.json index a1a7abc..4955878 100644 --- a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/OnBoarding/onboarding4.imageset/Contents.json +++ b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/OnBoarding/onboarding4.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "이미지.svg", + "filename" : "이미지.png", "idiom" : "universal" } ], diff --git "a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/OnBoarding/onboarding4.imageset/\354\235\264\353\257\270\354\247\200.png" "b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/OnBoarding/onboarding4.imageset/\354\235\264\353\257\270\354\247\200.png" new file mode 100644 index 0000000000000000000000000000000000000000..fa6e43acce5c2367cf853e53b40be2e3ec580313 GIT binary patch literal 14538 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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Projects/Shared/DesignSystem/Sources/Image/ImageAsset.swift b/Projects/Shared/DesignSystem/Sources/Image/ImageAsset.swift index 210ecb2..c19d69a 100644 --- a/Projects/Shared/DesignSystem/Sources/Image/ImageAsset.swift +++ b/Projects/Shared/DesignSystem/Sources/Image/ImageAsset.swift @@ -25,17 +25,27 @@ public enum ImageAsset: String { case onboarding3 case onboarding4 - // MARK: - GNB 탭 아이콘 + // MARK: - GNB 탭 아이콘 (비선택 / 선택) case tabHome + case tabHomeActive case tabExplore + case tabExploreActive case tabQuickBattle + case tabQuickBattleActive case tabMyPage - - - //MARK: - Home 탭 아이콘 + case tabMyPageActive + + // MARK: - Home 탭 아이콘 + case appLogo case bell + // MARK: - 철학자 아바타 + + case avatarPlato + case avatarSartre + case avatarSunja + case none } diff --git a/Projects/Shared/DesignSystem/Sources/UI/Button/CustomButtonConfig.swift b/Projects/Shared/DesignSystem/Sources/UI/Button/CustomButtonConfig.swift index 0c6fe5d..04d2861 100644 --- a/Projects/Shared/DesignSystem/Sources/UI/Button/CustomButtonConfig.swift +++ b/Projects/Shared/DesignSystem/Sources/UI/Button/CustomButtonConfig.swift @@ -14,13 +14,19 @@ public class CustomButtonConfig: PickeCustomButtonConfig { } /// CTA primary 팩토리. variant + size 조합을 `PickeCustomButtonConfig`로 변환한다. - public static func primary(_ size: CTAButtonSize) -> PickeCustomButtonConfig { + /// - Parameters: + /// - size: `CTAButtonSize` (기본 height 사용) + /// - height: 호출처에서 size.height 를 override 하고 싶을 때 명시 (e.g. .pen 디자인의 52pt) + public static func primary( + _ size: CTAButtonSize, + height: CGFloat? = nil + ) -> PickeCustomButtonConfig { let variant: CTAButtonVariant = .primary return PickeCustomButtonConfig( cornerRadius: .default, enableFontColor: variant.foregroundColor(isEnabled: true), enableBackgroundColor: variant.backgroundColor(isEnabled: true), - frameHeight: size.height, + frameHeight: height ?? size.height, disableFontColor: variant.foregroundColor(isEnabled: false), disableBackgroundColor: variant.backgroundColor(isEnabled: false) ) diff --git a/Projects/Shared/DesignSystem/Sources/UI/Navigaion/PickeNavigationBar.swift b/Projects/Shared/DesignSystem/Sources/UI/Navigaion/PickeNavigationBar.swift new file mode 100644 index 0000000..382d463 --- /dev/null +++ b/Projects/Shared/DesignSystem/Sources/UI/Navigaion/PickeNavigationBar.swift @@ -0,0 +1,76 @@ +// +// PickeNavigationBar.swift +// DesignSystem +// +// Picke 공통 네비게이션 바 — 좌측 back / 가운데 옵션 / 우측 ViewBuilder. +// + +import SwiftUI + +/// 화면 상단에 공통으로 올라가는 네비게이션 바. +/// - `onBack` 이 nil 이면 좌측 영역은 24×24 placeholder 만 남는다. +/// - `centerIcon` 이 nil 이면 가운데는 빈 공간. +/// - 우측은 호출처에서 자유롭게 ViewBuilder 로 주입. +/// +/// 색상은 호출처에서 `.foregroundStyle(.beige50)` 같은 modifier 로 위임. +public struct PickeNavigationBar: View { + private let onBack: (() -> Void)? + private let centerIcon: Image? + private let trailing: () -> Trailing + + public init( + onBack: (() -> Void)? = nil, + centerIcon: Image? = nil, + @ViewBuilder trailing: @escaping () -> Trailing + ) { + self.onBack = onBack + self.centerIcon = centerIcon + self.trailing = trailing + } + + public var body: some View { + HStack { + leadingArea + Spacer() + centerArea + Spacer() + trailing() + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + } + + @ViewBuilder + private var leadingArea: some View { + if let onBack { + Button { onBack() } label: { + Image(systemName: "chevron.left") + .font(.system(size: 18, weight: .semibold)) + .frame(width: 24, height: 24) + } + .buttonStyle(.plain) + } else { + Color.clear.frame(width: 24, height: 24) + } + } + + @ViewBuilder + private var centerArea: some View { + if let centerIcon { + centerIcon + .font(.system(size: 16, weight: .semibold)) + .frame(width: 24, height: 24) + } else { + EmptyView() + } + } +} + +public extension PickeNavigationBar where Trailing == EmptyView { + init( + onBack: (() -> Void)? = nil, + centerIcon: Image? = nil + ) { + self.init(onBack: onBack, centerIcon: centerIcon) { EmptyView() } + } +} diff --git a/Projects/Shared/DesignSystem/Sources/UI/Share/ShareSheet.swift b/Projects/Shared/DesignSystem/Sources/UI/Share/ShareSheet.swift new file mode 100644 index 0000000..367d116 --- /dev/null +++ b/Projects/Shared/DesignSystem/Sources/UI/Share/ShareSheet.swift @@ -0,0 +1,23 @@ +// +// ShareSheet.swift +// DesignSystem +// +// 애플 기본 공유 시트 (UIActivityViewController) 를 SwiftUI `.sheet` 로 띄우기 위한 wrapper. +// + +import SwiftUI +import UIKit + +public struct ShareSheet: UIViewControllerRepresentable { + private let items: [Any] + + public init(items: [Any]) { + self.items = items + } + + public func makeUIViewController(context _: Context) -> UIActivityViewController { + UIActivityViewController(activityItems: items, applicationActivities: nil) + } + + public func updateUIViewController(_: UIActivityViewController, context _: Context) {} +} From c38b825704d93e2455c905f8f696ce39ca058b09 Mon Sep 17 00:00:00 2001 From: Roy Date: Sun, 17 May 2026 22:19:16 +0900 Subject: [PATCH 02/21] =?UTF-8?q?feat:=20#3=20=ED=99=88=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=20(Hero=20=C2=B7=20Hot=20=C2=B7=20Best=20=C2=B7=20Vot?= =?UTF-8?q?e=20=C2=B7=20New)=20=ED=83=AD=20=E2=86=92=20=EC=82=AC=EC=A0=84?= =?UTF-8?q?=20=ED=88=AC=ED=91=9C=EC=B0=BD=20=EC=A7=84=EC=9E=85=20=EB=9D=BC?= =?UTF-8?q?=EC=9A=B0=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - HomeFeature.View 에 heroTapped · hotBattleTapped · bestBattleTapped · newBattleTapped 추가 - DelegateAction.presentPreVote(battleId:) 시그니처로 통일하여 카드 종류 무관하게 battleId 만 코디네이터에 전달 - HomeView 의 카드들에 .contentShape(Rectangle()) + .onTapGesture 부착, Vote 카드는 Button 래핑 제거 - HeroCarouselView 에 onTap 클로저 매개변수 추가 - 홈 배경을 .pen `#fafaf9` 매칭을 위해 .beige50 → .beige200 으로 보정 --- .../Sources/Main/Reducer/HomeFeature.swift | 29 +++++++++++++++++-- .../View/Components/HeroCarouselView.swift | 3 ++ .../Home/Sources/Main/View/HomeView.swift | 25 ++++++++++++---- 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/Projects/Presentation/Home/Sources/Main/Reducer/HomeFeature.swift b/Projects/Presentation/Home/Sources/Main/Reducer/HomeFeature.swift index 4ade8c4..5ea33f4 100644 --- a/Projects/Presentation/Home/Sources/Main/Reducer/HomeFeature.swift +++ b/Projects/Presentation/Home/Sources/Main/Reducer/HomeFeature.swift @@ -47,6 +47,11 @@ public struct HomeFeature { case onAppear case pullToRefresh case seeMoreTapped(Section) + case voteTapped(VoteQuestion) + case heroTapped(HeroBattle) + case hotBattleTapped(HotBattle) + case bestBattleTapped(BestBattle) + case newBattleTapped(NewBattle) } public enum Section: Equatable { @@ -64,7 +69,9 @@ public struct HomeFeature { case homeResponse(Result) } - public enum DelegateAction: Equatable {} + public enum DelegateAction: Equatable { + case presentPreVote(battleId: Int) + } nonisolated enum CancelID: Hashable { case fetchHome @@ -107,6 +114,21 @@ extension HomeFeature { case .seeMoreTapped: return .none + + case let .voteTapped(question): + return .send(.delegate(.presentPreVote(battleId: question.battleId))) + + case let .heroTapped(battle): + return .send(.delegate(.presentPreVote(battleId: battle.battleId))) + + case let .hotBattleTapped(battle): + return .send(.delegate(.presentPreVote(battleId: battle.battleId))) + + case let .bestBattleTapped(battle): + return .send(.delegate(.presentPreVote(battleId: battle.battleId))) + + case let .newBattleTapped(battle): + return .send(.delegate(.presentPreVote(battleId: battle.battleId))) } } @@ -158,6 +180,9 @@ extension HomeFeature { state _: inout State, action: DelegateAction ) -> Effect { - switch action {} + switch action { + case .presentPreVote: + .none + } } } diff --git a/Projects/Presentation/Home/Sources/Main/View/Components/HeroCarouselView.swift b/Projects/Presentation/Home/Sources/Main/View/Components/HeroCarouselView.swift index e6fac5b..f98b344 100644 --- a/Projects/Presentation/Home/Sources/Main/View/Components/HeroCarouselView.swift +++ b/Projects/Presentation/Home/Sources/Main/View/Components/HeroCarouselView.swift @@ -16,6 +16,7 @@ import Kingfisher struct HeroCarouselView: View { let heroes: [HeroBattle] @Binding var currentIndex: Int + var onTap: (HeroBattle) -> Void = { _ in } private static let autoScrollInterval: TimeInterval = 3 private let timer = Timer.publish(every: autoScrollInterval, on: .main, in: .common).autoconnect() @@ -28,6 +29,8 @@ struct HeroCarouselView: View { position: index + 1, total: heroes.count ) + .contentShape(Rectangle()) + .onTapGesture { onTap(hero) } .tag(index) } } diff --git a/Projects/Presentation/Home/Sources/Main/View/HomeView.swift b/Projects/Presentation/Home/Sources/Main/View/HomeView.swift index 0eb02d5..2669202 100644 --- a/Projects/Presentation/Home/Sources/Main/View/HomeView.swift +++ b/Projects/Presentation/Home/Sources/Main/View/HomeView.swift @@ -31,7 +31,8 @@ public struct HomeView: View { VStack(spacing: 32) { HeroCarouselView( heroes: store.heroes, - currentIndex: $store.heroIndex + currentIndex: $store.heroIndex, + onTap: { send(.heroTapped($0)) } ) hotBattlesSection() @@ -43,7 +44,7 @@ public struct HomeView: View { } } } - .background(Color.beige50.ignoresSafeArea()) + .background(Color.beige200.ignoresSafeArea()) .onAppear { send(.onAppear) } .navigationBarHidden(true) .scrollIndicators(.hidden) @@ -71,7 +72,11 @@ extension HomeView { } ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 16) { - ForEach(store.hotBattles) { HotBattleCardView(battle: $0) } + ForEach(store.hotBattles) { battle in + HotBattleCardView(battle: battle) + .contentShape(Rectangle()) + .onTapGesture { send(.hotBattleTapped(battle)) } + } } .padding(.horizontal, 16) } @@ -84,7 +89,11 @@ extension HomeView { send(.seeMoreTapped(.bestBattles)) } VStack(spacing: 12) { - ForEach(store.bestBattles) { BestBattleCardView(battle: $0) } + ForEach(store.bestBattles) { battle in + BestBattleCardView(battle: battle) + .contentShape(Rectangle()) + .onTapGesture { send(.bestBattleTapped(battle)) } + } } .padding(.horizontal, 16) } @@ -101,6 +110,8 @@ extension HomeView { } if let vote = store.currentVote { VoteCardView(question: vote) + .contentShape(Rectangle()) + .onTapGesture { send(.voteTapped(vote)) } } } .padding(.horizontal, 16) @@ -113,7 +124,11 @@ extension HomeView { send(.seeMoreTapped(.newBattles)) } VStack(spacing: 12) { - ForEach(store.newBattles) { NewBattleCardView(battle: $0) } + ForEach(store.newBattles) { battle in + NewBattleCardView(battle: battle) + .contentShape(Rectangle()) + .onTapGesture { send(.newBattleTapped(battle)) } + } } .padding(.horizontal, 16) } From e9d2d05c5b76c266050e398446234a1645d14d36 Mon Sep 17 00:00:00 2001 From: Roy Date: Sun, 17 May 2026 22:19:29 +0900 Subject: [PATCH 03/21] =?UTF-8?q?fix:=20#3=20=EC=83=88=EB=A1=9C=EC=9A=B4?= =?UTF-8?q?=20=EB=B0=B0=ED=8B=80=20=EC=B9=B4=EB=93=9C=20.pen=20=EB=94=94?= =?UTF-8?q?=EC=9E=90=EC=9D=B8=20=EB=A7=A4=EC=B9=AD=20+=20=ED=99=88=20?= =?UTF-8?q?=ED=97=A4=EB=8D=94=20bottom=20stroke=20=EC=A0=95=ED=95=A9?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - NewBattleCardView 재작성 — Admission 옵션 카드 (아바타 40×40 + 입장 + 철학자) · VS 뱃지 24×24 secondary200 · beige300/beige600 stroke - 아바타: PhilosopherAvatar 매핑된 일러스트 fallback · 추후 API 의 philosopherAImageUrl/BImageUrl 들어오면 KFImage 로 우선 표시 - NewBattle Entity 에 philosopherAImageURL · philosopherBImageURL 복원 (mock 은 picsum URL) - HomeHeaderView 하단에 1pt beige600 stroke overlay 추가 — .pen Header bottom stroke 매칭 --- .../Entity/Sources/Home/NewBattle.swift | 27 +++- .../Main/View/Components/HomeHeaderView.swift | 5 + .../View/Components/NewBattleCardView.swift | 120 ++++++++++++------ 3 files changed, 109 insertions(+), 43 deletions(-) diff --git a/Projects/Domain/Entity/Sources/Home/NewBattle.swift b/Projects/Domain/Entity/Sources/Home/NewBattle.swift index b80cdac..0830e3d 100644 --- a/Projects/Domain/Entity/Sources/Home/NewBattle.swift +++ b/Projects/Domain/Entity/Sources/Home/NewBattle.swift @@ -62,28 +62,43 @@ public extension NewBattle { static let mocks: [NewBattle] = [ .init( battleId: 51, + thumbnailURL: URL(string: "https://picsum.photos/seed/picke-new-1/400/200"), title: "인간은 본래 선한가, 악한가?", summary: "인간 본성의 선악과 문명의 역할에 관한 철학적 대결!", - philosopherA: "순자", optionATitle: "악하다", - philosopherB: "순자", optionBTitle: "악하다", + philosopherA: "순자", + optionATitle: "악하다", + philosopherAImageURL: URL(string: "https://picsum.photos/seed/picke-philo-a/80/80"), + philosopherB: "순자", + optionBTitle: "악하다", + philosopherBImageURL: URL(string: "https://picsum.photos/seed/picke-philo-b/80/80"), tags: [.init(tagId: 501, name: "#철학", type: .category)], audioDuration: 5 * 60, viewCount: 726 ), .init( battleId: 52, + thumbnailURL: URL(string: "https://picsum.photos/seed/picke-new-2/400/200"), title: "노키즈존: 영업상의 자유인가, 공공장소에서의 차별인가?", summary: "옆 테이블 아이의 울음소리가 평화로운 휴식시간을 깨뜨린다면?", - philosopherA: "순자", optionATitle: "악하다", - philosopherB: "순자", optionBTitle: "악하다", + philosopherA: "순자", + optionATitle: "악하다", + philosopherAImageURL: URL(string: "https://picsum.photos/seed/picke-philo-a/80/80"), + philosopherB: "순자", + optionBTitle: "악하다", + philosopherBImageURL: URL(string: "https://picsum.photos/seed/picke-philo-b/80/80"), tags: [.init(tagId: 502, name: "#사회", type: .category)], audioDuration: 5 * 60, viewCount: 726 ), .init( battleId: 53, + thumbnailURL: URL(string: "https://picsum.photos/seed/picke-new-3/400/200"), title: "사후세계는 존재하는가, 인간이 만든 위안인가?", summary: "죽음은 끝일까요, 아니면 다른 방식의 시작일까요?", - philosopherA: "순자", optionATitle: "악하다", - philosopherB: "순자", optionBTitle: "악하다", + philosopherA: "순자", + optionATitle: "악하다", + philosopherAImageURL: URL(string: "https://picsum.photos/seed/picke-philo-a/80/80"), + philosopherB: "순자", + optionBTitle: "악하다", + philosopherBImageURL: URL(string: "https://picsum.photos/seed/picke-philo-b/80/80"), tags: [.init(tagId: 503, name: "#철학", type: .category)], audioDuration: 5 * 60, viewCount: 726 ), diff --git a/Projects/Presentation/Home/Sources/Main/View/Components/HomeHeaderView.swift b/Projects/Presentation/Home/Sources/Main/View/Components/HomeHeaderView.swift index df5706c..db34097 100644 --- a/Projects/Presentation/Home/Sources/Main/View/Components/HomeHeaderView.swift +++ b/Projects/Presentation/Home/Sources/Main/View/Components/HomeHeaderView.swift @@ -33,5 +33,10 @@ struct HomeHeaderView: View { .padding(.vertical, 8) .frame(height: 56) .background(Color.beige50) + .overlay(alignment: .bottom) { + Rectangle() + .fill(.beige600) + .frame(height: 1) + } } } diff --git a/Projects/Presentation/Home/Sources/Main/View/Components/NewBattleCardView.swift b/Projects/Presentation/Home/Sources/Main/View/Components/NewBattleCardView.swift index 0c9e5eb..1c0c108 100644 --- a/Projects/Presentation/Home/Sources/Main/View/Components/NewBattleCardView.swift +++ b/Projects/Presentation/Home/Sources/Main/View/Components/NewBattleCardView.swift @@ -12,29 +12,46 @@ import Entity import Kingfisher -/// "새로운 배틀" 리스트 카드 (제목 + VS 아바타 두 개). +/// "새로운 배틀" 리스트 카드 (.pen `Card/BattleListCard` 의 thumbnail 제외 구성). struct NewBattleCardView: View { let battle: NewBattle var body: some View { + content + .padding(12) + .background(.beige50, in: RoundedRectangle(cornerRadius: 2)) + .overlay( + RoundedRectangle(cornerRadius: 2).stroke(.beige600, lineWidth: 1) + ) + } +} + +// MARK: - Sections + +extension NewBattleCardView { + private var content: some View { VStack(alignment: .leading, spacing: 12) { - headerRow - titleBlock + container versusRow } - .padding(12) - .background(.beige50, in: RoundedRectangle(cornerRadius: 2)) - .overlay( - RoundedRectangle(cornerRadius: 2).stroke(.beige600, lineWidth: 1) - ) } - private var headerRow: some View { - HStack { + private var container: some View { + VStack(alignment: .leading, spacing: 12) { + metaRow + titleBlock + } + } + + private var metaRow: some View { + HStack(spacing: 10) { if let tag = battle.tags.first { Text(tag.name) - .pretendardFont(family: .Medium, size: 11) + .pretendardFont(family: .SemiBold, size: 12) .foregroundStyle(.primary500) + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background(.beige600, in: RoundedRectangle(cornerRadius: 2)) } Spacer() MetaLabelView(systemImage: "clock", text: "\(battle.durationMinutes)분") @@ -43,29 +60,31 @@ struct NewBattleCardView: View { } private var titleBlock: some View { - VStack(alignment: .leading, spacing: 6) { + VStack(alignment: .leading, spacing: 4) { Text(battle.title) .pretendardFont(family: .SemiBold, size: 14) - .foregroundStyle(.neutral900) + .foregroundStyle(.neutral500) + .kerning(-0.35) .lineLimit(2) + .frame(maxWidth: .infinity, alignment: .leading) Text(battle.summary) .pretendardFont(family: .Medium, size: 12) - .foregroundStyle(.neutral300) + .foregroundStyle(.neutral200) + .lineSpacing(12 * 0.4) .lineLimit(2) + .frame(maxWidth: .infinity, alignment: .leading) } } private var versusRow: some View { HStack(spacing: 8) { - NewBattleAvatarPill( + admissionButton( label: battle.optionATitle, sub: battle.philosopherA, imageURL: battle.philosopherAImageURL ) - Text("VS") - .pretendardFont(family: .SemiBold, size: 11) - .foregroundStyle(.neutral300) - NewBattleAvatarPill( + vsBadge + admissionButton( label: battle.optionBTitle, sub: battle.philosopherB, imageURL: battle.philosopherBImageURL @@ -74,21 +93,23 @@ struct NewBattleCardView: View { } } -/// 새로운 배틀 카드 안의 발화자 아바타 (원형 thumbnail + 라벨). -struct NewBattleAvatarPill: View { - let label: String - let sub: String - let imageURL: URL? +// MARK: - Sub-components - var body: some View { - HStack(spacing: 8) { - avatar - VStack(alignment: .leading, spacing: 0) { +extension NewBattleCardView { + private func admissionButton( + label: String, + sub: String, + imageURL: URL? + ) -> some View { + HStack(spacing: 4) { + avatar(for: sub, imageURL: imageURL) + VStack(alignment: .leading, spacing: 2) { Text(label) - .pretendardFont(family: .SemiBold, size: 12) - .foregroundStyle(.neutral900) + .pretendardFont(family: .SemiBold, size: 14) + .foregroundStyle(.neutral600) + .kerning(-0.35) Text(sub) - .pretendardFont(family: .Medium, size: 10) + .pretendardFont(family: .Medium, size: 12) .foregroundStyle(.neutral300) } Spacer(minLength: 0) @@ -102,17 +123,42 @@ struct NewBattleAvatarPill: View { } @ViewBuilder - private var avatar: some View { - if let url = imageURL { - KFImage(url) + private func avatar(for philosopherName: String, imageURL: URL?) -> some View { + if let imageURL { + KFImage(imageURL) .resizable() .scaledToFill() - .frame(width: 28, height: 28) + .frame(width: 40, height: 40) .clipShape(Circle()) + } else if let asset = PhilosopherAvatar(rawValue: philosopherName)?.imageAsset { + Image(asset: asset) + .resizable() + .scaledToFit() + .frame(width: 40, height: 40) + .background(.beige600, in: Circle()) } else { Circle() - .fill(.beige500) - .frame(width: 28, height: 28) + .fill(.beige600) + .frame(width: 40, height: 40) + } + } + + private var vsBadge: some View { + Text("VS") + .pretendardFont(family: .Bold, size: 8) + .foregroundStyle(.neutral800) + .frame(width: 24, height: 24) + .background(.secondary200, in: Circle()) + .overlay(Circle().stroke(.beige50, lineWidth: 1.5)) + } +} + +private extension PhilosopherAvatar { + var imageAsset: ImageAsset { + switch self { + case .plato: .avatarPlato + case .sartre: .avatarSartre + case .sunja: .avatarSunja } } } From 0dcd42ceac5c617d8225fc240732fc72e3a48ede Mon Sep 17 00:00:00 2001 From: Roy Date: Sun, 17 May 2026 22:19:40 +0900 Subject: [PATCH 04/21] =?UTF-8?q?chore:=20#3=20=ED=83=AD=EB=B0=94=20GNB=20?= =?UTF-8?q?=EC=95=84=EC=9D=B4=EC=BD=98=20active/inactive=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=20+=20SVG=20single-scale=20=EC=9E=90=EC=82=B0=20?= =?UTF-8?q?=EA=B5=90=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GNB 아이콘 PNG → SVG 로 교체 (preserves-vector-representation · template-rendering-intent: original) - inactive 4개 + active 4개 총 8 imageset 등록 (Icon-홈/탐색/빠른배틀/마이 × default·active) - MainTabCoordinator.Tab.iconAsset(isSelected:) 함수화하여 선택 상태에 따라 active 자산 반환 - MainTabView.tabIcon 에서 selectedTab 비교로 isSelected 분기 + .renderingMode(.original) 강제하여 SVG 원본 색 유지 (시스템 tint 적용 X) --- .../Sources/Reducer/MainTabCoordinator.swift | 12 +++++------ .../MainTab/Sources/View/MainTabView.swift | 21 +++++++++++-------- .../GNB/tabExplore.imageset/Contents.json | 3 ++- .../tabExploreActive.imageset/Contents.json | 3 ++- .../GNB/tabHome.imageset/Contents.json | 3 ++- .../GNB/tabHomeActive.imageset/Contents.json | 3 ++- .../GNB/tabMyPage.imageset/Contents.json | 3 ++- .../tabMyPageActive.imageset/Contents.json | 3 ++- .../GNB/tabQuickBattle.imageset/Contents.json | 3 ++- .../Contents.json | 3 ++- 10 files changed, 34 insertions(+), 23 deletions(-) diff --git a/Projects/Presentation/MainTab/Sources/Reducer/MainTabCoordinator.swift b/Projects/Presentation/MainTab/Sources/Reducer/MainTabCoordinator.swift index 071ec78..d76465f 100644 --- a/Projects/Presentation/MainTab/Sources/Reducer/MainTabCoordinator.swift +++ b/Projects/Presentation/MainTab/Sources/Reducer/MainTabCoordinator.swift @@ -35,13 +35,13 @@ public struct MainTabCoordinator { } } - /// 디자인 시스템 GNB 아이콘 (Pencil 추출 PNG) - public var iconAsset: ImageAsset { + /// 디자인 시스템 GNB 아이콘 (탭바 아이콘 폴더 PNG, single-scale) + public func iconAsset(isSelected: Bool) -> ImageAsset { switch self { - case .home: .tabHome - case .explore: .tabExplore - case .quickBattle: .tabQuickBattle - case .myPage: .tabMyPage + case .home: isSelected ? .tabHomeActive : .tabHome + case .explore: isSelected ? .tabExploreActive : .tabExplore + case .quickBattle: isSelected ? .tabQuickBattleActive : .tabQuickBattle + case .myPage: isSelected ? .tabMyPageActive : .tabMyPage } } } diff --git a/Projects/Presentation/MainTab/Sources/View/MainTabView.swift b/Projects/Presentation/MainTab/Sources/View/MainTabView.swift index 8fd62c3..9f8d752 100644 --- a/Projects/Presentation/MainTab/Sources/View/MainTabView.swift +++ b/Projects/Presentation/MainTab/Sources/View/MainTabView.swift @@ -26,7 +26,7 @@ public struct MainTabView: View { TCAFlowTabRouter( selectedTab: $store.selectedTab.sending(\.selectTab), tabs: MainTabCoordinator.Tab.allCases.map { - TabItem(title: $0.title, icon: $0.iconAsset.rawValue, tag: $0.rawValue) + TabItem(title: $0.title, icon: $0.iconAsset(isSelected: false).rawValue, tag: $0.rawValue) }, onReselect: { tab in store.send(.tabReselected(tab)) @@ -73,13 +73,13 @@ extension MainTabView { itemAppearance.normal.iconColor = normalColor itemAppearance.normal.titleTextAttributes = [ .font: font, - .foregroundColor: normalColor + .foregroundColor: normalColor, ] itemAppearance.selected.iconColor = selectedColor itemAppearance.selected.titleTextAttributes = [ .font: font, - .foregroundColor: selectedColor + .foregroundColor: selectedColor, ] } @@ -94,12 +94,15 @@ extension MainTabView { @ViewBuilder private func tabIcon(for tab: TabItem) -> some View { - if let image = UIImage(assetName: tab.icon)?.withRenderingMode(.alwaysTemplate) { - Image(uiImage: image) - .renderingMode(.template) - } else { - Image(systemName: "questionmark") - } + let isSelected = store.selectedTab == tab.tag + let asset = MainTabCoordinator.Tab(rawValue: tab.tag)? + .iconAsset(isSelected: isSelected) ?? .none + + Image(asset: asset) + .renderingMode(.original) + .resizable() + .scaledToFit() + .frame(width: 24, height: 24) } @ViewBuilder diff --git a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabExplore.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabExplore.imageset/Contents.json index c9212d0..61f2414 100644 --- a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabExplore.imageset/Contents.json +++ b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabExplore.imageset/Contents.json @@ -10,6 +10,7 @@ "version" : 1 }, "properties" : { - "preserves-vector-representation" : true + "preserves-vector-representation" : true, + "template-rendering-intent" : "original" } } diff --git a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabExploreActive.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabExploreActive.imageset/Contents.json index 8806eb6..65fe5dc 100644 --- a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabExploreActive.imageset/Contents.json +++ b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabExploreActive.imageset/Contents.json @@ -10,6 +10,7 @@ "version" : 1 }, "properties" : { - "preserves-vector-representation" : true + "preserves-vector-representation" : true, + "template-rendering-intent" : "original" } } diff --git a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabHome.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabHome.imageset/Contents.json index cab2f1b..3340058 100644 --- a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabHome.imageset/Contents.json +++ b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabHome.imageset/Contents.json @@ -10,6 +10,7 @@ "version" : 1 }, "properties" : { - "preserves-vector-representation" : true + "preserves-vector-representation" : true, + "template-rendering-intent" : "original" } } diff --git a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabHomeActive.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabHomeActive.imageset/Contents.json index 30d363c..d19457d 100644 --- a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabHomeActive.imageset/Contents.json +++ b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabHomeActive.imageset/Contents.json @@ -10,6 +10,7 @@ "version" : 1 }, "properties" : { - "preserves-vector-representation" : true + "preserves-vector-representation" : true, + "template-rendering-intent" : "original" } } diff --git a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabMyPage.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabMyPage.imageset/Contents.json index e115f5e..c92b999 100644 --- a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabMyPage.imageset/Contents.json +++ b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabMyPage.imageset/Contents.json @@ -10,6 +10,7 @@ "version" : 1 }, "properties" : { - "preserves-vector-representation" : true + "preserves-vector-representation" : true, + "template-rendering-intent" : "original" } } diff --git a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabMyPageActive.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabMyPageActive.imageset/Contents.json index 86a48ec..9ab58b6 100644 --- a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabMyPageActive.imageset/Contents.json +++ b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabMyPageActive.imageset/Contents.json @@ -10,6 +10,7 @@ "version" : 1 }, "properties" : { - "preserves-vector-representation" : true + "preserves-vector-representation" : true, + "template-rendering-intent" : "original" } } diff --git a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabQuickBattle.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabQuickBattle.imageset/Contents.json index 8ff80d6..44915c7 100644 --- a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabQuickBattle.imageset/Contents.json +++ b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabQuickBattle.imageset/Contents.json @@ -10,6 +10,7 @@ "version" : 1 }, "properties" : { - "preserves-vector-representation" : true + "preserves-vector-representation" : true, + "template-rendering-intent" : "original" } } diff --git a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabQuickBattleActive.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabQuickBattleActive.imageset/Contents.json index e567445..c65b07d 100644 --- a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabQuickBattleActive.imageset/Contents.json +++ b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/GNB/tabQuickBattleActive.imageset/Contents.json @@ -10,6 +10,7 @@ "version" : 1 }, "properties" : { - "preserves-vector-representation" : true + "preserves-vector-representation" : true, + "template-rendering-intent" : "original" } } From 52c5b109f96efd6a01e1fd64afbebefdf9aa094e Mon Sep 17 00:00:00 2001 From: Roy Date: Sun, 17 May 2026 22:19:57 +0900 Subject: [PATCH 05/21] =?UTF-8?q?chore:=20Service=20=EB=AA=A8=EB=93=88?= =?UTF-8?q?=EC=97=90=20Entity=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - HomeService 등이 Entity 타입을 직접 참조할 수 있도록 .Domain(implements: .Entity) 의존성 추가 --- Projects/Data/Service/Project.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Projects/Data/Service/Project.swift b/Projects/Data/Service/Project.swift index 2ed4c4b..6d7abb2 100644 --- a/Projects/Data/Service/Project.swift +++ b/Projects/Data/Service/Project.swift @@ -11,6 +11,7 @@ let project = Project.makeModule( settings: .settings(), dependencies: [ .Data(implements: .API), + .Domain(implements: .Entity), .Network(implements: .Foundations), .SPM.asyncMoya, ], From d3a3c5bf04defa32b9a39e2e6ec91fcd85a9f1ab Mon Sep 17 00:00:00 2001 From: Roy Date: Mon, 18 May 2026 11:27:09 +0900 Subject: [PATCH 06/21] =?UTF-8?q?feat:=20#31=20Apple=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=E2=80=94=20identityToken=20=EC=A0=84=EB=8B=AC=20+?= =?UTF-8?q?=20redirectUri=20=EC=98=B5=EC=85=94=EB=84=90=20+=20UserDefaults?= =?UTF-8?q?=20=EC=BA=90=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - OAuthLoginRequest 에 idToken: String? 추가 (CodingKey: identityToken) · redirectUri 옵셔널화 - AuthInterface / Default · Mock · AuthRepositoryImpl / AuthUseCaseImpl 의 login 시그니처에 idToken · redirectUri? 추가 - UnifiedOAuthUseCase.appleLogin: redirectUri = nil, idToken = payload.idToken 으로 호출하고 authCode · idToken 을 UserDefaults 에 동시 저장 - AuthLocalStorage 신규 — picke.auth.authCode / picke.auth.idToken 키로 단순 캐시 (Keychain 과 분리) - Google · Kakao 는 기존 redirectUri 유지하고 idToken = nil 명시 --- .../Auth/Repository/AuthRepositoryImpl.swift | 6 ++-- .../Service/Sources/Auth/OAuthRequest.swift | 17 +++++++++-- .../Sources/Auth/AuthInterface.swift | 3 +- .../Auth/DefaultAuthRepositoryImpl.swift | 3 +- .../Sources/Auth/MockAuthRepository.swift | 3 +- .../Sources/Manager/AuthLocalStorage.swift | 29 +++++++++++++++++++ .../Sources/Auth/AuthUseCaseImpl.swift | 6 ++-- .../Sources/OAuth/UnifiedOAuthUseCase.swift | 15 +++++++--- 8 files changed, 68 insertions(+), 14 deletions(-) create mode 100644 Projects/Domain/DomainInterface/Sources/Manager/AuthLocalStorage.swift diff --git a/Projects/Data/Repository/Sources/Auth/Repository/AuthRepositoryImpl.swift b/Projects/Data/Repository/Sources/Auth/Repository/AuthRepositoryImpl.swift index 53b1fde..3603724 100644 --- a/Projects/Data/Repository/Sources/Auth/Repository/AuthRepositoryImpl.swift +++ b/Projects/Data/Repository/Sources/Auth/Repository/AuthRepositoryImpl.swift @@ -38,14 +38,16 @@ public final class AuthRepositoryImpl: AuthInterface, @unchecked Sendable { public func login( provider socialProvider: SocialType, authorizationCode: String, - redirectUri: String + redirectUri: String?, + idToken: String? ) async throws -> LoginEntity { let dto: LoginResponseDTO = try await provider.request( .login( provider: socialProvider, body: OAuthLoginRequest( authorizationCode: authorizationCode, - redirectUri: redirectUri + redirectUri: redirectUri, + idToken: idToken ) ) ) diff --git a/Projects/Data/Service/Sources/Auth/OAuthRequest.swift b/Projects/Data/Service/Sources/Auth/OAuthRequest.swift index 21d7fd1..7176e7c 100644 --- a/Projects/Data/Service/Sources/Auth/OAuthRequest.swift +++ b/Projects/Data/Service/Sources/Auth/OAuthRequest.swift @@ -7,16 +7,27 @@ import Foundation -/// `/api/v1/auth/login/{provider}` 요청 바디 +/// `/api/v1/auth/login/{provider}` 요청 바디. +/// - `idToken`: Apple 로그인에서만 채워서 보낸다 (JSON key: `identityToken`). +/// - `redirectUri`: Apple 은 nil 로 보낸다 (서버에서 redirect 사용 X). public struct OAuthLoginRequest: Encodable { public let authorizationCode: String - public let redirectUri: String + public let redirectUri: String? + public let idToken: String? + + enum CodingKeys: String, CodingKey { + case authorizationCode + case redirectUri + case idToken = "identityToken" + } public init( authorizationCode: String, - redirectUri: String + redirectUri: String?, + idToken: String? = nil ) { self.authorizationCode = authorizationCode self.redirectUri = redirectUri + self.idToken = idToken } } diff --git a/Projects/Domain/DomainInterface/Sources/Auth/AuthInterface.swift b/Projects/Domain/DomainInterface/Sources/Auth/AuthInterface.swift index 11d7bca..80897ec 100644 --- a/Projects/Domain/DomainInterface/Sources/Auth/AuthInterface.swift +++ b/Projects/Domain/DomainInterface/Sources/Auth/AuthInterface.swift @@ -14,7 +14,8 @@ public protocol AuthInterface: Sendable { func login( provider: SocialType, authorizationCode: String, - redirectUri: String + redirectUri: String?, + idToken: String? ) async throws -> LoginEntity func refresh() async throws -> AuthTokens func withDraw(token: String) async throws -> WithdrawEntity diff --git a/Projects/Domain/DomainInterface/Sources/Auth/DefaultAuthRepositoryImpl.swift b/Projects/Domain/DomainInterface/Sources/Auth/DefaultAuthRepositoryImpl.swift index a80261c..0919124 100644 --- a/Projects/Domain/DomainInterface/Sources/Auth/DefaultAuthRepositoryImpl.swift +++ b/Projects/Domain/DomainInterface/Sources/Auth/DefaultAuthRepositoryImpl.swift @@ -15,7 +15,8 @@ public final class DefaultAuthRepositoryImpl: AuthInterface, @unchecked Sendable public func login( provider: SocialType, authorizationCode _: String, - redirectUri _: String + redirectUri _: String?, + idToken _: String? ) async throws -> LoginEntity { LoginEntity( name: "Mock User", diff --git a/Projects/Domain/DomainInterface/Sources/Auth/MockAuthRepository.swift b/Projects/Domain/DomainInterface/Sources/Auth/MockAuthRepository.swift index 6e8d2d6..8caa5e4 100644 --- a/Projects/Domain/DomainInterface/Sources/Auth/MockAuthRepository.swift +++ b/Projects/Domain/DomainInterface/Sources/Auth/MockAuthRepository.swift @@ -45,7 +45,8 @@ public final class MockAuthRepository: AuthInterface, @unchecked Sendable { public func login( provider: SocialType, authorizationCode _: String, - redirectUri _: String + redirectUri _: String?, + idToken _: String? ) async throws -> LoginEntity { loginCallCount += 1 try await Task.sleep(for: .milliseconds(10)) diff --git a/Projects/Domain/DomainInterface/Sources/Manager/AuthLocalStorage.swift b/Projects/Domain/DomainInterface/Sources/Manager/AuthLocalStorage.swift new file mode 100644 index 0000000..a485de7 --- /dev/null +++ b/Projects/Domain/DomainInterface/Sources/Manager/AuthLocalStorage.swift @@ -0,0 +1,29 @@ +// +// AuthLocalStorage.swift +// DomainInterface +// +// Apple OAuth 가 응답으로 내려준 authorizationCode 와 identityToken 을 +// 로컬에 보관해두는 UserDefaults wrapper. Keychain 과 달리 단순 캐시 용도. +// + +import Foundation + +public enum AuthLocalStorage { + private static let authCodeKey = "picke.auth.authCode" + private static let idTokenKey = "picke.auth.idToken" + + public static var authCode: String? { + get { UserDefaults.standard.string(forKey: authCodeKey) } + set { UserDefaults.standard.set(newValue, forKey: authCodeKey) } + } + + public static var idToken: String? { + get { UserDefaults.standard.string(forKey: idTokenKey) } + set { UserDefaults.standard.set(newValue, forKey: idTokenKey) } + } + + public static func clear() { + UserDefaults.standard.removeObject(forKey: authCodeKey) + UserDefaults.standard.removeObject(forKey: idTokenKey) + } +} diff --git a/Projects/Domain/UseCase/Sources/Auth/AuthUseCaseImpl.swift b/Projects/Domain/UseCase/Sources/Auth/AuthUseCaseImpl.swift index db079ee..c094c4a 100644 --- a/Projects/Domain/UseCase/Sources/Auth/AuthUseCaseImpl.swift +++ b/Projects/Domain/UseCase/Sources/Auth/AuthUseCaseImpl.swift @@ -25,12 +25,14 @@ public struct AuthUseCaseImpl: AuthInterface { public func login( provider: SocialType, authorizationCode: String, - redirectUri: String + redirectUri: String?, + idToken: String? ) async throws -> LoginEntity { let result = try await authRepository.login( provider: provider, authorizationCode: authorizationCode, - redirectUri: redirectUri + redirectUri: redirectUri, + idToken: idToken ) $userSession.withLock { diff --git a/Projects/Domain/UseCase/Sources/OAuth/UnifiedOAuthUseCase.swift b/Projects/Domain/UseCase/Sources/OAuth/UnifiedOAuthUseCase.swift index a3355ca..ff4a2e0 100644 --- a/Projects/Domain/UseCase/Sources/OAuth/UnifiedOAuthUseCase.swift +++ b/Projects/Domain/UseCase/Sources/OAuth/UnifiedOAuthUseCase.swift @@ -85,10 +85,15 @@ public extension UnifiedOAuthUseCase { $0.name = userName } + let authCode = payload.authorizationCode ?? "" + AuthLocalStorage.authCode = authCode + AuthLocalStorage.idToken = payload.idToken + let loginEntity = try await authRepository.login( provider: .apple, - authorizationCode: payload.authorizationCode ?? "", - redirectUri: SocialType.apple.redirectUri + authorizationCode: authCode, + redirectUri: nil, + idToken: payload.idToken ) keychainManager.save( @@ -116,7 +121,8 @@ public extension UnifiedOAuthUseCase { let loginEntity = try await authRepository.login( provider: .google, authorizationCode: payload.authorizationCode ?? "", - redirectUri: payload.redirectUri ?? SocialType.google.redirectUri + redirectUri: payload.redirectUri ?? SocialType.google.redirectUri, + idToken: nil ) keychainManager.save( @@ -142,7 +148,8 @@ public extension UnifiedOAuthUseCase { let loginEntity = try await authRepository.login( provider: .kakao, authorizationCode: payload.authorizationCode ?? "", - redirectUri: payload.redirectUri ?? SocialType.kakao.redirectUri + redirectUri: payload.redirectUri ?? SocialType.kakao.redirectUri, + idToken: nil ) keychainManager.save( From 5e8bec89930dce3be5dcb7be959c0f8eaa4de850 Mon Sep 17 00:00:00 2001 From: Roy Date: Mon, 18 May 2026 11:27:19 +0900 Subject: [PATCH 07/21] =?UTF-8?q?chore:=20#31=20Apple=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EB=8F=99=EC=9E=91=20=ED=99=95=EC=9D=B8=EC=9D=84=20?= =?UTF-8?q?=EC=9C=84=ED=95=B4=20Splash=20=E2=86=92=20Auth=20=EA=B0=95?= =?UTF-8?q?=EC=A0=9C=20=EC=A7=84=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SplashFeature 의 keychain 자격 분기를 임시 주석 처리 (이후 Apple 로그인 흐름 검증 끝나면 복원 예정) - AppReducer 가 splash.onAppear 시 곧바로 .view(.presentAuth) 를 dispatch 하도록 임시 변경 --- Projects/App/Sources/Reducer/AppReducer.swift | 2 +- .../Splash/Sources/Reducer/SplashFeature.swift | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Projects/App/Sources/Reducer/AppReducer.swift b/Projects/App/Sources/Reducer/AppReducer.swift index 1e1cd6e..476ce87 100644 --- a/Projects/App/Sources/Reducer/AppReducer.swift +++ b/Projects/App/Sources/Reducer/AppReducer.swift @@ -233,7 +233,7 @@ public struct AppReducer: Sendable { private func handleScopeNavigation(action: ScopeAction) -> Effect { switch action { case .splash(.view(.onAppear)): - return .none + return .send(.view(.presentAuth)) case .splash(.delegate(.presentAuth)): return .run { send in diff --git a/Projects/Presentation/Splash/Sources/Reducer/SplashFeature.swift b/Projects/Presentation/Splash/Sources/Reducer/SplashFeature.swift index 0560f3a..6ac0463 100644 --- a/Projects/Presentation/Splash/Sources/Reducer/SplashFeature.swift +++ b/Projects/Presentation/Splash/Sources/Reducer/SplashFeature.swift @@ -100,11 +100,11 @@ extension SplashFeature { let hasStoredCredential = hasStoredCredential return .run { send in try await clock.sleep(for: .seconds(1.2)) - if hasStoredCredential { - await send(.delegate(.presentMainTab)) - } else { - await send(.delegate(.presentAuth)) - } +// if hasStoredCredential { +// await send(.delegate(.presentMainTab)) +// } else { +// await send(.delegate(.presentAuth)) +// } } } From 294f91af2a34f6b4c021325a8f27be50b553726e Mon Sep 17 00:00:00 2001 From: Roy Date: Mon, 18 May 2026 11:27:29 +0900 Subject: [PATCH 08/21] =?UTF-8?q?ci:=20Gemini=202.5=20Flash=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20=EC=9D=B8=EB=9D=BC=EC=9D=B8=20PR=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=A6=AC=EB=B7=B0=20=EC=9B=8C=ED=81=AC=ED=94=8C?= =?UTF-8?q?=EB=A1=9C=EC=9A=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - pull_request (develop · main · master, opened/synchronize) 트리거 - Swift diff 만 추출하여 [LINE N] 어노테이션 후 모델에 전달 → 라인 단위 인라인 코멘트 게시 - 기존 Codex PR Review 워크플로우와 독립 실행되어 한쪽 실패가 다른 쪽에 영향 X - secrets: GEMINI_API_KEY 필요 (repo Settings → Secrets → Actions) --- .github/workflows/gemini-code-review.yml | 270 +++++++++++++++++++++++ 1 file changed, 270 insertions(+) create mode 100644 .github/workflows/gemini-code-review.yml diff --git a/.github/workflows/gemini-code-review.yml b/.github/workflows/gemini-code-review.yml new file mode 100644 index 0000000..e8fe47f --- /dev/null +++ b/.github/workflows/gemini-code-review.yml @@ -0,0 +1,270 @@ +name: Gemini Code Review + +on: + pull_request: + branches: + - develop + - main + - master + types: [opened, synchronize] + +concurrency: + group: gemini-code-review-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + code-review: + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install GoogleGenerativeAI + run: npm install @google/generative-ai + + - name: Get PR Context and Filtered Git Diff + id: get_diff + run: | + git fetch origin "${{ github.event.pull_request.base.ref }}" + git fetch origin "${{ github.event.pull_request.head.ref }}" + git diff "origin/${{ github.event.pull_request.base.ref }}"..."origin/${{ github.event.pull_request.head.ref }}" -- "*.swift" > diff.txt + if [ ! -s diff.txt ]; then + echo "skip_review=true" >> $GITHUB_OUTPUT + else + echo "skip_review=false" >> $GITHUB_OUTPUT + fi + + - name: Parse Diff for Valid Lines and Annotate + if: steps.get_diff.outputs.skip_review == 'false' + uses: actions/github-script@v7 + id: parse_diff + with: + script: | + const fs = require("fs"); + const diff = fs.readFileSync("diff.txt", "utf8"); + const validLines = {}; + const lineContentMap = {}; + const annotatedLines = []; + let currentFile = null; + let lineNum = 0; + for (const line of diff.split("\n")) { + if (line.startsWith("diff --git")) { + const match = line.match(/b\/(.+)$/); + if (match) currentFile = match[1]; + annotatedLines.push(line); + continue; + } + if (line.startsWith("+++") || line.startsWith("---") || line.startsWith("index ")) { + annotatedLines.push(line); + continue; + } + if (line.startsWith("@@") && currentFile) { + const match = line.match(/@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/); + if (match) lineNum = parseInt(match[1]); + annotatedLines.push(line); + continue; + } + if (!currentFile) { + annotatedLines.push(line); + continue; + } + if (line.startsWith("+") && !line.startsWith("+++")) { + if (!validLines[currentFile]) validLines[currentFile] = new Set(); + validLines[currentFile].add(lineNum); + if (!lineContentMap[currentFile]) lineContentMap[currentFile] = {}; + lineContentMap[currentFile][lineNum] = line.substring(1).trim(); + annotatedLines.push(`[LINE ${lineNum}] ${line}`); + lineNum++; + } else if (line.startsWith("-") && !line.startsWith("---")) { + annotatedLines.push(`[DEL] ${line}`); + } else { + annotatedLines.push(`[CTX ${lineNum}] ${line}`); + lineNum++; + } + } + const serializable = {}; + for (const [file, lines] of Object.entries(validLines)) { + serializable[file] = [...lines].sort((a, b) => a - b); + } + fs.writeFileSync("valid_lines.json", JSON.stringify(serializable)); + fs.writeFileSync("line_content_map.json", JSON.stringify(lineContentMap)); + fs.writeFileSync("annotated_diff.txt", annotatedLines.join("\n")); + + - name: Run Gemini Review + if: steps.get_diff.outputs.skip_review == 'false' + uses: actions/github-script@v7 + with: + script: | + const fs = require("fs"); + const annotated_diff = fs.readFileSync("annotated_diff.txt", "utf8"); + const pr_title = context.payload.pull_request.title; + const pr_body = context.payload.pull_request.body || "내용 없음"; + const { GoogleGenerativeAI } = require("@google/generative-ai"); + const genAI = new GoogleGenerativeAI("${{ secrets.GEMINI_API_KEY }}"); + const model = genAI.getGenerativeModel({ + model: "gemini-2.5-flash", + generationConfig: { + responseMimeType: "application/json", + }, + }); + const prompt = `You are a senior iOS engineer performing a code review on a Swift 6 / SwiftUI / TCA 1.25 multi-module Clean Architecture project built with Tuist 4. + + [Tone and Style Guidelines] + - Do NOT include unnecessary praise, greetings, or overly verbose explanations. + - Do NOT provide unsolicited CS insights (컴퓨터 과학적 통찰) or related interview questions. + - Provide concise, objective, and well-organized feedback suitable for immediate practical use. + + CRITICAL LINE NUMBER RULES (MUST FOLLOW): + - Each diff line is annotated with [LINE N] for added lines or [CTX N] for context lines. + - You MUST only comment on [LINE N] lines (added/modified code). NEVER comment on [CTX] or [DEL] lines. + - Use the EXACT number N shown in the [LINE N] annotation. Do NOT compute line numbers yourself. + - The "code_snippet" field must contain the actual code from that [LINE N] line. + + [PR Context] + Title: ${pr_title} + Description: ${pr_body} + + [Project Architecture] + - Stack: Swift 6, SwiftUI, TCA 1.25, Tuist 4 (multi-module) + - Layer dependency: Presentation → Domain ← Data, Network is only referenced by Data + - Deployment target: iOS 26.0 (iPhone only) + - Source roots: Projects/App, Projects/Presentation, Projects/Domain/{Entity,UseCase,DomainInterface,DataInterface}, Projects/Data/{Model,Repository,API,Service}, Projects/Network/*, Projects/Shared/* + + [Review Criteria] + 1. TCA Convention: Verify @Reducer + @ObservableState usage; Action naming describes events that occurred (e.g., xxxButtonTapped, xxxResponse), NOT intended effects (e.g., performLogin, loadData); Effect is .none when no side effect and .run for async work; shared logic lives in private methods, NOT shared Actions; Effect.run must NOT capture entire @ObservableState (extract needed values first); Reducer must NOT perform CPU-intensive work (offload to Effect); Store.scope must use stored property paths only (no computed transforms); Navigation uses @Reducer enum; transient UI state (hover, focus, animation) stays in SwiftUI @State, not TCA State. + 2. Module Architecture: Respect Presentation → Domain ← Data dependency direction; Network is only imported by Data; module boundaries expose protocols (DomainInterface / DataInterface); DTO-to-Entity mapping stays in Data layer. + 3. SwiftUI Convention: SubViews are structs (NOT @ViewBuilder functions); use @Binding when a SubView mutates parent @State; no "View" suffix in View names (unless clarity requires it); use .frame(maxWidth/maxHeight: .infinity) instead of Spacer() for simple expansion; required props via init, optional props via ViewModifier-style functions. + 4. Swift Code Quality: guard early return with shorthand optional binding (guard let value else { ... }) followed by a blank line; final class by default; private first (avoid fileprivate unless required); never force unwrap; operator line break puts operator at the start of the next line; function params line-break with closing paren on its own line; ternary for simple return/assignment only, split on '?'; [weak self] + guard let self else { return } in closures; constant groups as private enum (Metric/Font/Constant), NOT struct; empty collection literals ([] / [:]); indent 4 spaces; 120-char line limit. + 5. Actionable Feedback: When improvement is needed you MUST provide a concrete Swift fix using GitHub's \`\`\`suggestion block. + + [Severity Prefix] + Each comment body MUST start with a severity tag on its own line, then a blank line, then the actual comment: + - 🔴 [P1] Critical: force-unwrap crash risk, retain cycles / memory leaks, heavy or blocking work inside a Reducer, main-thread blocking + - 🟠 [P2] Major: module dependency-direction violations (e.g., Domain importing Data), Effect.run capturing entire @ObservableState, sharing logic through Actions, Store.scope with computed property, serious concurrency or error-mapping issues + - 🟡 [P3] Minor: Action naming that describes intent/effect (performLogin, loadData, setRecords), SubView written as @ViewBuilder function, Swift API Design Guideline violations on public APIs, inefficient Effect composition + - 🔵 [P4] Readability: View-suffix naming, Spacer() misuse, missing final / private, guard / ternary / line-break style violations, constant groups declared as struct instead of enum + - ⚪ [P5] Nitpick: typos, whitespace, formatting + + Format: "🔴 **[P1] Critical**\\n\\nActual comment content here..." + + Ignore comments and formatting-only changes. Write all review comments in Korean using Markdown, without greetings or closings. + + Respond ONLY with a JSON object in this exact format: + { + "summary": "전체 리뷰 요약 (한국어, 마크다운)", + "comments": [ + { + "path": "file path relative to repo root (from the b/ prefix in diff)", + "line": , + "code_snippet": "the actual code content from that line", + "body": "🔴/🟠/🟡/🔵/⚪ **[P1~P5] Label**\\n\\n리뷰 코멘트 (한국어, 마크다운. 개선이 필요하면 \\\`\\\`\\\`suggestion 블록 포함)" + } + ] + } + If no issues are found, return {"summary": "...", "comments": []}. + + + ${annotated_diff} + `; + const result = await model.generateContent(prompt); + const text = result.response.text(); + fs.writeFileSync("review_result.json", text); + + - name: Post Inline Review Comments + if: steps.get_diff.outputs.skip_review == 'false' + uses: actions/github-script@v7 + with: + script: | + const fs = require("fs"); + const raw = fs.readFileSync("review_result.json", "utf8"); + const validLinesMap = JSON.parse(fs.readFileSync("valid_lines.json", "utf8")); + const lineContentMap = JSON.parse(fs.readFileSync("line_content_map.json", "utf8")); + let review; + try { + review = JSON.parse(raw); + } catch (e) { + console.log("JSON parse failed, falling back to single comment"); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + body: raw, + }); + return; + } + + function snapToValidLine(comment) { + const fileLines = validLinesMap[comment.path]; + if (!fileLines || fileLines.length === 0) { + console.log(`Dropping comment: file not in diff - ${comment.path}`); + return null; + } + if (fileLines.includes(comment.line)) return comment.line; + + const fileContent = lineContentMap[comment.path] || {}; + if (comment.code_snippet) { + const snippet = comment.code_snippet.trim(); + for (const validLine of fileLines) { + const content = fileContent[validLine] || ""; + if (content.includes(snippet) || snippet.includes(content)) { + return validLine; + } + } + } + + const THRESHOLD = 5; + let bestLine = null; + let bestDist = THRESHOLD + 1; + for (const validLine of fileLines) { + const dist = Math.abs(validLine - comment.line); + if (dist < bestDist) { + bestDist = dist; + bestLine = validLine; + } + } + if (bestLine !== null) return bestLine; + return null; + } + + const reviewComments = (review.comments || []) + .map((c) => { + const snappedLine = snapToValidLine(c); + if (snappedLine === null) return null; + return { + path: c.path, + line: snappedLine, + side: "RIGHT", + body: c.body, + }; + }) + .filter(Boolean); + + if (reviewComments.length > 0) { + await github.rest.pulls.createReview({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.pull_request.number, + event: "COMMENT", + body: review.summary || "", + comments: reviewComments, + }); + } else if (review.summary) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + body: review.summary, + }); + } From 338cce282836c6654731b7aef66f2aa510eceb84 Mon Sep 17 00:00:00 2001 From: Roy Date: Tue, 19 May 2026 01:21:18 +0900 Subject: [PATCH 09/21] =?UTF-8?q?feat:=20#19=20Kingfisher=20=EA=B8=80?= =?UTF-8?q?=EB=A1=9C=EB=B2=8C=20requestModifier=20=E2=80=94=20=EB=B3=B4?= =?UTF-8?q?=ED=98=B8=20=EC=9D=B4=EB=AF=B8=EC=A7=80(/api/v1/resources/...)?= =?UTF-8?q?=20=EC=9E=90=EB=8F=99=20Bearer=20=ED=86=A0=ED=81=B0=20=EC=B2=A8?= =?UTF-8?q?=EB=B6=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - App 모듈에 .SPM.kingfisher 의존성 추가 - KingfisherConfigurator: AnyModifier 로 매 요청마다 KeychainManaging.accessToken() 을 Authorization 헤더에 자동 첨부 - AppDelegate 의 DI bootstrap 직후 호출하여 모든 KFImage 가 인증 토큰을 갖고 동작 --- Projects/App/Project.swift | 9 ++-- .../App/Sources/Application/AppDelegate.swift | 44 ++++++++++++------- .../Application/KingfisherConfigurator.swift | 33 ++++++++++++++ 3 files changed, 65 insertions(+), 21 deletions(-) create mode 100644 Projects/App/Sources/Application/KingfisherConfigurator.swift diff --git a/Projects/App/Project.swift b/Projects/App/Project.swift index 0668f4e..a37d865 100644 --- a/Projects/App/Project.swift +++ b/Projects/App/Project.swift @@ -1,7 +1,7 @@ -import ProjectDescription +import DependencyPackagePlugin import DependencyPlugin +import ProjectDescription import ProjectTemplatePlugin -import DependencyPackagePlugin let project = Project.makeAppModule( name: Project.Environment.appName, @@ -16,8 +16,9 @@ let project = Project.makeAppModule( .SPM.googleMobileAds, .SPM.firebaseCrashlytics, .SPM.mixpanel, - .SPM.mixpanelSessionReplay - + .SPM.mixpanelSessionReplay, + .SPM.kingfisher, + ], sources: ["Sources/**"], resources: ["Resources/**"], diff --git a/Projects/App/Sources/Application/AppDelegate.swift b/Projects/App/Sources/Application/AppDelegate.swift index f8b2252..d1f69e4 100644 --- a/Projects/App/Sources/Application/AppDelegate.swift +++ b/Projects/App/Sources/Application/AppDelegate.swift @@ -1,24 +1,25 @@ -import UIKit -import WeaveDI import Firebase import GoogleMobileAds import LogMacro import Mixpanel import MixpanelSessionReplay +import UIKit +import WeaveDI +import DomainInterface class AppDelegate: UIResponder, UIApplicationDelegate { let mixPanelKey = Bundle.main.object(forInfoDictionaryKey: "MIXPANEL_TOKEN") as? String func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + _: UIApplication, + didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { FirebaseApp.configure() #logDebug( "Mixpanel initialize", [ "token_exists": !(mixPanelKey?.isEmpty ?? true), - "token_prefix": String((mixPanelKey ?? "").prefix(6)) + "token_prefix": String((mixPanelKey ?? "").prefix(6)), ] ) Mixpanel.initialize(token: mixPanelKey ?? "", trackAutomaticEvents: true) @@ -40,32 +41,41 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // DI 관리자 초기화 WeaveDI.Container.bootstrapInTask { @DIContainerActor _ in await AppDIManager.shared.registerDefaultDependencies() + + // Kingfisher 글로벌 requestModifier 등록 — DI 등록 직후라 KeychainManaging resolve 보장 + if let keychainManager = UnifiedDI.resolve(KeychainManaging.self) { + await MainActor.run { + KingfisherConfigurator.configureAuthorizedDownloader( + keychainManager: keychainManager + ) + } + } } return true } - + func application( - _ application: UIApplication, + _: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, - options: UIScene.ConnectionOptions + options _: UIScene.ConnectionOptions ) -> UISceneConfiguration { - return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) } - + func application( - _ application: UIApplication, - didDiscardSceneSessions sceneSessions: Set - ) { - } - + _: UIApplication, + didDiscardSceneSessions _: Set + ) {} + // MARK: - Image Caching Configuration + private func initializeMixpanelSessionReplay() { guard !(mixPanelKey?.isEmpty ?? true) else { return } - + var config = MPSessionReplayConfig(wifiOnly: false) config.enableSessionReplayOniOS26AndLater = true - + MPSessionReplay.initialize( token: Mixpanel.mainInstance().apiToken, distinctId: Mixpanel.mainInstance().distinctId, diff --git a/Projects/App/Sources/Application/KingfisherConfigurator.swift b/Projects/App/Sources/Application/KingfisherConfigurator.swift new file mode 100644 index 0000000..6500fe4 --- /dev/null +++ b/Projects/App/Sources/Application/KingfisherConfigurator.swift @@ -0,0 +1,33 @@ +// +// KingfisherConfigurator.swift +// App +// +// Picke 백엔드 보호 이미지(`/api/v1/resources/...`) 를 KFImage 로 로딩하려면 +// 요청마다 Bearer 토큰이 필요하다. 앱 시작 시 한 번만 호출해서 +// KingfisherManager 의 defaultOptions 에 글로벌 requestModifier 를 등록한다. +// + +import Foundation + +import Kingfisher + +import DomainInterface +import Foundations + +enum KingfisherConfigurator { + static func configureAuthorizedDownloader( + keychainManager: KeychainManaging + ) { + let modifier = AnyModifier { request in + var req = request + if let token = keychainManager.accessToken(), !token.isEmpty { + req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + } + return req + } + + KingfisherManager.shared.defaultOptions = [ + .requestModifier(modifier), + ] + } +} From e52b8946d886b58abab6f0ed214745c5ac24e1c6 Mon Sep 17 00:00:00 2001 From: Roy Date: Tue, 19 May 2026 01:21:37 +0900 Subject: [PATCH 10/21] =?UTF-8?q?feat:=20#3=20=ED=99=88=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C/=EC=84=B9=EC=85=98=20.pen=20=EC=A0=95=EB=B0=80=20?= =?UTF-8?q?=EB=A7=A4=EC=B9=AD=20+=20Vote=20=EC=B9=B4=EB=93=9C=20Property?= =?UTF-8?q?=201=3DDefault=20=E2=86=94=20Result=20=EC=9D=B8=ED=84=B0?= =?UTF-8?q?=EB=9E=99=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - VoteCardView: 선택 전 단순 옵션 4개 → 선택 후 빈칸 채움 + secondary100/500 percentage bar 4 row (.pen `Radar Wrap`), 0 → target width easeOut 0.6s 애니메이션 - QuizCardView: .pen 단일 상태로 복원 (Result variant 없음) - HomeSectionHeader: title 안의 라틴 단어(Best · Pické)와 한글 `배틀` 을 primary500 으로 자동 강조 (AttributedString) - HeroCarouselView: thumbnail 영역 정밀 매핑 (375 wide + GeometryReader + clipped), var → @ViewBuilder func 변환 - NewBattleCardView: thumbnail 95h 복원 + 아바타 베이지 원형 배경 + 16×28 일러스트 ( .pen `Avatar/Philosopher` Frame 195 매칭) - HotBattle mock 에 picsum thumbnail URL 추가하여 trendingBattles 빈 응답 시에도 이미지 표시 - HomeView · HomeSkeletonView 의 inner sub-view 들을 `@ViewBuilder` 명시 --- .../Entity/Sources/Home/HotBattle.swift | 4 + .../View/Components/HeroCarouselView.swift | 113 ++++++++----- .../View/Components/HomeSectionHeader.swift | 27 ++- .../View/Components/NewBattleCardView.swift | 48 +++--- .../Main/View/Components/QuizCardView.swift | 20 ++- .../Main/View/Components/VoteCardView.swift | 158 ++++++++++++++---- .../Sources/Main/View/HomeSkeletonView.swift | 3 + .../Home/Sources/Main/View/HomeView.swift | 6 +- 8 files changed, 271 insertions(+), 108 deletions(-) diff --git a/Projects/Domain/Entity/Sources/Home/HotBattle.swift b/Projects/Domain/Entity/Sources/Home/HotBattle.swift index d75afa5..42c5077 100644 --- a/Projects/Domain/Entity/Sources/Home/HotBattle.swift +++ b/Projects/Domain/Entity/Sources/Home/HotBattle.swift @@ -41,6 +41,7 @@ public extension HotBattle { static let mocks: [HotBattle] = [ .init( battleId: 11, + thumbnailURL: URL(string: "https://picsum.photos/seed/picke-hot-1/400/250"), title: "인간은 본래 선한가, 악한가?", tags: [.init(tagId: 301, name: "#철학", type: .category)], audioDuration: 8 * 60, @@ -48,6 +49,7 @@ public extension HotBattle { ), .init( battleId: 12, + thumbnailURL: URL(string: "https://picsum.photos/seed/picke-hot-2/400/250"), title: "안락사 도입, 당신의 입장은?", tags: [.init(tagId: 302, name: "#역사", type: .category)], audioDuration: 5 * 60, @@ -55,6 +57,7 @@ public extension HotBattle { ), .init( battleId: 13, + thumbnailURL: URL(string: "https://picsum.photos/seed/picke-hot-3/400/250"), title: "노키즈존, 영업의 자유인가?", tags: [.init(tagId: 303, name: "#사회", type: .category)], audioDuration: 5 * 60, @@ -62,6 +65,7 @@ public extension HotBattle { ), .init( battleId: 14, + thumbnailURL: URL(string: "https://picsum.photos/seed/picke-hot-4/400/250"), title: "AI는 의식을 가질 수 있는가?", tags: [.init(tagId: 304, name: "#과학", type: .category)], audioDuration: 6 * 60, diff --git a/Projects/Presentation/Home/Sources/Main/View/Components/HeroCarouselView.swift b/Projects/Presentation/Home/Sources/Main/View/Components/HeroCarouselView.swift index f98b344..902170f 100644 --- a/Projects/Presentation/Home/Sources/Main/View/Components/HeroCarouselView.swift +++ b/Projects/Presentation/Home/Sources/Main/View/Components/HeroCarouselView.swift @@ -19,6 +19,9 @@ struct HeroCarouselView: View { var onTap: (HeroBattle) -> Void = { _ in } private static let autoScrollInterval: TimeInterval = 3 + private static let controlHeight: CGFloat = 51 + private static let thumbnailHeight: CGFloat = 220 + private static let subjectHeight: CGFloat = 88 private let timer = Timer.publish(every: autoScrollInterval, on: .main, in: .common).autoconnect() var body: some View { @@ -35,7 +38,7 @@ struct HeroCarouselView: View { } } .tabViewStyle(.page(indexDisplayMode: .never)) - .frame(height: 341) // .pen 합: control(53) + thumbnail(167) + subject(121) + .frame(height: Self.controlHeight + Self.thumbnailHeight + Self.subjectHeight) // .pen 합: control(53) + thumbnail(167) + subject(121) .background(Color.neutral800) .onReceive(timer) { _ in advance() } } @@ -54,17 +57,20 @@ struct HeroCardView: View { let position: Int let total: Int + private let thumbnailHeight: CGFloat = 220 + var body: some View { VStack(spacing: 0) { - controlRow - thumbnail - subject + controlRow() + thumbnail() + subject() } - .background(Color.neutral800) - .frame(maxWidth: .infinity) + .background(.neutral800) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) } - private var controlRow: some View { + @ViewBuilder + private func controlRow() -> some View { HStack { Text(hero.badge) .pretendardFont(family: .SemiBold, size: 11) @@ -91,44 +97,54 @@ struct HeroCardView: View { .padding(16) } - private var thumbnail: some View { - ZStack { - if let url = hero.thumbnailURL { - KFImage(url) - .resizable() - .scaledToFill() - .frame(height: 167) - .clipped() - Color.black.opacity(0.4) // .pen 의 "#00000066" 오버레이 - } else { - Color.neutral500.opacity(0.4) - } + @ViewBuilder + private func thumbnail() -> some View { + GeometryReader { proxy in + ZStack { + Rectangle() + .fill(.neutral500.opacity(0.4)) - HStack(spacing: 24) { - Text(hero.optionA) - .pretendardFont(family: .SemiBold, size: 14) - .foregroundStyle(.beige100) - ZStack { - Circle() - .stroke(.secondary50.opacity(0.2), lineWidth: 2) - .frame(width: 32, height: 32) - Text("VS") - .pretendardFont(family: .SemiBold, size: 11) - .foregroundStyle(.secondary50) + if let url = hero.thumbnailURL { + KFImage(url) + .resizable() + .aspectRatio(contentMode: .fill) + .frame( + width: proxy.size.width, + height: proxy.size.height, + alignment: .top + ) + .clipped() } - Text(hero.optionB) - .pretendardFont(family: .SemiBold, size: 14) - .foregroundStyle(.beige100) + + Color.black.opacity(0.4) + + HStack(spacing: 24) { + Text(hero.optionA) + .pretendardFont(family: .SemiBold, size: 14) + .foregroundStyle(.beige100) + ZStack { + Circle() + .stroke(.secondary50.opacity(0.2), lineWidth: 2) + .frame(width: 32, height: 32) + Text("VS") + .pretendardFont(family: .SemiBold, size: 11) + .foregroundStyle(.secondary50) + } + Text(hero.optionB) + .pretendardFont(family: .SemiBold, size: 14) + .foregroundStyle(.beige100) + } + .opacity(0.85) } - .opacity(0.85) } - .frame(height: 167) + .frame(height: thumbnailHeight) .clipped() } - private var subject: some View { + @ViewBuilder + private func subject() -> some View { HStack(alignment: .bottom) { - VStack(alignment: .leading, spacing: 4) { + VStack(alignment: .leading, spacing: 0) { Text(hero.title) .pretendardFont(family: .SemiBold, size: 16) .foregroundStyle(.beige100) @@ -136,18 +152,25 @@ struct HeroCardView: View { .pretendardFont(family: .Medium, size: 12) .foregroundStyle(.neutral200) .lineLimit(2) - HStack(spacing: 4) { - ForEach(hero.tags) { tag in - Text(tag.name) - .pretendardFont(family: .Medium, size: 11) - .foregroundStyle(.neutral200) + + if !hero.tags.isEmpty { + HStack(spacing: 4) { + ForEach(hero.tags) { tag in + Text(tag.name) + .pretendardFont(family: .Medium, size: 11) + .foregroundStyle(.neutral200) + } } + .padding(.top, 6) } - .padding(.top, 2) } + Spacer() - MetaLabelView(systemImage: "eye", text: "\(hero.viewCount)") + + MetaLabelView(systemImage: "eye", text: "\(hero.viewCount.formatted())") } - .padding(20) + .padding(.horizontal, 20) + .padding(.top, 20) + .padding(.bottom, 20) } } diff --git a/Projects/Presentation/Home/Sources/Main/View/Components/HomeSectionHeader.swift b/Projects/Presentation/Home/Sources/Main/View/Components/HomeSectionHeader.swift index b5388dc..9575096 100644 --- a/Projects/Presentation/Home/Sources/Main/View/Components/HomeSectionHeader.swift +++ b/Projects/Presentation/Home/Sources/Main/View/Components/HomeSectionHeader.swift @@ -16,10 +16,9 @@ struct HomeSectionHeader: View { var body: some View { HStack(spacing: 12) { - Text(title) + Text(attributedTitle) .pretendardFont(family: .Bold, size: 18) .kerning(-0.45) - .foregroundStyle(.neutral900) Spacer(minLength: 0) Button(action: onSeeMoreTapped) { Text("더 보기") @@ -29,4 +28,28 @@ struct HomeSectionHeader: View { } .padding(.horizontal, 16) } + + /// 강조 규칙: + /// - 라틴 단어 (Best · Pické 등) 가 있으면 그 단어만 primary500, 나머지 한글은 neutral900 + /// - 라틴 단어가 없으면 한글 `배틀` 만 primary500, 나머지 한글은 neutral900 + private var attributedTitle: AttributedString { + var attr = AttributedString(title) + attr.foregroundColor = .neutral900 + + let latinPattern = /[A-Za-zÀ-ÿ]+/ + let latinMatches = Array(title.matches(of: latinPattern)) + + if latinMatches.isEmpty { + if let range = attr.range(of: "배틀") { + attr[range].foregroundColor = .primary500 + } + } else { + for match in latinMatches { + if let range = attr.range(of: String(match.0)) { + attr[range].foregroundColor = .primary500 + } + } + } + return attr + } } diff --git a/Projects/Presentation/Home/Sources/Main/View/Components/NewBattleCardView.swift b/Projects/Presentation/Home/Sources/Main/View/Components/NewBattleCardView.swift index 1c0c108..a090bc4 100644 --- a/Projects/Presentation/Home/Sources/Main/View/Components/NewBattleCardView.swift +++ b/Projects/Presentation/Home/Sources/Main/View/Components/NewBattleCardView.swift @@ -17,18 +17,21 @@ struct NewBattleCardView: View { let battle: NewBattle var body: some View { - content - .padding(12) - .background(.beige50, in: RoundedRectangle(cornerRadius: 2)) - .overlay( - RoundedRectangle(cornerRadius: 2).stroke(.beige600, lineWidth: 1) - ) + VStack(alignment: .leading, spacing: 8) { + content + } + .padding(12) + .background(.beige50, in: RoundedRectangle(cornerRadius: 2)) + .overlay( + RoundedRectangle(cornerRadius: 2).stroke(.beige600, lineWidth: 1) + ) } } // MARK: - Sections extension NewBattleCardView { + @ViewBuilder private var content: some View { VStack(alignment: .leading, spacing: 12) { container @@ -36,6 +39,7 @@ extension NewBattleCardView { } } + @ViewBuilder private var container: some View { VStack(alignment: .leading, spacing: 12) { metaRow @@ -43,6 +47,7 @@ extension NewBattleCardView { } } + @ViewBuilder private var metaRow: some View { HStack(spacing: 10) { if let tag = battle.tags.first { @@ -59,6 +64,7 @@ extension NewBattleCardView { } } + @ViewBuilder private var titleBlock: some View { VStack(alignment: .leading, spacing: 4) { Text(battle.title) @@ -76,6 +82,7 @@ extension NewBattleCardView { } } + @ViewBuilder private var versusRow: some View { HStack(spacing: 8) { admissionButton( @@ -96,6 +103,7 @@ extension NewBattleCardView { // MARK: - Sub-components extension NewBattleCardView { + @ViewBuilder private func admissionButton( label: String, sub: String, @@ -123,26 +131,26 @@ extension NewBattleCardView { } @ViewBuilder - private func avatar(for philosopherName: String, imageURL: URL?) -> some View { - if let imageURL { - KFImage(imageURL) - .resizable() - .scaledToFill() - .frame(width: 40, height: 40) - .clipShape(Circle()) - } else if let asset = PhilosopherAvatar(rawValue: philosopherName)?.imageAsset { - Image(asset: asset) - .resizable() - .scaledToFit() - .frame(width: 40, height: 40) - .background(.beige600, in: Circle()) - } else { + private func avatar( + for philosopherName: String, + imageURL: URL? + ) -> some View { + // .pen `Avatar/Philosopher` 매핑: 베이지 40×40 원형 배경 + 가운데 16×28 일러스트 + ZStack { Circle() .fill(.beige600) .frame(width: 40, height: 40) + if let imageURL { + KFImage(imageURL) + .resizable() + .scaledToFit() + .frame(width: 20, height: 38) + } } + .frame(width: 40, height: 40) } + @ViewBuilder private var vsBadge: some View { Text("VS") .pretendardFont(family: .Bold, size: 8) diff --git a/Projects/Presentation/Home/Sources/Main/View/Components/QuizCardView.swift b/Projects/Presentation/Home/Sources/Main/View/Components/QuizCardView.swift index 4eba39b..f1e17dd 100644 --- a/Projects/Presentation/Home/Sources/Main/View/Components/QuizCardView.swift +++ b/Projects/Presentation/Home/Sources/Main/View/Components/QuizCardView.swift @@ -4,21 +4,23 @@ // // Created by Wonji Suh on 5/15/26. // +// Pencil .pen `Card/Quiz` — 단일 상태 (Property variant 없음). +// import SwiftUI import DesignSystem import Entity -/// "오늘의 Pické — 퀴즈" 카드. +/// "오늘의 Pické — 퀴즈" 카드. (선택/결과 상태 분리 없음 — .pen 디자인 단일) struct QuizCardView: View { let question: QuizQuestion var body: some View { VStack(alignment: .leading, spacing: 20) { - header - titleBlock - options + header() + titleBlock() + options() } .padding(.vertical, 20) .padding(.horizontal, 16) @@ -28,7 +30,8 @@ struct QuizCardView: View { ) } - private var header: some View { + @ViewBuilder + private func header() -> some View { HStack { TagBadgeView(text: "퀴즈") Spacer() @@ -38,7 +41,8 @@ struct QuizCardView: View { } } - private var titleBlock: some View { + @ViewBuilder + private func titleBlock() -> some View { VStack(alignment: .leading, spacing: 6) { Text(question.title) .pretendardFont(family: .SemiBold, size: 15) @@ -52,13 +56,15 @@ struct QuizCardView: View { } } - private var options: some View { + @ViewBuilder + private func options() -> some View { HStack(spacing: 8) { option(label: question.itemA, desc: question.itemADesc) option(label: question.itemB, desc: question.itemBDesc) } } + @ViewBuilder private func option(label: String, desc: String) -> some View { VStack(spacing: 2) { Text(label) diff --git a/Projects/Presentation/Home/Sources/Main/View/Components/VoteCardView.swift b/Projects/Presentation/Home/Sources/Main/View/Components/VoteCardView.swift index 8337b55..9e0a202 100644 --- a/Projects/Presentation/Home/Sources/Main/View/Components/VoteCardView.swift +++ b/Projects/Presentation/Home/Sources/Main/View/Components/VoteCardView.swift @@ -4,7 +4,7 @@ // // Created by Wonji Suh on 5/15/26. // -// Pencil .pen `wZ4Yt` (Card/Vote) 기준으로 1:1 매핑. +// Pencil .pen `wZ4Yt` (Card/Vote) — Property 1=Default · Property 1=Result 두 상태. // import SwiftUI @@ -12,34 +12,51 @@ import SwiftUI import DesignSystem import Entity -/// "오늘의 Pické — 투표" 카드. +/// "오늘의 Pické — 투표" 카드. 옵션 탭 시 result 모드로 전환되어 +/// 빈칸에 선택지 텍스트가 채워지고 옵션 박스 아래에 percentage bar 들이 표시된다. struct VoteCardView: View { let question: VoteQuestion + @State private var selectedIndex: Int? + @State private var animatedFill: Bool = false + + private var isResultMode: Bool { selectedIndex != nil } + + private var selectedLabel: String? { + guard let idx = selectedIndex else { return nil } + return question.options[safe: idx]?.title + } + private let columns = [ GridItem(.flexible(), spacing: 8), GridItem(.flexible(), spacing: 8), ] + /// API 가 결과 비율을 내려주기 전까지 사용하는 임시 mock 비율. + private static let mockPercentages: [Int] = [45, 25, 20, 10] + var body: some View { VStack(alignment: .leading, spacing: 20) { - header - heading - grid + header() + heading() + grid() + if isResultMode { + resultBars() + } } .padding(.vertical, 20) .padding(.horizontal, 16) .background(.beige50, in: RoundedRectangle(cornerRadius: 2)) .overlay( - RoundedRectangle(cornerRadius: 2).stroke(.beige700, lineWidth: 1) + RoundedRectangle(cornerRadius: 2).stroke(.beige600, lineWidth: 1) ) } - private var header: some View { + @ViewBuilder + private func header() -> some View { HStack { Text("투표") .pretendardFont(family: .SemiBold, size: 14) - .kerning(-0.35) .foregroundStyle(.primary500) .frame(width: 35, height: 21) .background(.beige600, in: RoundedRectangle(cornerRadius: 2)) @@ -52,56 +69,131 @@ struct VoteCardView: View { } } - private var heading: some View { + @ViewBuilder + private func heading() -> some View { VStack(spacing: 6) { HStack(spacing: 4) { Text(question.titlePrefix) .pretendardFont(family: .SemiBold, size: 15) - .kerning(-0.375) - .foregroundStyle(.neutral900) + .foregroundStyle(.neutral500) - RoundedRectangle(cornerRadius: 2) - .fill(.beige200) - .frame(width: 52, height: 24) - .overlay( - RoundedRectangle(cornerRadius: 2) - .stroke(.beige700, lineWidth: 1) - ) + answerSlot() Text(question.titleSuffix) .pretendardFont(family: .SemiBold, size: 15) - .kerning(-0.375) - .foregroundStyle(.neutral900) + .foregroundStyle(.neutral500) } Text(question.summary) .pretendardFont(family: .Medium, size: 12) - .foregroundStyle(.neutral200) + .foregroundStyle(.neutral300) } .frame(maxWidth: .infinity) } - private var grid: some View { - LazyVGrid(columns: columns, spacing: 8) { + /// 빈칸: 선택 전엔 빈 placeholder, 선택 후엔 선택된 옵션 텍스트 표시. + @ViewBuilder + private func answerSlot() -> some View { + if let label = selectedLabel { + Text(label) + .pretendardFont(family: .SemiBold, size: 15) + .foregroundStyle(.primary500) + .frame(width: 52, height: 24) + .background(.beige200, in: RoundedRectangle(cornerRadius: 2)) + .overlay( + RoundedRectangle(cornerRadius: 2) + .stroke(.primary500, lineWidth: 1) + ) + } else { + RoundedRectangle(cornerRadius: 2) + .fill(.beige200) + .frame(width: 52, height: 24) + .overlay( + RoundedRectangle(cornerRadius: 2) + .stroke(.beige600, lineWidth: 1) + ) + } + } + + @ViewBuilder + private func grid() -> some View { + LazyVGrid(columns: columns, spacing: 7) { ForEach(Array(question.options.enumerated()), id: \.offset) { idx, option in - optionButton(index: idx + 1, label: option.title) + optionButton(index: idx, label: option.title) } } } + @ViewBuilder private func optionButton(index: Int, label: String) -> some View { - HStack(spacing: 2) { - Text("\(index).") - .pretendardFont(family: .SemiBold, size: 10) - .foregroundStyle(.secondary900) + let isSelected = selectedIndex == index + + Button { + withAnimation(.easeInOut(duration: 0.2)) { + selectedIndex = isSelected ? nil : index + } + } label: { Text(label) .pretendardFont(family: .SemiBold, size: 13) .foregroundStyle(.neutral900) + .frame(maxWidth: .infinity, minHeight: 44) + .background(.beige400, in: RoundedRectangle(cornerRadius: 2)) + .overlay( + RoundedRectangle(cornerRadius: 2) + .stroke(isSelected ? .primary500 : .beige600, lineWidth: isSelected ? 1.5 : 1) + ) } - .frame(maxWidth: .infinity, minHeight: 44) - .background(.beige300, in: RoundedRectangle(cornerRadius: 2)) - .overlay( - RoundedRectangle(cornerRadius: 2).stroke(.beige600, lineWidth: 1) - ) + .buttonStyle(.plain) + } + + /// Result mode 옵션 박스 아래 별도 영역 — 4 row (옵션명 + bar + percentage). + /// .pen `Radar Wrap` 디자인을 2-column 으로 재배치. + @ViewBuilder + private func resultBars() -> some View { + LazyVGrid(columns: columns, spacing: 8) { + ForEach(Array(question.options.enumerated()), id: \.offset) { idx, option in + resultBarRow( + label: option.title, + percentage: Self.mockPercentages[safe: idx] ?? 0 + ) + } + } + .padding(.top, 4) + .onAppear { + animatedFill = false + withAnimation(.easeOut(duration: 0.6)) { + animatedFill = true + } + } + .onDisappear { + animatedFill = false + } + } + + @ViewBuilder + private func resultBarRow(label: String, percentage: Int) -> some View { + HStack(spacing: 6) { + Text(label) + .pretendardFont(family: .Medium, size: 10) + .foregroundStyle(.neutral400) + ZStack(alignment: .leading) { + RoundedRectangle(cornerRadius: 1) + .fill(.secondary100) + .frame(width: 48, height: 4) + RoundedRectangle(cornerRadius: 1) + .fill(.secondary500) + .frame(width: animatedFill ? 48 * CGFloat(percentage) / 100 : 0, height: 4) + } + Spacer(minLength: 0) + Text("\(percentage)%") + .pretendardFont(family: .Bold, size: 11) + .foregroundStyle(.neutral500) + } + } +} + +private extension Array { + subscript(safe index: Int) -> Element? { + indices.contains(index) ? self[index] : nil } } diff --git a/Projects/Presentation/Home/Sources/Main/View/HomeSkeletonView.swift b/Projects/Presentation/Home/Sources/Main/View/HomeSkeletonView.swift index 82bc2cb..ce7b709 100644 --- a/Projects/Presentation/Home/Sources/Main/View/HomeSkeletonView.swift +++ b/Projects/Presentation/Home/Sources/Main/View/HomeSkeletonView.swift @@ -134,6 +134,7 @@ private struct HomeTodayPickeSkeletonView: View { } } + @ViewBuilder private func todayQuizCard() -> some View { VStack(alignment: .leading, spacing: 20) { HStack { @@ -161,6 +162,7 @@ private struct HomeTodayPickeSkeletonView: View { ) } + @ViewBuilder private func todayVoteCard() -> some View { VStack(alignment: .leading, spacing: 20) { HStack { @@ -304,6 +306,7 @@ private struct SkeletonShimmerModifier: ViewModifier { } } + @ViewBuilder private var shimmer: some View { GeometryReader { proxy in LinearGradient( diff --git a/Projects/Presentation/Home/Sources/Main/View/HomeView.swift b/Projects/Presentation/Home/Sources/Main/View/HomeView.swift index 2669202..2aea739 100644 --- a/Projects/Presentation/Home/Sources/Main/View/HomeView.swift +++ b/Projects/Presentation/Home/Sources/Main/View/HomeView.swift @@ -65,6 +65,7 @@ extension HomeView { store.newBattles.isEmpty } + @ViewBuilder private func hotBattlesSection() -> some View { VStack(alignment: .leading, spacing: 12) { HomeSectionHeader(title: "지금 뜨는 배틀") { @@ -83,6 +84,7 @@ extension HomeView { } } + @ViewBuilder private func bestBattlesSection() -> some View { VStack(alignment: .leading, spacing: 12) { HomeSectionHeader(title: "Best 배틀") { @@ -99,6 +101,7 @@ extension HomeView { } } + @ViewBuilder private func todayPickeSection() -> some View { VStack(alignment: .leading, spacing: 16) { HomeSectionHeader(title: "오늘의 Pické") { @@ -111,13 +114,14 @@ extension HomeView { if let vote = store.currentVote { VoteCardView(question: vote) .contentShape(Rectangle()) - .onTapGesture { send(.voteTapped(vote)) } + .onTapGesture { } } } .padding(.horizontal, 16) } } + @ViewBuilder private func newBattlesSection() -> some View { VStack(alignment: .leading, spacing: 16) { HomeSectionHeader(title: "새로운 배틀") { From 0cd82b9aa9b59ca12c174ca7ab53ef4ea6e3150c Mon Sep 17 00:00:00 2001 From: Roy Date: Tue, 19 May 2026 01:21:49 +0900 Subject: [PATCH 11/21] =?UTF-8?q?chore:=20SwiftUI=20sub-view=20`@ViewBuild?= =?UTF-8?q?er`=20=EB=AA=85=EC=8B=9C=20=EC=9D=BC=EA=B4=84=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20+=20AGENTS.md=20=EA=B7=9C=EC=B9=99=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 자식 ≥ 2개를 감싸거나 if/switch/ForEach 분기·반복이 있는 inner `private var`/`func` 에 `@ViewBuilder` 명시 (OnBoardingView · MainTabView · PreVoteView) - AGENTS.md `#### 🧱 @ViewBuilder 함수 vs var` 규칙 섹션 추가 — 자식 개수/분기 유무로 함수+@ViewBuilder vs var 결정 기준 명문화 --- AGENTS.md | 49 +++++++++++++++++++ .../OnBoarding/View/OnBoardingView.swift | 5 ++ .../Home/Sources/Vote/View/PreVoteView.swift | 12 +++++ .../MainTab/Sources/View/MainTabView.swift | 1 + 4 files changed, 67 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 7ae0966..2f62e84 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -148,6 +148,55 @@ public var body: some View { - 한 메서드 안에서 다시 큰 블록이 생기면 더 작게 쪼개기 (재귀 적용) - 공통 컴포넌트는 별도 파일 (`Components/*.swift`) 로 추출 +#### 🧱 `@ViewBuilder` 함수 vs `var` — 자식 개수 / 분기 유무로 결정 + +분리한 sub-view 의 선언 형태는 **자식 개수와 분기 유무** 로만 정한다. + +```swift +// ✅ 다중 자식을 감싸거나 if/else · switch 분기가 있으면 `@ViewBuilder` + 함수 +@ViewBuilder +private func hotBattlesSection() -> some View { + VStack(alignment: .leading, spacing: 12) { + HomeSectionHeader(title: "지금 뜨는 배틀") { send(.seeMoreTapped(.hotBattles)) } + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 16) { + ForEach(store.hotBattles) { HotBattleCardView(battle: $0) } + } + } + } +} + +@ViewBuilder +private func thumbnail(url: URL?) -> some View { + if let url { + KFImage(url).resizable().scaledToFill() + } else { + Color.neutral200 + } +} + +// ✅ 단일 뷰만 반환하면 `private var` 형태 +private var primaryButton: some View { + CustomButton( + action: { send(.primaryButtonTapped) }, + title: "사전 투표하기", + config: CustomButtonConfig.primary(.large, height: 52), + isEnable: store.isPrimaryButtonEnabled + ) +} + +// ❌ 금지 — VStack 으로 자식 여러 개 감싸는데 var 만 쓰는 경우 (분기 / 동적 children 추가 시 깨짐) +private var section: some View { + VStack { ... } // → @ViewBuilder + func 으로 가야 안전 +} +``` + +규칙: +- **`@ViewBuilder` + `private func`** : VStack/HStack/ZStack 등으로 **자식 ≥ 2개** 를 감싸거나 `if` / `switch` / `ForEach` 같은 분기·반복이 있을 때 +- **`private var ...: some View`** : **단일 뷰** 1개만 반환할 때 (단순 wrapping · CTA 버튼 · 단일 Image 등) +- body 안에서 호출하는 sub-view 가 인자가 필요하면 함수, 없으면 var 가 우선 — 기준은 "자식 수 / 분기 유무" 가 먼저 +- 레퍼런스: `HomeView.hotBattlesSection`, `PreVoteView.primaryButton`, `HeroCardView.thumbnail` + #### 🔤 폰트 — `.font(.system(...))` 금지, Pretendard 토큰 사용 ```swift diff --git a/Projects/Presentation/Auth/Sources/OnBoarding/View/OnBoardingView.swift b/Projects/Presentation/Auth/Sources/OnBoarding/View/OnBoardingView.swift index 043e487..b7f7774 100644 --- a/Projects/Presentation/Auth/Sources/OnBoarding/View/OnBoardingView.swift +++ b/Projects/Presentation/Auth/Sources/OnBoarding/View/OnBoardingView.swift @@ -39,6 +39,7 @@ public struct OnBoardingView: View { extension OnBoardingView { /// 상단: 타이틀 + 서브타이틀 + 일러스트 (페이지 스와이프 지원) + @ViewBuilder private func topSection() -> some View { TabView(selection: $store.currentIndex) { ForEach(OnBoardingFeature.pages) { page in @@ -50,6 +51,7 @@ extension OnBoardingView { .animation(.easeInOut(duration: 0.25), value: store.currentIndex) } + @ViewBuilder private func pageContent(_ page: OnBoardingFeature.Page) -> some View { VStack(spacing: 40) { titleBlock(page) @@ -57,6 +59,7 @@ extension OnBoardingView { } } + @ViewBuilder private func titleBlock(_ page: OnBoardingFeature.Page) -> some View { VStack(spacing: 12) { Text(page.title) @@ -75,6 +78,7 @@ extension OnBoardingView { .padding(.horizontal, 16) } + @ViewBuilder private func illustration(for page: OnBoardingFeature.Page) -> some View { Image(asset: page.imageAsset) .resizable() @@ -83,6 +87,7 @@ extension OnBoardingView { } /// 하단: indicator + CTA 버튼 (Frame 324) + @ViewBuilder private func bottomSection() -> some View { VStack(spacing: 24) { OnBoardingPageIndicator( diff --git a/Projects/Presentation/Home/Sources/Vote/View/PreVoteView.swift b/Projects/Presentation/Home/Sources/Vote/View/PreVoteView.swift index 0f5ed92..1df5b7f 100644 --- a/Projects/Presentation/Home/Sources/Vote/View/PreVoteView.swift +++ b/Projects/Presentation/Home/Sources/Vote/View/PreVoteView.swift @@ -45,6 +45,7 @@ public struct PreVoteView: View { // MARK: - Background extension PreVoteView { + @ViewBuilder private var backgroundImage: some View { ZStack { if let urlString = store.battle.backgroundImageURL, @@ -70,6 +71,7 @@ extension PreVoteView { // MARK: - Navigation bar extension PreVoteView { + @ViewBuilder private var navigationBar: some View { PickeNavigationBar( onBack: { send(.backButtonTapped) } @@ -88,6 +90,7 @@ extension PreVoteView { // MARK: - Content (gradient + 카피 + 선택지 + CTA) extension PreVoteView { + @ViewBuilder private var contentArea: some View { VStack(spacing: 40) { contentSection @@ -111,6 +114,7 @@ extension PreVoteView { .ignoresSafeArea(edges: .bottom) } + @ViewBuilder private var contentSection: some View { VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 20) { @@ -122,6 +126,7 @@ extension PreVoteView { .frame(maxWidth: .infinity, alignment: .leading) } + @ViewBuilder private var tagsRow: some View { HStack(spacing: 9) { ForEach(store.battle.tags, id: \.self) { tag in @@ -135,6 +140,7 @@ extension PreVoteView { } } + @ViewBuilder private var titleText: some View { Text("\(store.battle.titleLine1)\n\(store.battle.titleLine2)") .pretendardFont(family: .Bold, size: 24) @@ -145,6 +151,7 @@ extension PreVoteView { .frame(maxWidth: .infinity, alignment: .leading) } + @ViewBuilder private var summaryText: some View { Text(store.battle.summary) .pretendardFont(family: .Regular, size: 13) @@ -158,6 +165,7 @@ extension PreVoteView { // MARK: - 선택지 extension PreVoteView { + @ViewBuilder private var optionSection: some View { ZStack { HStack(spacing: 8) { @@ -168,6 +176,7 @@ extension PreVoteView { } } + @ViewBuilder private func optionCard(_ option: PreVoteOption) -> some View { let isSelected = store.selectedSide == option.philosopher @@ -202,6 +211,7 @@ extension PreVoteView { .buttonStyle(.plain) } + @ViewBuilder private func avatarView(_ philosopher: PhilosopherAvatar) -> some View { let asset: ImageAsset = switch philosopher { case .plato: .avatarPlato @@ -216,6 +226,7 @@ extension PreVoteView { .background(.beige600, in: Circle()) } + @ViewBuilder private var vsBadge: some View { Text("VS") .pretendardFont(family: .Bold, size: 11) @@ -229,6 +240,7 @@ extension PreVoteView { // MARK: - CTA extension PreVoteView { + @ViewBuilder private var primaryButton: some View { CustomButton( action: { send(.primaryButtonTapped) }, diff --git a/Projects/Presentation/MainTab/Sources/View/MainTabView.swift b/Projects/Presentation/MainTab/Sources/View/MainTabView.swift index 9f8d752..a846aab 100644 --- a/Projects/Presentation/MainTab/Sources/View/MainTabView.swift +++ b/Projects/Presentation/MainTab/Sources/View/MainTabView.swift @@ -83,6 +83,7 @@ extension MainTabView { ] } + @ViewBuilder private func tabLabel(for tab: TabItem) -> some View { Label { Text(tab.title) From 101edb6e05dd0b8190c9286aa75061719373690a Mon Sep 17 00:00:00 2001 From: Roy Date: Tue, 19 May 2026 01:22:11 +0900 Subject: [PATCH 12/21] =?UTF-8?q?revert:=20#31=20Splash=20=E2=86=92=20Auth?= =?UTF-8?q?=20=EA=B0=95=EC=A0=9C=20=EC=A7=84=EC=9E=85=20=EC=9E=84=EC=8B=9C?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=20=EB=90=98=EB=8F=8C=EB=A6=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Apple 로그인 동작 확인을 위해 임시로 비활성화했던 keychain 자격 분기를 복원 - SplashFeature 가 hasStoredCredential 에 따라 .presentMainTab / .presentAuth 로 분기 - AppReducer 의 splash.onAppear 핸들러도 .none 으로 원복하여 SplashFeature 의 delegate 흐름이 정상 작동 --- Projects/App/Sources/Reducer/AppReducer.swift | 2 +- .../Splash/Sources/Reducer/SplashFeature.swift | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Projects/App/Sources/Reducer/AppReducer.swift b/Projects/App/Sources/Reducer/AppReducer.swift index 476ce87..1e1cd6e 100644 --- a/Projects/App/Sources/Reducer/AppReducer.swift +++ b/Projects/App/Sources/Reducer/AppReducer.swift @@ -233,7 +233,7 @@ public struct AppReducer: Sendable { private func handleScopeNavigation(action: ScopeAction) -> Effect { switch action { case .splash(.view(.onAppear)): - return .send(.view(.presentAuth)) + return .none case .splash(.delegate(.presentAuth)): return .run { send in diff --git a/Projects/Presentation/Splash/Sources/Reducer/SplashFeature.swift b/Projects/Presentation/Splash/Sources/Reducer/SplashFeature.swift index 6ac0463..0560f3a 100644 --- a/Projects/Presentation/Splash/Sources/Reducer/SplashFeature.swift +++ b/Projects/Presentation/Splash/Sources/Reducer/SplashFeature.swift @@ -100,11 +100,11 @@ extension SplashFeature { let hasStoredCredential = hasStoredCredential return .run { send in try await clock.sleep(for: .seconds(1.2)) -// if hasStoredCredential { -// await send(.delegate(.presentMainTab)) -// } else { -// await send(.delegate(.presentAuth)) -// } + if hasStoredCredential { + await send(.delegate(.presentMainTab)) + } else { + await send(.delegate(.presentAuth)) + } } } From 75752cf1ef53e63b4938e2439ae4583851df496e Mon Sep 17 00:00:00 2001 From: Roy Date: Tue, 19 May 2026 01:28:11 +0900 Subject: [PATCH 13/21] =?UTF-8?q?refactor:=20=ED=99=88=20Hero/Battle=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC=20=EB=B0=8F=20=EB=AF=B8=EC=82=AC=EC=9A=A9=20?= =?UTF-8?q?=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Main/View/Components/HeroCarouselView.swift | 14 ++++++-------- .../Main/View/Components/NewBattleCardView.swift | 4 ++-- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/Projects/Presentation/Home/Sources/Main/View/Components/HeroCarouselView.swift b/Projects/Presentation/Home/Sources/Main/View/Components/HeroCarouselView.swift index 902170f..6e427f5 100644 --- a/Projects/Presentation/Home/Sources/Main/View/Components/HeroCarouselView.swift +++ b/Projects/Presentation/Home/Sources/Main/View/Components/HeroCarouselView.swift @@ -153,16 +153,14 @@ struct HeroCardView: View { .foregroundStyle(.neutral200) .lineLimit(2) - if !hero.tags.isEmpty { - HStack(spacing: 4) { - ForEach(hero.tags) { tag in - Text(tag.name) - .pretendardFont(family: .Medium, size: 11) - .foregroundStyle(.neutral200) - } + HStack(spacing: 4) { + ForEach(hero.tags) { tag in + Text(tag.name) + .pretendardFont(family: .Medium, size: 11) + .foregroundStyle(.neutral200) } - .padding(.top, 6) } + .padding(.top, 6) } Spacer() diff --git a/Projects/Presentation/Home/Sources/Main/View/Components/NewBattleCardView.swift b/Projects/Presentation/Home/Sources/Main/View/Components/NewBattleCardView.swift index a090bc4..e1cf34b 100644 --- a/Projects/Presentation/Home/Sources/Main/View/Components/NewBattleCardView.swift +++ b/Projects/Presentation/Home/Sources/Main/View/Components/NewBattleCardView.swift @@ -33,7 +33,7 @@ struct NewBattleCardView: View { extension NewBattleCardView { @ViewBuilder private var content: some View { - VStack(alignment: .leading, spacing: 12) { + VStack(alignment: .leading, spacing: 16) { container versusRow } @@ -132,7 +132,7 @@ extension NewBattleCardView { @ViewBuilder private func avatar( - for philosopherName: String, + for _: String, imageURL: URL? ) -> some View { // .pen `Avatar/Philosopher` 매핑: 베이지 40×40 원형 배경 + 가운데 16×28 일러스트 From a3cd05e31f817ebd079a84d05ab414c4c6cb1b43 Mon Sep 17 00:00:00 2001 From: Roy Date: Tue, 19 May 2026 22:35:09 +0900 Subject: [PATCH 14/21] =?UTF-8?q?chore:=20Tokens=20Studio=203-=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=ED=86=A0=ED=81=B0=20=ED=8F=AC=EB=A7=B7=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20(=EB=94=94=EC=9E=90=EC=9D=B8=20v0519)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Mode 1.tokens.json 폐기, primitive/semantic/component/$metadata JSON 도입 - Tools/TokenGenerator.swift 를 새 포맷에 맞게 재작성: 3-파일 deep-merge, 참조 체인 재귀 해석, rgba() 색 지원 - 자동 생성물 (ShapeStyle+.swift / CGFloat+Spacing+ / CGFloat+Radius+ / CGFloat+Component+ / ComponentToken.swift) 갱신 --- .../View/Components/PreVoteSkeletonView.swift | 73 + .../DesignSystem/Resources/$metadata.json | 7 + .../DesignSystem/Resources/Mode 1.tokens.json | 1675 ----------------- .../DesignSystem/Resources/component.json | 598 ++++++ .../DesignSystem/Resources/primitive.json | 510 +++++ .../DesignSystem/Resources/semantic.json | 725 +++++++ .../Sources/Color/ShapeStyle+.swift | 213 ++- .../CGFloat/CGFloat+Component+.swift | 39 +- .../Extension/CGFloat/CGFloat+Radius+.swift | 7 +- .../Extension/CGFloat/CGFloat+Spacing+.swift | 52 +- .../Sources/UI/Token/ComponentToken.swift | 297 ++- Tools/TokenGenerator.swift | 565 +++--- 12 files changed, 2729 insertions(+), 2032 deletions(-) create mode 100644 Projects/Presentation/Home/Sources/Vote/View/Components/PreVoteSkeletonView.swift create mode 100644 Projects/Shared/DesignSystem/Resources/$metadata.json delete mode 100644 Projects/Shared/DesignSystem/Resources/Mode 1.tokens.json create mode 100644 Projects/Shared/DesignSystem/Resources/component.json create mode 100644 Projects/Shared/DesignSystem/Resources/primitive.json create mode 100644 Projects/Shared/DesignSystem/Resources/semantic.json diff --git a/Projects/Presentation/Home/Sources/Vote/View/Components/PreVoteSkeletonView.swift b/Projects/Presentation/Home/Sources/Vote/View/Components/PreVoteSkeletonView.swift new file mode 100644 index 0000000..2a6c4ea --- /dev/null +++ b/Projects/Presentation/Home/Sources/Vote/View/Components/PreVoteSkeletonView.swift @@ -0,0 +1,73 @@ +// +// PreVoteSkeletonView.swift +// Home +// +// .pen `사전 투표창 - Skeleton Loader` (nTffe) 1:1 매핑. +// PreVoteView 의 로딩 상태 placeholder. +// + +import SwiftUI + +import DesignSystem + +struct PreVoteSkeletonView: View { + private static let designWidth: CGFloat = 375 + private static let designHeight: CGFloat = 812 + + var body: some View { + GeometryReader { proxy in + let scale = proxy.size.width / Self.designWidth + + ZStack(alignment: .topLeading) { + Color.beige50 + + // 상단 이미지 영역 + block(width: 375, height: 329.25, x: 0, y: 0) + + // 헤더 영역 + block(width: 375, height: 60, x: 0, y: 70) + + // 태그 2개 + block(width: 29, height: 17, x: 22, y: 360) + block(width: 49, height: 17, x: 72, y: 360) + + // 타이틀 + block(width: 167, height: 68, x: 16, y: 399) + + // 설명 + block(width: 235.5, height: 61.43, x: 16, y: 479) + + // 좌/우 옵션 카드 + block(width: 167, height: 105.72, x: 14.625, y: 574) + block(width: 167, height: 105.72, x: 193.375, y: 574) + + // VS 작은 점 + block(width: 15, height: 15, x: 373.5, y: 619, cornerRadius: 8) + + // CTA 영역 + block(width: 87, height: 24, x: 144, y: 734) + } + .frame(width: Self.designWidth, height: Self.designHeight, alignment: .topLeading) + .scaleEffect(scale, anchor: .topLeading) + .frame( + width: proxy.size.width, + height: Self.designHeight * scale, + alignment: .topLeading + ) + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + } + + @ViewBuilder + private func block( + width: CGFloat, + height: CGFloat, + x: CGFloat, + y: CGFloat, + cornerRadius: CGFloat = 6 + ) -> some View { + SkeletonView(cornerRadius: cornerRadius) + .frame(width: width, height: height) + .offset(x: x, y: y) + } +} diff --git a/Projects/Shared/DesignSystem/Resources/$metadata.json b/Projects/Shared/DesignSystem/Resources/$metadata.json new file mode 100644 index 0000000..ccc8a53 --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/$metadata.json @@ -0,0 +1,7 @@ +{ + "tokenSetOrder": [ + "primitive", + "semantic", + "component" + ] +} \ No newline at end of file diff --git a/Projects/Shared/DesignSystem/Resources/Mode 1.tokens.json b/Projects/Shared/DesignSystem/Resources/Mode 1.tokens.json deleted file mode 100644 index fa1e048..0000000 --- a/Projects/Shared/DesignSystem/Resources/Mode 1.tokens.json +++ /dev/null @@ -1,1675 +0,0 @@ -{ - "Colors": { - "brand": { - "primary": { - "50": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.9529411792755127, - 0.9215686321258545, - 0.9137254953384399 - ], - "alpha": 1, - "hex": "#F3EBE9" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7232", - "com.figma.hiddenFromPublishing": true, - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "100": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.9058823585510254, - 0.843137264251709, - 0.8274509906768799 - ], - "alpha": 1, - "hex": "#E7D7D3" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7228", - "com.figma.hiddenFromPublishing": true, - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "200": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.8156862854957581, - 0.686274528503418, - 0.658823549747467 - ], - "alpha": 1, - "hex": "#D0AFA8" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7229", - "com.figma.hiddenFromPublishing": true, - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "300": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.7215686440467834, - 0.5333333611488342, - 0.48627451062202454 - ], - "alpha": 1, - "hex": "#B8887C" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7236", - "com.figma.hiddenFromPublishing": true, - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "400": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.6313725709915161, - 0.3764705955982208, - 0.3176470696926117 - ], - "alpha": 1, - "hex": "#A16051" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7230", - "com.figma.hiddenFromPublishing": true, - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "500": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.5372549295425415, - 0.21960784494876862, - 0.14509804546833038 - ], - "alpha": 1, - "hex": "#893825" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7234", - "com.figma.hiddenFromPublishing": true, - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "600": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.47843137383461, - 0.21176470816135406, - 0.14901961386203766 - ], - "alpha": 1, - "hex": "#7A3626" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7231", - "com.figma.hiddenFromPublishing": true, - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "700": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.4431372582912445, - 0.21568627655506134, - 0.16470588743686676 - ], - "alpha": 1, - "hex": "#71372A" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7233", - "com.figma.hiddenFromPublishing": true, - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "800": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.3960784375667572, - 0.19607843458652496, - 0.14901961386203766 - ], - "alpha": 1, - "hex": "#653226" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7235", - "com.figma.hiddenFromPublishing": true, - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "900": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.30588236451148987, - 0.16470588743686676, - 0.12941177189350128 - ], - "alpha": 1, - "hex": "#4E2A21" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7237", - "com.figma.hiddenFromPublishing": true, - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "Alpha": { - "8": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.5372549295425415, - 0.21960784494876862, - 0.14509804546833038 - ], - "alpha": 0.07999999821186066, - "hex": "#893825" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6766:7483", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - } - } - }, - "secondary": { - "50": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.9882352948188782, - 0.9725490212440491, - 0.9450980424880981 - ], - "alpha": 1, - "hex": "#FCF8F1" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7292", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "100": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.9764705896377563, - 0.9450980424880981, - 0.8901960849761963 - ], - "alpha": 1, - "hex": "#F9F1E3" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7293", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "200": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.9529411792755127, - 0.8901960849761963, - 0.7803921699523926 - ], - "alpha": 1, - "hex": "#F3E3C7" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7294", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "300": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.929411768913269, - 0.8352941274642944, - 0.6745098233222961 - ], - "alpha": 1, - "hex": "#EDD5AC" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7295", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "400": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.9058823585510254, - 0.7803921699523926, - 0.5647059082984924 - ], - "alpha": 1, - "hex": "#E7C790" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7300", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "500": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.8823529481887817, - 0.7254902124404907, - 0.45490196347236633 - ], - "alpha": 1, - "hex": "#E1B974" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7296", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "600": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.8076171875, - 0.66231369972229, - 0.411665141582489 - ], - "alpha": 1, - "hex": "#CEA969" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7297", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "700": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.7186185121536255, - 0.5878713130950928, - 0.3623323142528534 - ], - "alpha": 1, - "hex": "#B7965C" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7298", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "800": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.6392157077789307, - 0.5254902243614197, - 0.32549020648002625 - ], - "alpha": 1, - "hex": "#A38653" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7299", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "900": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.572549045085907, - 0.47058823704719543, - 0.29019609093666077 - ], - "alpha": 1, - "hex": "#92784A" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7301", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - } - }, - "beige": { - "50": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.9960784316062927, - 0.9960784316062927, - 0.9921568632125854 - ], - "alpha": 1, - "hex": "#FEFEFD" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7325", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "100": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.9921568632125854, - 0.9882352948188782, - 0.9843137264251709 - ], - "alpha": 1, - "hex": "#FDFCFB" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7324", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "200": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.9843137264251709, - 0.9764705896377563, - 0.9686274528503418 - ], - "alpha": 1, - "hex": "#FBF9F7" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7326", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "300": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.9764705896377563, - 0.9686274528503418, - 0.9490196108818054 - ], - "alpha": 1, - "hex": "#F9F7F2" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7327", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "400": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.9686274528503418, - 0.95686274766922, - 0.9333333373069763 - ], - "alpha": 1, - "hex": "#F7F4EE" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7328", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "500": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.9607843160629272, - 0.9450980424880981, - 0.9176470637321472 - ], - "alpha": 1, - "hex": "#F5F1EA" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7333", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "600": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.9372549057006836, - 0.9176470637321472, - 0.8784313797950745 - ], - "alpha": 1, - "hex": "#EFEAE0" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7329", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "700": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.8549019694328308, - 0.8196078538894653, - 0.7490196228027344 - ], - "alpha": 1, - "hex": "#DAD1BF" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7332", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "800": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.8078431487083435, - 0.7568627595901489, - 0.658823549747467 - ], - "alpha": 1, - "hex": "#CEC1A8" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7331", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "900": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.7176470756530762, - 0.658823549747467, - 0.545098066329956 - ], - "alpha": 1, - "hex": "#B7A88B" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7330", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - } - }, - "neutral": { - "50": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.9215686321258545, - 0.9215686321258545, - 0.9215686321258545 - ], - "alpha": 1, - "hex": "#EBEBEB" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7356", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "100": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.843137264251709, - 0.843137264251709, - 0.843137264251709 - ], - "alpha": 1, - "hex": "#D7D7D7" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7363", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "200": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.6901960968971252, - 0.686274528503418, - 0.6823529601097107 - ], - "alpha": 1, - "hex": "#B0AFAE" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7357", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "300": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.5333333611488342, - 0.529411792755127, - 0.5254902243614197 - ], - "alpha": 1, - "hex": "#888786" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7358", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "400": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.3803921639919281, - 0.37254902720451355, - 0.364705890417099 - ], - "alpha": 1, - "hex": "#615F5D" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7359", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "500": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.2235294133424759, - 0.21568627655506134, - 0.2078431397676468 - ], - "alpha": 1, - "hex": "#393735" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7360", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "600": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.16862745583057404, - 0.16470588743686676, - 0.1568627506494522 - ], - "alpha": 1, - "hex": "#2B2A28" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7362", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "700": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.13333334028720856, - 0.12941177189350128, - 0.125490203499794 - ], - "alpha": 1, - "hex": "#222120" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7364", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "800": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.10196078568696976, - 0.09803921729326248, - 0.0941176488995552 - ], - "alpha": 1, - "hex": "#1A1918" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7365", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "900": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.07450980693101883, - 0.07058823853731155, - 0.07058823853731155 - ], - "alpha": 1, - "hex": "#131212" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7361", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - } - } - }, - "semantic": { - "text": { - "primary": { - "$type": "color", - "$value": "{Colors.brand.neutral.900}", - "$extensions": { - "com.figma.variableId": "VariableID:6708:7367", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "secondary": { - "$type": "color", - "$value": "{Colors.brand.neutral.700}", - "$extensions": { - "com.figma.variableId": "VariableID:6708:7369", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "tertiary": { - "$type": "color", - "$value": "{Colors.brand.neutral.500}", - "$extensions": { - "com.figma.variableId": "VariableID:6708:7370", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "muted": { - "$type": "color", - "$value": "{Colors.brand.neutral.300}", - "$extensions": { - "com.figma.variableId": "VariableID:6708:7371", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "inverse": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.9960784316062927, - 0.9960784316062927, - 0.9921568632125854 - ], - "alpha": 1, - "hex": "#FEFEFD" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7372", - "com.figma.scopes": [ - "ALL_SCOPES" - ], - "com.figma.aliasData": { - "targetVariableId": "VariableID:5ca37bec2680585542952367424f7ecf45490cc3/-1:-1", - "targetVariableName": "beige color50", - "targetVariableSetId": "VariableCollectionId:bbcfac25774af1795702717ddc0dd55de8fd3823/-1:-1", - "targetVariableSetName": "beige color" - } - } - }, - "brand": { - "$type": "color", - "$value": "{Colors.brand.primary.500}", - "$extensions": { - "com.figma.variableId": "VariableID:6708:7373", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - } - }, - "border": { - "default": { - "$type": "color", - "$value": "{Colors.brand.beige.600}", - "$extensions": { - "com.figma.variableId": "VariableID:6715:7399", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "subtle": { - "$type": "color", - "$value": "{Colors.brand.beige.700}", - "$extensions": { - "com.figma.variableId": "VariableID:6715:7400", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "disabled": { - "$type": "color", - "$value": "{Colors.brand.beige.500}", - "$extensions": { - "com.figma.variableId": "VariableID:6715:7401", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "selected": { - "$type": "color", - "$value": "{Colors.brand.secondary.500}", - "$extensions": { - "com.figma.variableId": "VariableID:6715:7434", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "strong": { - "$type": "color", - "$value": "{Colors.brand.primary.500}", - "$extensions": { - "com.figma.variableId": "VariableID:6715:7467", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "focus": { - "$type": "color", - "$value": "{Colors.brand.beige.700}", - "$extensions": { - "com.figma.variableId": "VariableID:6715:7468", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "error": { - "$type": "color", - "$value": "{Colors.semantic.status.error.Alpha}", - "$extensions": { - "com.figma.variableId": "VariableID:6715:7469", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - } - }, - "surface": { - "default": { - "$type": "color", - "$value": "{Colors.brand.beige.50}", - "$extensions": { - "com.figma.variableId": "VariableID:6715:7387", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "subtle": { - "$type": "color", - "$value": "{Colors.brand.beige.300}", - "$extensions": { - "com.figma.variableId": "VariableID:6715:7388", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "tertiary": { - "$type": "color", - "$value": "{Colors.brand.beige.400}", - "$extensions": { - "com.figma.variableId": "VariableID:6715:7389", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "selected": { - "$type": "color", - "$value": "{Colors.brand.primary.500}", - "$extensions": { - "com.figma.variableId": "VariableID:6715:7390", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "disabled": { - "$type": "color", - "$value": "{Colors.brand.primary.200}", - "$extensions": { - "com.figma.variableId": "VariableID:6715:7391", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - } - }, - "background": { - "default": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.9803921580314636, - 0.9803921580314636, - 0.9764705896377563 - ], - "alpha": 1, - "hex": "#FAFAF9" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7382", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "subtle": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.9607843160629272, - 0.9607843160629272, - 0.95686274766922 - ], - "alpha": 1, - "hex": "#F5F5F4" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7383", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "tertiary": { - "$type": "color", - "$value": "{Colors.brand.neutral.50}", - "$extensions": { - "com.figma.variableId": "VariableID:6715:7398", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "brand": { - "$type": "color", - "$value": "{Colors.brand.beige.200}", - "$extensions": { - "com.figma.variableId": "VariableID:6708:7384", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "inverse": { - "$type": "color", - "$value": "{Colors.brand.neutral.800}", - "$extensions": { - "com.figma.variableId": "VariableID:6708:7386", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "overlay": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0, - 0, - 0 - ], - "alpha": 0.4000000059604645, - "hex": "#000000" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6715:7392", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - } - }, - "status": { - "error": { - "error": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.7882353067398071, - 0.1764705926179886, - 0.20000000298023224 - ], - "alpha": 1, - "hex": "#C92D33" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7376", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "Alpha": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.7882353067398071, - 0.1764705926179886, - 0.20000000298023224 - ], - "alpha": 0.4000000059604645, - "hex": "#C92D33" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6761:657", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - } - }, - "warning": { - "warning": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 1, - 0.7058823704719543, - 0 - ], - "alpha": 1, - "hex": "#FFB400" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6708:7380", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "Alpha": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 1, - 0.7058823704719543, - 0 - ], - "alpha": 0.4000000059604645, - "hex": "#FFB400" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6761:658", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - } - } - } - } - }, - "Radius": { - "none": { - "$type": "number", - "$value": 0, - "$extensions": { - "com.figma.variableId": "VariableID:6761:7000", - "com.figma.hiddenFromPublishing": true, - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "default": { - "$type": "number", - "$value": 2, - "$extensions": { - "com.figma.variableId": "VariableID:6761:6998", - "com.figma.hiddenFromPublishing": true, - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "full": { - "$type": "number", - "$value": 999, - "$extensions": { - "com.figma.variableId": "VariableID:6761:6999", - "com.figma.hiddenFromPublishing": true, - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - } - }, - "Spacing": { - "0": { - "$type": "number", - "$value": 0, - "$extensions": { - "com.figma.variableId": "VariableID:6762:6955", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "2": { - "$type": "number", - "$value": 2, - "$extensions": { - "com.figma.variableId": "VariableID:6762:6956", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "4": { - "$type": "number", - "$value": 4, - "$extensions": { - "com.figma.variableId": "VariableID:6761:7107", - "com.figma.hiddenFromPublishing": true, - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "8": { - "$type": "number", - "$value": 8, - "$extensions": { - "com.figma.variableId": "VariableID:6761:7111", - "com.figma.hiddenFromPublishing": true, - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "16": { - "$type": "number", - "$value": 16, - "$extensions": { - "com.figma.variableId": "VariableID:6761:7110", - "com.figma.hiddenFromPublishing": true, - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "24": { - "$type": "number", - "$value": 24, - "$extensions": { - "com.figma.variableId": "VariableID:6761:7108", - "com.figma.hiddenFromPublishing": true, - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "32": { - "$type": "number", - "$value": 32, - "$extensions": { - "com.figma.variableId": "VariableID:6761:7112", - "com.figma.hiddenFromPublishing": true, - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "40": { - "$type": "number", - "$value": 40, - "$extensions": { - "com.figma.variableId": "VariableID:6761:7104", - "com.figma.hiddenFromPublishing": true, - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "48": { - "$type": "number", - "$value": 48, - "$extensions": { - "com.figma.variableId": "VariableID:6761:7103", - "com.figma.hiddenFromPublishing": true, - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "64": { - "$type": "number", - "$value": 64, - "$extensions": { - "com.figma.variableId": "VariableID:6761:7109", - "com.figma.hiddenFromPublishing": true, - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "80": { - "$type": "number", - "$value": 80, - "$extensions": { - "com.figma.variableId": "VariableID:6761:7106", - "com.figma.hiddenFromPublishing": true, - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "96": { - "$type": "number", - "$value": 96, - "$extensions": { - "com.figma.variableId": "VariableID:6761:7105", - "com.figma.hiddenFromPublishing": true, - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - } - }, - "Component": { - "bedge": { - "filled": { - "background": { - "default": { - "$type": "color", - "$value": "{Colors.brand.beige.600}", - "$extensions": { - "com.figma.variableId": "VariableID:6792:1209", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "inverse": { - "$type": "color", - "$value": "{Colors.brand.primary.500}", - "$extensions": { - "com.figma.variableId": "VariableID:6904:2076", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - } - }, - "text": { - "default": { - "$type": "color", - "$value": "{Colors.brand.primary.500}", - "$extensions": { - "com.figma.variableId": "VariableID:6904:2078", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "inverse": { - "$type": "color", - "$value": "{Colors.brand.beige.50}", - "$extensions": { - "com.figma.variableId": "VariableID:6904:2082", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - } - } - }, - "outline": { - "text": { - "$type": "color", - "$value": "{Colors.brand.primary.500}", - "$extensions": { - "com.figma.variableId": "VariableID:6872:7386", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "backround": { - "$type": "color", - "$value": "{Colors.brand.beige.50}", - "$extensions": { - "com.figma.variableId": "VariableID:6904:2067", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "border": { - "$type": "color", - "$value": "{Colors.brand.primary.100}", - "$extensions": { - "com.figma.variableId": "VariableID:6904:2071", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - } - } - }, - "input": { - "border": { - "default": { - "$type": "color", - "$value": "{Colors.brand.beige.600}", - "$extensions": { - "com.figma.variableId": "VariableID:6792:1210", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "active": { - "$type": "color", - "$value": "{Colors.brand.beige.700}", - "$extensions": { - "com.figma.variableId": "VariableID:6857:7360", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "error": { - "$type": "color", - "$value": "{Colors.semantic.border.error}", - "$extensions": { - "com.figma.variableId": "VariableID:6857:7361", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - } - }, - "surface": { - "default": { - "$type": "color", - "$value": "{Colors.brand.beige.50}", - "$extensions": { - "com.figma.variableId": "VariableID:6857:7362", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "disabled": { - "$type": "color", - "$value": "{Colors.brand.beige.300}", - "$extensions": { - "com.figma.variableId": "VariableID:6857:7363", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - } - }, - "text": { - "default": { - "$type": "color", - "$value": "{Colors.brand.neutral.300}", - "$extensions": { - "com.figma.variableId": "VariableID:6857:7367", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "active": { - "$type": "color", - "$value": "{Colors.brand.neutral.500}", - "$extensions": { - "com.figma.variableId": "VariableID:6857:7380", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "error": { - "$type": "color", - "$value": "{Colors.semantic.status.error.error}", - "$extensions": { - "com.figma.variableId": "VariableID:6857:7371", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - } - } - }, - "button": { - "radius": { - "$type": "number", - "$value": "{Radius.default}", - "$extensions": { - "com.figma.variableId": "VariableID:6854:7375", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "primary": { - "background": { - "default": { - "$type": "color", - "$value": "{Colors.brand.primary.500}", - "$extensions": { - "com.figma.variableId": "VariableID:6792:1208", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "pressed": { - "$type": "color", - "$value": "{Colors.brand.primary.800}", - "$extensions": { - "com.figma.variableId": "VariableID:6829:7354", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "disabled": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.8156862854957581, - 0.686274528503418, - 0.658823549747467 - ], - "alpha": 1, - "hex": "#D0AFA8" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6829:7355", - "com.figma.scopes": [ - "ALL_SCOPES" - ], - "com.figma.aliasData": { - "targetVariableId": "VariableID:cfdbc8ce010b4bf46c1aa1aebabc7686775c41ce/-1:-1", - "targetVariableName": "Primary200", - "targetVariableSetId": "VariableCollectionId:054680f45b5bcc4599f8f5dbddcbb91b1f17bca3/-1:-1", - "targetVariableSetName": "primary color" - } - } - } - }, - "text": { - "default": { - "$type": "color", - "$value": "{Colors.brand.beige.50}", - "$extensions": { - "com.figma.variableId": "VariableID:6829:7367", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - } - } - }, - "secondary": { - "background": { - "default": { - "$type": "color", - "$value": "{Colors.brand.beige.300}", - "$extensions": { - "com.figma.variableId": "VariableID:6841:7370", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "pressed": { - "$type": "color", - "$value": "{Colors.brand.beige.400}", - "$extensions": { - "com.figma.variableId": "VariableID:6904:2583", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - } - }, - "border": { - "default": { - "$type": "color", - "$value": "{Colors.brand.beige.600}", - "$extensions": { - "com.figma.variableId": "VariableID:6841:7373", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "pressed": { - "$type": "color", - "$value": "{Colors.brand.secondary.500}", - "$extensions": { - "com.figma.variableId": "VariableID:6841:7374", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - } - }, - "text": { - "default": { - "$type": "color", - "$value": "{Colors.brand.neutral.600}", - "$extensions": { - "com.figma.variableId": "VariableID:6854:7377", - "com.figma.scopes": [ - "ALL_SCOPES" - ] - } - }, - "disabled": { - "$type": "color", - "$value": { - "colorSpace": "srgb", - "components": [ - 0.5333333611488342, - 0.529411792755127, - 0.5254902243614197 - ], - "alpha": 1, - "hex": "#888786" - }, - "$extensions": { - "com.figma.variableId": "VariableID:6904:2584", - "com.figma.scopes": [ - "ALL_SCOPES" - ], - "com.figma.aliasData": { - "targetVariableId": "VariableID:e9a3ae05dbe56ce69dee81ab24ef760e560eede8/-1:-1", - "targetVariableName": "gray300", - "targetVariableSetId": "VariableCollectionId:58e4e447eaf6a6da4ebd7c821fb97c415c00f391/-1:-1", - "targetVariableSetName": "gray color" - } - } - } - } - } - } - }, - "$extensions": { - "com.figma.modeName": "Mode 1" - } -} \ No newline at end of file diff --git a/Projects/Shared/DesignSystem/Resources/component.json b/Projects/Shared/DesignSystem/Resources/component.json new file mode 100644 index 0000000..f739d1e --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/component.json @@ -0,0 +1,598 @@ +{ + "button": { + "primary": { + "background": { + "default": { + "$type": "color", + "$value": "{action.primary.default}" + }, + "pressed": { + "$type": "color", + "$value": "{action.primary.pressed}" + }, + "disabled": { + "$type": "color", + "$value": "{action.primary.disabled}" + } + }, + "text": { + "default": { + "$type": "color", + "$value": "{text.inverse}" + } + }, + "large": { + "width": { + "$type": "sizing", + "$value": "343" + }, + "height": { + "$type": "sizing", + "$value": "52" + } + }, + "medium": { + "width": { + "$type": "sizing", + "$value": "343" + }, + "height": { + "$type": "sizing", + "$value": "42" + } + }, + "small": { + "width": { + "$type": "sizing", + "$value": "100" + }, + "height": { + "$type": "sizing", + "$value": "42" + } + } + }, + "icon": { + "background": { + "default": { + "$type": "color", + "$value": "{action.primary.default}" + }, + "disabled": { + "$type": "color", + "$value": "{action.primary.disabled}" + } + }, + "size": { + "$type": "sizing", + "$value": "{number-36}" + } + }, + "secondary": { + "background": { + "default": { + "$type": "color", + "$value": "{action.secondary.default}" + }, + "pressed": { + "$type": "color", + "$value": "{action.beige.strong}" + } + }, + "text": { + "default": { + "$type": "color", + "$value": "{text.primary}" + } + } + } + }, + "badge": { + "filled": { + "background": { + "$type": "color", + "$value": "{action.beige.strong}" + }, + "text": { + "$type": "color", + "$value": "{text.primary}" + } + }, + "outline": { + "background": { + "$type": "color", + "$value": "{action.beige.subtle}" + }, + "border": { + "$type": "color", + "$value": "{action.primary.subtle}" + }, + "text": { + "$type": "color", + "$value": "{primary.500}" + } + }, + "primary": { + "background": { + "$type": "color", + "$value": "{action.primary.default}" + }, + "text": { + "$type": "color", + "$value": "{text.inverse}" + } + }, + "counter": { + "background": { + "$type": "color", + "$value": "{gray.500}" + }, + "text": { + "active": { + "$type": "color", + "$value": "{text.inverse}" + }, + "default": { + "$type": "color", + "$value": "{text.muted}" + } + } + } + }, + "chip": { + "background": { + "default": { + "$type": "color", + "$value": "{action.primary.default}" + }, + "selected": { + "$type": "color", + "$value": "{primary.50}" + } + }, + "border": { + "default": { + "$type": "color", + "$value": "{action.primary.default}" + } + }, + "text": { + "default": { + "$type": "color", + "$value": "{text.inverse}" + }, + "selected": { + "$type": "color", + "$value": "{text.primary}" + } + } + }, + "toggle": { + "track": { + "on": { + "$type": "color", + "$value": "{action.primary.default}" + }, + "off": { + "$type": "color", + "$value": "{gray.100}" + } + }, + "thumb": { + "default": { + "$type": "color", + "$value": "{action.beige.subtle}" + }, + "size": { + "$type": "sizing", + "$value": "14" + } + }, + "width": { + "$type": "sizing", + "$value": "{number-32}" + }, + "height": { + "$type": "sizing", + "$value": "18" + } + }, + "card": { + "base": { + "background": { + "default": { + "$type": "color", + "$value": "{action.beige.subtle}" + }, + "beige": { + "$type": "color", + "$value": "{surface.beige.strong}" + } + }, + "border": { + "default": { + "$type": "color", + "$value": "{border.beige.default}" + }, + "selected": { + "$type": "color", + "$value": "{border.secondary.selected}" + }, + "primary": { + "$type": "color", + "$value": "{border.primary.default}" + } + }, + "text": { + "title": { + "$type": "color", + "$value": "{text.subtler}" + }, + "decription": { + "$type": "color", + "$value": "{text.muted}" + }, + "body": { + "$type": "color", + "$value": "{text.body}" + }, + "secondary": { + "$type": "color", + "$value": "{text.secondary}" + }, + "primary": { + "$type": "color", + "$value": "{text.primary}" + }, + "inverse": { + "$type": "color", + "$value": "{text.inverse}" + } + } + }, + "gray": { + "background": { + "default": { + "$type": "color", + "$value": "{action.gray.default}" + }, + "pressed": { + "$type": "color", + "$value": "{action.gray.pressed}" + } + }, + "border": { + "selected": { + "$type": "color", + "$value": "{border.secondary.selected}" + } + } + }, + "opinion": { + "background": { + "default": { + "$type": "color", + "$value": "{action.beige.default}" + }, + "pressed": { + "$type": "color", + "$value": "{action.beige.pressed}" + } + }, + "border": { + "default": { + "$type": "color", + "$value": "{border.beige.default}" + }, + "pressed": { + "$type": "color", + "$value": "{border.secondary.selected}" + } + } + }, + "avatar": { + "md": { + "$type": "sizing", + "$value": "{avatar.md}" + }, + "lg": { + "$type": "sizing", + "$value": "{avatar.lg}" + }, + "sm": { + "$type": "sizing", + "$value": "{avatar.sm}" + } + } + }, + "input": { + "textfield": { + "background": { + "default": { + "$type": "color", + "$value": "{action.beige.subtle}" + }, + "disabled": { + "$type": "color", + "$value": "{action.beige.pressed}" + } + }, + "border": { + "default": { + "$type": "color", + "$value": "{border.beige.default}" + }, + "focus": { + "$type": "color", + "$value": "{border.beige.focus}" + }, + "error": { + "$type": "color", + "$value": "{badge.outline.border}" + } + }, + "text": { + "default": { + "$type": "color", + "$value": "{text.muted}" + }, + "focus": { + "$type": "color", + "$value": "{text.subtler}" + }, + "error": { + "$type": "color", + "$value": "{text.error}" + } + }, + "width": { + "$type": "sizing", + "$value": "343" + }, + "height": { + "$type": "sizing", + "$value": "{number-44}" + } + }, + "textarea": { + "background": { + "default": { + "$type": "color", + "$value": "{action.beige.subtle}" + }, + "disabled": { + "$type": "color", + "$value": "{action.beige.strong}" + } + }, + "text": { + "default": { + "$type": "color", + "$value": "{text.muted}" + }, + "focus": { + "$type": "color", + "$value": "{text.subtler}" + }, + "error": { + "$type": "color", + "$value": "{text.error}" + } + }, + "border": { + "error": { + "$type": "color", + "$value": "{border.error.default}" + } + } + } + }, + "navigation": { + "tab": { + "text": { + "default": { + "$type": "color", + "$value": "{text.muted}" + }, + "active": { + "$type": "color", + "$value": "{text.primary}" + } + }, + "border": { + "default": { + "$type": "color", + "$value": "{border.gray.default}" + }, + "active": { + "$type": "color", + "$value": "{border.primary.default}" + } + } + } + }, + "popup": { + "background": { + "$type": "color", + "$value": "{surface.beige.strong}" + }, + "border": { + "$type": "color", + "$value": "{border.primary.default}" + }, + "text": { + "$type": "color", + "$value": "{text.default}" + }, + "width": { + "$type": "sizing", + "$value": "393" + } + }, + "thumbnail": { + "width": { + "$type": "sizing", + "$value": "196" + }, + "height": { + "$type": "sizing", + "$value": "140" + } + }, + "padding": { + "button": { + "primary": { + "large": { + "horizontal": { + "$type": "spacing", + "$value": "{padding.component.12}" + }, + "vertical": { + "$type": "spacing", + "$value": "{padding.component.12}" + } + }, + "medium": { + "horizontal": { + "$type": "spacing", + "$value": "{padding.component.12}" + }, + "vertical": { + "$type": "spacing", + "$value": "{padding.component.12}" + } + }, + "small": { + "horizontal": { + "$type": "spacing", + "$value": "{padding.component.12}" + }, + "vertical": { + "$type": "spacing", + "$value": "{padding.component.12}" + } + } + } + }, + "chip": { + "horizontal": { + "$type": "spacing", + "$value": "{padding.component.12}" + }, + "vertical": { + "$type": "spacing", + "$value": "{padding.component.6}" + } + }, + "badge": { + "horizontal": { + "$type": "spacing", + "$value": "{padding.component.6}" + }, + "vertical": { + "$type": "spacing", + "$value": "{padding.component.2}" + } + }, + "card": { + "base": { + "$type": "spacing", + "$value": "{padding.component.16}" + } + }, + "input": { + "horizontal": { + "$type": "spacing", + "$value": "{padding.component.8}" + } + }, + "navigation": { + "tab": { + "horizontal": { + "$type": "spacing", + "$value": "{padding.component.12}" + }, + "vertical": { + "$type": "spacing", + "$value": "{padding.component.12}" + } + } + }, + "appbar": { + "horizontal": { + "$type": "spacing", + "$value": "{padding.component.16}" + }, + "vertical": { + "$type": "spacing", + "$value": "{padding.component.12}" + } + } + }, + "listItem": { + "agreement": { + "background": { + "default": { + "$type": "color", + "$value": "{background.default}" + } + }, + "border": { + "default": { + "$type": "color", + "$value": "{border.gray.subtle}" + }, + "error": { + "$type": "color", + "$value": "{border.error.default}" + } + }, + "text": { + "defualt": { + "$type": "color", + "$value": "{text.default}" + }, + "error": { + "$type": "color", + "$value": "{text.error}" + } + } + } + }, + "checkbox": { + "background": { + "default": { + "$type": "color", + "$value": "{primary.50}" + }, + "selected": { + "$type": "color", + "$value": "{primary.500}" + } + }, + "border": { + "default": { + "$type": "color", + "$value": "{primary.100}" + }, + "selected": { + "$type": "color", + "$value": "{primary.700}" + } + }, + "width": { + "$type": "sizing", + "$value": "{number-24}" + }, + "height": { + "$type": "sizing", + "$value": "{number-24}" + } + }, + "avatar": { + "backround": { + "$type": "color", + "$value": "{action.beige.strong}" + } + } +} \ No newline at end of file diff --git a/Projects/Shared/DesignSystem/Resources/primitive.json b/Projects/Shared/DesignSystem/Resources/primitive.json new file mode 100644 index 0000000..4435d92 --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/primitive.json @@ -0,0 +1,510 @@ +{ + "130": { + "$type": "lineHeights", + "$value": "130%" + }, + "140": { + "$type": "lineHeights", + "$value": "140%" + }, + "150": { + "$type": "lineHeights", + "$value": "150%" + }, + "primary": { + "50": { + "$type": "color", + "$value": "#F3EBE9" + }, + "100": { + "$type": "color", + "$value": "#E7D7D3" + }, + "200": { + "$type": "color", + "$value": "#D0AFA8" + }, + "300": { + "$type": "color", + "$value": "#B8887C" + }, + "400": { + "$type": "color", + "$value": "#A16051" + }, + "500": { + "$type": "color", + "$value": "#893825" + }, + "600": { + "$type": "color", + "$value": "#7A3626" + }, + "700": { + "$type": "color", + "$value": "#71372A" + }, + "800": { + "$type": "color", + "$value": "#653226" + }, + "900": { + "$type": "color", + "$value": "#4E2A21" + } + }, + "secondary": { + "50": { + "$type": "color", + "$value": "#FCF8F1" + }, + "100": { + "$type": "color", + "$value": "#F9F1E3" + }, + "200": { + "$type": "color", + "$value": "#F3E3C7" + }, + "300": { + "$type": "color", + "$value": "#EDD5AC" + }, + "400": { + "$type": "color", + "$value": "#E7C790" + }, + "500": { + "$type": "color", + "$value": "#E1B974" + }, + "600": { + "$type": "color", + "$value": "#CEA969" + }, + "700": { + "$type": "color", + "$value": "#B7965C" + }, + "800": { + "$type": "color", + "$value": "#A38653" + }, + "900": { + "$type": "color", + "$value": "#92784A" + } + }, + "beige": { + "50": { + "$type": "color", + "$value": "#FEFEFD" + }, + "100": { + "$type": "color", + "$value": "#FDFCFB" + }, + "200": { + "$type": "color", + "$value": "#FBF9F7" + }, + "300": { + "$type": "color", + "$value": "#F9F7F2" + }, + "400": { + "$type": "color", + "$value": "#F7F4EE" + }, + "500": { + "$type": "color", + "$value": "#F5F1EA" + }, + "600": { + "$type": "color", + "$value": "#EFEAE0" + }, + "700": { + "$type": "color", + "$value": "#DAD1BF" + }, + "800": { + "$type": "color", + "$value": "#CEC1A8" + }, + "900": { + "$type": "color", + "$value": "#B7A88B" + } + }, + "gray": { + "50": { + "$type": "color", + "$value": "#EBEBEB" + }, + "100": { + "$type": "color", + "$value": "#D7D7D7" + }, + "200": { + "$type": "color", + "$value": "#B0AFAE" + }, + "300": { + "$type": "color", + "$value": "#888786" + }, + "400": { + "$type": "color", + "$value": "#615F5D" + }, + "500": { + "$type": "color", + "$value": "#393735" + }, + "600": { + "$type": "color", + "$value": "#2B2A28" + }, + "700": { + "$type": "color", + "$value": "#222120" + }, + "800": { + "$type": "color", + "$value": "#1A1918" + }, + "900": { + "$type": "color", + "$value": "#131212" + } + }, + "number-2": { + "$type": "number", + "$value": "2" + }, + "number-4": { + "$type": "number", + "$value": "4" + }, + "number-6": { + "$type": "number", + "$value": "6" + }, + "number-8": { + "$type": "number", + "$value": "8" + }, + "number-12": { + "$type": "number", + "$value": "12" + }, + "number-16": { + "$type": "number", + "$value": "16" + }, + "number-20": { + "$type": "number", + "$value": "20" + }, + "number-24": { + "$type": "number", + "$value": "24" + }, + "number-28": { + "$type": "number", + "$value": "28" + }, + "number-32": { + "$type": "number", + "$value": "32" + }, + "number-36": { + "$type": "number", + "$value": "36" + }, + "number-40": { + "$type": "number", + "$value": "40" + }, + "number-44": { + "$type": "number", + "$value": "44" + }, + "number-48": { + "$type": "number", + "$value": "48" + }, + "number-52": { + "$type": "number", + "$value": "52" + }, + "number-56": { + "$type": "number", + "$value": "56" + }, + "number-60": { + "$type": "number", + "$value": "60" + }, + "number-68": { + "$type": "number", + "$value": "68" + }, + "number-64": { + "$type": "number", + "$value": "64" + }, + "number-72": { + "$type": "number", + "$value": "72" + }, + "number-76": { + "$type": "number", + "$value": "76" + }, + "number-80": { + "$type": "number", + "$value": "80" + }, + "number-84": { + "$type": "number", + "$value": "84" + }, + "number-88": { + "$type": "number", + "$value": "88" + }, + "number-92": { + "$type": "number", + "$value": "92" + }, + "number-96": { + "$type": "number", + "$value": "96" + }, + "number-100": { + "$type": "number", + "$value": "100" + }, + "icon": { + "xs": { + "$type": "sizing", + "$value": "{number-12}" + }, + "sm": { + "$type": "sizing", + "$value": "{number-16}" + }, + "md": { + "$type": "sizing", + "$value": "{number-20}" + }, + "lg": { + "$type": "sizing", + "$value": "{number-24}" + } + }, + "avatar": { + "sm": { + "$type": "sizing", + "$value": "{number-36}" + }, + "md": { + "$type": "sizing", + "$value": "{number-40}" + }, + "lg": { + "$type": "sizing", + "$value": "{number-68}" + } + }, + "control": { + "sm": { + "$type": "sizing", + "$value": "{number-28}", + "$description": "카테고리 칩, 정렬 칩 높이" + }, + "md": { + "$type": "sizing", + "$value": "{number-52}", + "$description": "버튼" + } + }, + "spacing-0": { + "$type": "spacing", + "$value": "{number-0}" + }, + "number-0": { + "$type": "number", + "$value": "0" + }, + "spacing-4": { + "$type": "spacing", + "$value": "{number-4}" + }, + "spacing-2": { + "$type": "spacing", + "$value": "{number-2}" + }, + "spacing-8": { + "$type": "spacing", + "$value": "{number-8}" + }, + "spacing-16": { + "$type": "spacing", + "$value": "{number-16}" + }, + "spacing-24": { + "$type": "spacing", + "$value": "{number-24}" + }, + "spacing-32": { + "$type": "spacing", + "$value": "{number-32}" + }, + "spacing-40": { + "$type": "spacing", + "$value": "{number-40}" + }, + "spacing-48": { + "$type": "spacing", + "$value": "{number-48}" + }, + "spacing-64": { + "$type": "spacing", + "$value": "{number-64}" + }, + "spacing-80": { + "$type": "spacing", + "$value": "{number-80}" + }, + "spacing-96": { + "$type": "spacing", + "$value": "{number-96}" + }, + "type": { + "$type": "fontFamilies", + "$value": "Pretendard" + }, + "font-weight-regular": { + "$type": "fontWeights", + "$value": "regular" + }, + "font-weight-medium": { + "$type": "fontWeights", + "$value": "medium" + }, + "font-weight-bold": { + "$type": "fontWeights", + "$value": "bold" + }, + "font-weight-semibold": { + "$type": "fontWeights", + "$value": "semibold" + }, + "error": { + "default": { + "$type": "color", + "$value": "#C92D33" + }, + "alpha": { + "$type": "color", + "$value": "rgba(201,45,51,0.4)" + } + }, + "warning": { + "default": { + "$type": "color", + "$value": "#FFB400" + }, + "slpha": { + "$type": "color", + "$value": "rgba(255,180,0,0.4)" + } + }, + "spacing-6": { + "$type": "spacing", + "$value": "{number-6}" + }, + "spscing-12": { + "$type": "spacing", + "$value": "{number-12}" + }, + "spacing-20": { + "$type": "spacing", + "$value": "{number-20}" + }, + "headings": { + "xs": { + "$type": "fontSizes", + "$value": "14" + }, + "sm": { + "$type": "fontSizes", + "$value": "{number-16}" + }, + "md": { + "$type": "fontSizes", + "$value": "18" + }, + "lg": { + "$type": "fontSizes", + "$value": "{number-20}" + }, + "xl": { + "$type": "fontSizes", + "$value": "{number-24}" + } + }, + "displays": { + "md": { + "$type": "fontSizes", + "$value": "30" + }, + "lg": { + "$type": "fontSizes", + "$value": "{number-40}" + } + }, + "bodys": { + "lg": { + "$type": "fontSizes", + "$value": "{number-16}" + }, + "md": { + "$type": "fontSizes", + "$value": "15" + }, + "sm": { + "$type": "fontSizes", + "$value": "14" + }, + "xs": { + "$type": "fontSizes", + "$value": "13" + }, + "xxs": { + "$type": "fontSizes", + "$value": "12" + } + }, + "captions": { + "lg": { + "$type": "fontSizes", + "$value": "12" + }, + "md": { + "$type": "fontSizes", + "$value": "11" + }, + "sm": { + "$type": "fontSizes", + "$value": "10" + } + }, + "font-weight-extrabold": { + "$type": "fontWeights", + "$value": "extrabold" + } +} \ No newline at end of file diff --git a/Projects/Shared/DesignSystem/Resources/semantic.json b/Projects/Shared/DesignSystem/Resources/semantic.json new file mode 100644 index 0000000..55c1442 --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/semantic.json @@ -0,0 +1,725 @@ +{ + "background": { + "default": { + "$type": "color", + "$value": "#FAFAF9", + "$description": "전체 화면 배경" + }, + "subtle": { + "$type": "color", + "$value": "#F5F5F4", + "$description": "연한 회색 전체 화면 배경" + }, + "subtler": { + "$type": "color", + "$value": "{gray.50}", + "$description": "진한 회색 전체 화면 배경" + }, + "beige": { + "$type": "color", + "$value": "{beige.200}", + "$description": "마이페이지" + } + }, + "text": { + "default": { + "$type": "color", + "$value": "{gray.800}" + }, + "subtle": { + "$type": "color", + "$value": "{gray.700}" + }, + "subtler": { + "$type": "color", + "$value": "{gray.500}" + }, + "muted": { + "$type": "color", + "$value": "{gray.300}" + }, + "inverse": { + "$type": "color", + "$value": "{beige.50}" + }, + "primary": { + "$type": "color", + "$value": "{primary.500}" + }, + "body": { + "$type": "color", + "$value": "{gray.400}" + }, + "secondary": { + "$type": "color", + "$value": "{secondary.500}" + }, + "error": { + "$type": "color", + "$value": "{error.default}" + } + }, + "border": { + "beige": { + "selected": { + "$type": "color", + "$value": "{beige.700}" + }, + "disabled": { + "$type": "color", + "$value": "{beige.500}" + }, + "focus": { + "$type": "color", + "$value": "{beige.700}" + }, + "default": { + "$type": "color", + "$value": "{beige.600}" + } + }, + "secondary": { + "selected": { + "$type": "color", + "$value": "{secondary.500}" + } + }, + "primary": { + "default": { + "$type": "color", + "$value": "{primary.500}" + } + }, + "error": { + "default": { + "$type": "color", + "$value": "{error.alpha}" + } + }, + "warning": { + "default": { + "$type": "color", + "$value": "{warning.slpha}" + } + }, + "gray": { + "default": { + "$type": "color", + "$value": "{gray.100}" + }, + "subtle": { + "$type": "color", + "$value": "{gray.50}" + } + } + }, + "surface": { + "beige": { + "default": { + "$type": "color", + "$value": "{beige.50}", + "$description": "카드, 리스트 아이템, 입력창 같은 기본 면\n예: 흰색 카드" + }, + "subtle": { + "$type": "color", + "$value": "{beige.300}", + "$description": "베이지 버튼\n예: 홈화면 투표 버튼, 배튼 리스트 버튼" + }, + "strong": { + "$type": "color", + "$value": "{beige.400}" + } + }, + "primary": { + "default": { + "$type": "color", + "$value": "{primary.500}" + }, + "subtle": { + "$type": "color", + "$value": "{primary.50}" + } + } + }, + "action": { + "primary": { + "default": { + "$type": "color", + "$value": "{primary.500}" + }, + "pressed": { + "$type": "color", + "$value": "{primary.800}" + }, + "disabled": { + "$type": "color", + "$value": "{primary.200}" + }, + "subtle": { + "$type": "color", + "$value": "{primary.100}" + } + }, + "beige": { + "default": { + "$type": "color", + "$value": "{beige.300}" + }, + "subtle": { + "$type": "color", + "$value": "{beige.50}" + }, + "pressed": { + "$type": "color", + "$value": "{beige.400}" + }, + "strong": { + "$type": "color", + "$value": "{beige.600}" + } + }, + "secondary": { + "default": { + "$type": "color", + "$value": "{secondary.50}" + } + }, + "gray": { + "default": { + "$type": "color", + "$value": "{gray.700}" + }, + "pressed": { + "$type": "color", + "$value": "{gray.900}" + } + } + }, + "gap": { + "0": { + "$type": "spacing", + "$value": "{spacing-0}" + }, + "2": { + "$type": "spacing", + "$value": "{spacing-2}" + }, + "4": { + "$type": "spacing", + "$value": "{spacing-4}" + }, + "6": { + "$type": "spacing", + "$value": "{spacing-6}" + }, + "8": { + "$type": "spacing", + "$value": "{spacing-8}" + }, + "12": { + "$type": "spacing", + "$value": "{spscing-12}" + }, + "16": { + "$type": "spacing", + "$value": "{spacing-16}" + }, + "20": { + "$type": "spacing", + "$value": "{spacing-20}" + }, + "24": { + "$type": "spacing", + "$value": "{spacing-24}" + }, + "32": { + "$type": "spacing", + "$value": "{spacing-32}" + }, + "40": { + "$type": "spacing", + "$value": "{spacing-40}" + } + }, + "padding": { + "screen": { + "horizontal": { + "$type": "spacing", + "$value": "{spacing-16}" + } + }, + "container": { + "8": { + "$type": "spacing", + "$value": "{spacing-8}" + }, + "12": { + "$type": "spacing", + "$value": "{spscing-12}" + }, + "16": { + "$type": "spacing", + "$value": "{spacing-16}" + }, + "20": { + "$type": "spacing", + "$value": "{spacing-20}" + }, + "24": { + "$type": "spacing", + "$value": "{spacing-24}" + }, + "32": { + "$type": "spacing", + "$value": "{spacing-32}" + }, + "40": { + "$type": "spacing", + "$value": "{spacing-40}" + } + }, + "component": { + "2": { + "$type": "spacing", + "$value": "{spacing-2}" + }, + "4": { + "$type": "spacing", + "$value": "{spacing-4}" + }, + "6": { + "$type": "spacing", + "$value": "{spacing-6}" + }, + "8": { + "$type": "spacing", + "$value": "{spacing-8}" + }, + "12": { + "$type": "spacing", + "$value": "{spscing-12}" + }, + "16": { + "$type": "spacing", + "$value": "{spacing-16}" + } + } + }, + "radius-default": { + "$type": "borderRadius", + "$value": "{number-2}" + }, + "radius-max": { + "$type": "borderRadius", + "$value": "999" + }, + "border-width-regular": { + "$type": "borderWidth", + "$value": "1" + }, + "border-width-medium": { + "$type": "borderWidth", + "$value": "1.5" + }, + "border-width-large": { + "$type": "borderWidth", + "$value": "4" + }, + "heading": { + "xl": { + "$type": "typography", + "$value": { + "fontFamily": "{type}", + "fontWeight": "{font-weight-semibold}", + "fontSize": "{headings.xl}", + "lineHeight": "{130}", + "letterSpacing": "{number-0}" + } + }, + "lg": { + "$type": "typography", + "$value": { + "fontFamily": "{type}", + "fontWeight": "{font-weight-semibold}", + "fontSize": "{headings.lg}", + "lineHeight": "{130}", + "letterSpacing": "{number-0}" + } + }, + "md": { + "$type": "typography", + "$value": { + "fontFamily": "{type}", + "fontWeight": "{font-weight-bold}", + "fontSize": "{headings.md}", + "lineHeight": "{130}", + "letterSpacing": "{number-0}" + } + }, + "sm": { + "$type": "typography", + "$value": { + "fontFamily": "{type}", + "fontWeight": "{font-weight-semibold}", + "fontSize": "{headings.sm}", + "lineHeight": "{130}", + "letterSpacing": "{number-0}" + } + }, + "xs": { + "$type": "typography", + "$value": { + "fontFamily": "{type}", + "fontSize": "{headings.xs}", + "fontWeight": "{font-weight-semibold}", + "lineHeight": "{130}", + "letterSpacing": "{number-0}" + } + } + }, + "body": { + "lg": { + "regular": { + "$type": "typography", + "$value": { + "fontFamily": "{type}", + "fontWeight": "{font-weight-regular}", + "fontSize": "{bodys.lg}", + "lineHeight": "{150}", + "letterSpacing": "{number-0}" + } + }, + "medium": { + "$type": "typography", + "$value": { + "fontFamily": "{type}", + "fontWeight": "{font-weight-medium}", + "fontSize": "{bodys.lg}", + "lineHeight": "{150}", + "letterSpacing": "{number-0}" + } + }, + "semibold": { + "$type": "typography", + "$value": { + "fontFamily": "{type}", + "fontWeight": "{font-weight-semibold}", + "fontSize": "{bodys.lg}", + "lineHeight": "{150}", + "letterSpacing": "{number-0}" + } + }, + "bold": { + "$type": "typography", + "$value": { + "fontFamily": "{type}", + "fontWeight": "{font-weight-bold}", + "fontSize": "{bodys.lg}", + "lineHeight": "{150}", + "letterSpacing": "{number-0}" + } + } + }, + "md": { + "semebold": { + "$type": "typography", + "$value": { + "fontFamily": "{type}", + "fontWeight": "{font-weight-semibold}", + "fontSize": "{bodys.md}", + "lineHeight": "{150}", + "letterSpacing": "{number-0}" + } + }, + "medium": { + "$type": "typography", + "$value": { + "fontFamily": "{type}", + "fontWeight": "{font-weight-medium}", + "fontSize": "{bodys.md}", + "lineHeight": "{150}", + "letterSpacing": "{number-0}" + } + }, + "bold": { + "$type": "typography", + "$value": { + "fontFamily": "{type}", + "fontWeight": "{font-weight-bold}", + "fontSize": "{bodys.md}", + "lineHeight": "{150}", + "letterSpacing": "{number-0}" + } + } + }, + "sm": { + "regular": { + "$type": "typography", + "$value": { + "fontFamily": "{type}", + "fontWeight": "{font-weight-regular}", + "fontSize": "{bodys.sm}", + "lineHeight": "{140}", + "letterSpacing": "{number-0}" + } + }, + "medium": { + "$type": "typography", + "$value": { + "fontFamily": "{type}", + "fontWeight": "{font-weight-medium}", + "fontSize": "{bodys.sm}", + "lineHeight": "{140}", + "letterSpacing": "{number-0}" + } + }, + "semibold": { + "$type": "typography", + "$value": { + "fontFamily": "{type}", + "fontWeight": "{font-weight-semibold}", + "fontSize": "{bodys.sm}", + "lineHeight": "{140}", + "letterSpacing": "{number-0}" + } + } + }, + "xs": { + "medium": { + "$type": "typography", + "$value": { + "fontFamily": "{type}", + "fontWeight": "{font-weight-medium}", + "fontSize": "{bodys.xs}", + "lineHeight": "{140}", + "letterSpacing": "{number-0}" + } + }, + "semibold": { + "$type": "typography", + "$value": { + "fontFamily": "{type}", + "fontWeight": "{font-weight-semibold}", + "fontSize": "{bodys.xs}", + "lineHeight": "{140}", + "letterSpacing": "{number-0}" + } + }, + "regular": { + "$type": "typography", + "$value": { + "fontFamily": "{type}", + "fontWeight": "{font-weight-regular}", + "fontSize": "{bodys.xs}", + "lineHeight": "{140}", + "letterSpacing": "{number-0}" + } + } + }, + "xxs": { + "semibold": { + "$type": "typography", + "$value": { + "fontFamily": "{type}", + "fontWeight": "{font-weight-semibold}", + "fontSize": "{bodys.xxs}", + "lineHeight": "{140}", + "letterSpacing": "{number-0}" + } + }, + "regular": { + "$type": "typography", + "$value": { + "fontFamily": "{type}", + "fontWeight": "{font-weight-regular}", + "fontSize": "{bodys.xxs}", + "lineHeight": "{140}", + "letterSpacing": "{number-0}" + } + }, + "medium": { + "$type": "typography", + "$value": { + "fontFamily": "{type}", + "fontWeight": "{font-weight-medium}", + "fontSize": "{bodys.xxs}", + "lineHeight": "{140}", + "letterSpacing": "{number-0}" + } + } + } + }, + "display": { + "lg": { + "$type": "typography", + "$value": { + "fontFamily": "{type}", + "fontWeight": "{font-weight-extrabold}", + "fontSize": "{displays.lg}", + "lineHeight": "{130}", + "letterSpacing": "{number-0}" + } + }, + "md": { + "$type": "typography", + "$value": { + "fontFamily": "{type}", + "fontWeight": "{font-weight-semibold}", + "fontSize": "{displays.md}", + "lineHeight": "{130}", + "letterSpacing": "{number-0}" + } + } + }, + "caption": { + "lg": { + "regular": { + "$type": "typography", + "$value": { + "fontFamily": "{type}", + "fontWeight": "{font-weight-regular}", + "fontSize": "{captions.lg}", + "lineHeight": "{140}", + "letterSpacing": "{number-0}" + } + }, + "semibold": { + "$type": "typography", + "$value": { + "fontFamily": "{type}", + "fontWeight": "{font-weight-semibold}", + "fontSize": "{captions.lg}", + "lineHeight": "{140}", + "letterSpacing": "{number-0}" + } + }, + "bold": { + "$type": "typography", + "$value": { + "fontFamily": "{type}", + "fontWeight": "{font-weight-bold}", + "fontSize": "{captions.lg}", + "lineHeight": "{140}", + "letterSpacing": "{number-0}" + } + }, + "medium": { + "$type": "typography", + "$value": { + "fontFamily": "{type}", + "fontWeight": "{font-weight-medium}", + "fontSize": "{captions.md}", + "lineHeight": "{140}", + "letterSpacing": "{number-0}" + } + } + }, + "md": { + "semibold": { + "$type": "typography", + "$value": { + "fontFamily": "{type}", + "fontWeight": "{font-weight-semibold}", + "fontSize": "{captions.md}", + "lineHeight": "{140}", + "letterSpacing": "{number-0}" + } + }, + "medium": { + "$type": "typography", + "$value": { + "fontFamily": "{type}", + "fontWeight": "{font-weight-medium}", + "fontSize": "{captions.md}", + "lineHeight": "{140}", + "letterSpacing": "{number-0}" + } + }, + "regular": { + "$type": "typography", + "$value": { + "fontFamily": "{type}", + "fontWeight": "{font-weight-regular}", + "fontSize": "{captions.md}", + "lineHeight": "{140}", + "letterSpacing": "{number-0}" + } + }, + "bold": { + "$type": "typography", + "$value": { + "fontFamily": "{type}", + "fontWeight": "{font-weight-bold}", + "fontSize": "{captions.md}", + "lineHeight": "{140}", + "letterSpacing": "{number-0}" + } + } + }, + "sm": { + "medium": { + "$type": "typography", + "$value": { + "fontFamily": "{type}", + "fontWeight": "{font-weight-medium}", + "fontSize": "{captions.sm}", + "lineHeight": "{140}", + "letterSpacing": "{number-0}" + } + }, + "semibold": { + "$type": "typography", + "$value": { + "fontFamily": "{type}", + "fontWeight": "{font-weight-semibold}", + "fontSize": "{captions.sm}", + "lineHeight": "{140}", + "letterSpacing": "{number-0}" + } + }, + "bold": { + "$type": "typography", + "$value": { + "fontFamily": "{type}", + "fontWeight": "{font-weight-bold}", + "fontSize": "{captions.sm}", + "lineHeight": "{140}", + "letterSpacing": "{number-0}" + } + } + } + }, + "default": { + "$type": "boxShadow", + "$value": { + "x": "0", + "y": "-4", + "blur": "12", + "spread": "0", + "color": "rgba(0,0,0,0.08)", + "type": "dropShadow" + } + }, + "icon": { + "gray": { + "default": { + "$type": "color", + "$value": "{gray.900}" + }, + "subtle": { + "$type": "color", + "$value": "{gray.300}" + }, + "inverse": { + "$type": "color", + "$value": "{text.inverse}" + } + }, + "primary": { + "default": { + "$type": "color", + "$value": "{primary.500}" + } + } + } +} \ No newline at end of file diff --git a/Projects/Shared/DesignSystem/Sources/Color/ShapeStyle+.swift b/Projects/Shared/DesignSystem/Sources/Color/ShapeStyle+.swift index 499baa8..f705a69 100644 --- a/Projects/Shared/DesignSystem/Sources/Color/ShapeStyle+.swift +++ b/Projects/Shared/DesignSystem/Sources/Color/ShapeStyle+.swift @@ -1,11 +1,11 @@ // AUTO-GENERATED by Tools/TokenGenerator.swift — DO NOT EDIT -// Source: Projects/Shared/DesignSystem/Resources/Mode 1.tokens.json +// Source: Projects/Shared/DesignSystem/Resources/{primitive,semantic,component}.json import SwiftUI public extension ShapeStyle where Self == Color { - // MARK: - Brand / Primary + // MARK: - Primitive / Primary static var primary50: Color { .init(hex: "F3EBE9") } static var primary100: Color { .init(hex: "E7D7D3") } static var primary200: Color { .init(hex: "D0AFA8") } @@ -16,9 +16,8 @@ public extension ShapeStyle where Self == Color { static var primary700: Color { .init(hex: "71372A") } static var primary800: Color { .init(hex: "653226") } static var primary900: Color { .init(hex: "4E2A21") } - static var primaryAlpha8: Color { .init(hex: "893825", alpha: 0.08) } - // MARK: - Brand / Secondary + // MARK: - Primitive / Secondary static var secondary50: Color { .init(hex: "FCF8F1") } static var secondary100: Color { .init(hex: "F9F1E3") } static var secondary200: Color { .init(hex: "F3E3C7") } @@ -30,7 +29,7 @@ public extension ShapeStyle where Self == Color { static var secondary800: Color { .init(hex: "A38653") } static var secondary900: Color { .init(hex: "92784A") } - // MARK: - Brand / Beige + // MARK: - Primitive / Beige static var beige50: Color { .init(hex: "FEFEFD") } static var beige100: Color { .init(hex: "FDFCFB") } static var beige200: Color { .init(hex: "FBF9F7") } @@ -42,82 +41,154 @@ public extension ShapeStyle where Self == Color { static var beige800: Color { .init(hex: "CEC1A8") } static var beige900: Color { .init(hex: "B7A88B") } - // MARK: - Brand / Neutral - static var neutral50: Color { .init(hex: "EBEBEB") } - static var neutral100: Color { .init(hex: "D7D7D7") } - static var neutral200: Color { .init(hex: "B0AFAE") } - static var neutral300: Color { .init(hex: "888786") } - static var neutral400: Color { .init(hex: "615F5D") } - static var neutral500: Color { .init(hex: "393735") } - static var neutral600: Color { .init(hex: "2B2A28") } - static var neutral700: Color { .init(hex: "222120") } - static var neutral800: Color { .init(hex: "1A1918") } - static var neutral900: Color { .init(hex: "131212") } + // MARK: - Primitive / Gray + static var gray50: Color { .init(hex: "EBEBEB") } + static var gray100: Color { .init(hex: "D7D7D7") } + static var gray200: Color { .init(hex: "B0AFAE") } + static var gray300: Color { .init(hex: "888786") } + static var gray400: Color { .init(hex: "615F5D") } + static var gray500: Color { .init(hex: "393735") } + static var gray600: Color { .init(hex: "2B2A28") } + static var gray700: Color { .init(hex: "222120") } + static var gray800: Color { .init(hex: "1A1918") } + static var gray900: Color { .init(hex: "131212") } + + // MARK: - Primitive / Status + static var errorAlpha: Color { .init(hex: "C92D33", alpha: 0.4) } + static var errorDefault: Color { .init(hex: "C92D33") } + static var warningDefault: Color { .init(hex: "FFB400") } + static var warningAlpha: Color { .init(hex: "FFB400", alpha: 0.4) } + + // MARK: - Semantic / Background + static var bgBeige: Color { .beige200 } + static var bgDefault: Color { .init(hex: "FAFAF9") } + static var bgSubtle: Color { .init(hex: "F5F5F4") } + static var bgSubtler: Color { .gray50 } // MARK: - Semantic / Text - static var textBrand: Color { .primary500 } - static var textInverse: Color { .init(hex: "FEFEFD") } - static var textMuted: Color { .neutral300 } - static var textPrimary: Color { .neutral900 } - static var textSecondary: Color { .neutral700 } - static var textTertiary: Color { .neutral500 } + static var textBody: Color { .gray400 } + static var textDefault: Color { .gray800 } + static var textError: Color { .errorDefault } + static var textInverse: Color { .beige50 } + static var textMuted: Color { .gray300 } + static var textPrimary: Color { .primary500 } + static var textSecondary: Color { .secondary500 } + static var textSubtle: Color { .gray700 } + static var textSubtler: Color { .gray500 } // MARK: - Semantic / Border - static var borderDefault: Color { .beige600 } - static var borderDisabled: Color { .beige500 } - static var borderError: Color { .statusErrorAlpha } - static var borderFocus: Color { .beige700 } - static var borderSelected: Color { .secondary500 } - static var borderStrong: Color { .primary500 } - static var borderSubtle: Color { .beige700 } + static var borderBeigeDefault: Color { .beige600 } + static var borderBeigeDisabled: Color { .beige500 } + static var borderBeigeFocus: Color { .beige700 } + static var borderBeigeSelected: Color { .beige700 } + static var borderErrorDefault: Color { .init(hex: "C92D33", alpha: 0.4) } + static var borderGrayDefault: Color { .gray100 } + static var borderGraySubtle: Color { .gray50 } + static var borderPrimaryDefault: Color { .primary500 } + static var borderSecondarySelected: Color { .secondary500 } + static var borderWarningDefault: Color { .init(hex: "FFB400", alpha: 0.4) } // MARK: - Semantic / Surface - static var surfaceDefault: Color { .beige50 } - static var surfaceDisabled: Color { .primary200 } - static var surfaceSelected: Color { .primary500 } - static var surfaceSubtle: Color { .beige300 } - static var surfaceTertiary: Color { .beige400 } + static var surfaceBeigeDefault: Color { .beige50 } + static var surfaceBeigeStrong: Color { .beige400 } + static var surfaceBeigeSubtle: Color { .beige300 } + static var surfacePrimaryDefault: Color { .primary500 } + static var surfacePrimarySubtle: Color { .primary50 } + + // MARK: - Semantic / Action + static var actionBeigeDefault: Color { .beige300 } + static var actionBeigePressed: Color { .beige400 } + static var actionBeigeStrong: Color { .beige600 } + static var actionBeigeSubtle: Color { .beige50 } + static var actionGrayDefault: Color { .gray700 } + static var actionGrayPressed: Color { .gray900 } + static var actionPrimaryDefault: Color { .primary500 } + static var actionPrimaryDisabled: Color { .primary200 } + static var actionPrimaryPressed: Color { .primary800 } + static var actionPrimarySubtle: Color { .primary100 } + static var actionSecondaryDefault: Color { .secondary50 } + + // MARK: - Semantic / Icon + static var iconGrayDefault: Color { .gray900 } + static var iconGrayInverse: Color { .beige50 } + static var iconGraySubtle: Color { .gray300 } + static var iconPrimaryDefault: Color { .primary500 } - // MARK: - Semantic / Background - static var bgBrand: Color { .beige200 } - static var bgDefault: Color { .init(hex: "FAFAF9") } - static var bgInverse: Color { .neutral800 } - static var bgOverlay: Color { .init(hex: "000000", alpha: 0.4) } - static var bgSubtle: Color { .init(hex: "F5F5F4") } - static var bgTertiary: Color { .neutral50 } - static var borderGray: Color { .init(hex: "CCCCCC") } - static var gray50: Color { .init(hex: "FFFFFF")} - - // MARK: - Semantic / Status - static var statusErrorAlpha: Color { .init(hex: "C92D33", alpha: 0.4) } - static var statusError: Color { .init(hex: "C92D33") } - static var statusWarningAlpha: Color { .init(hex: "FFB400", alpha: 0.4) } - static var statusWarning: Color { .init(hex: "FFB400") } // MARK: - Component - static var bedgeFilledBackgroundDefault: Color { .beige600 } - static var bedgeFilledBackgroundInverse: Color { .primary500 } - static var bedgeFilledTextDefault: Color { .primary500 } - static var bedgeFilledTextInverse: Color { .beige50 } - static var bedgeOutlineBackround: Color { .beige50 } - static var bedgeOutlineBorder: Color { .primary100 } - static var bedgeOutlineText: Color { .primary500 } + static var avatarBackround: Color { .beige600 } + static var badgeCounterBackground: Color { .gray500 } + static var badgeCounterTextActive: Color { .beige50 } + static var badgeCounterTextDefault: Color { .gray300 } + static var badgeFilledBackground: Color { .beige600 } + static var badgeFilledText: Color { .primary500 } + static var badgeOutlineBackground: Color { .beige50 } + static var badgeOutlineBorder: Color { .primary100 } + static var badgeOutlineText: Color { .primary500 } + static var badgePrimaryBackground: Color { .primary500 } + static var badgePrimaryText: Color { .beige50 } + static var buttonIconBackgroundDefault: Color { .primary500 } + static var buttonIconBackgroundDisabled: Color { .primary200 } static var buttonPrimaryBackgroundDefault: Color { .primary500 } static var buttonPrimaryBackgroundDisabled: Color { .primary200 } static var buttonPrimaryBackgroundPressed: Color { .primary800 } static var buttonPrimaryTextDefault: Color { .beige50 } - static var buttonSecondaryBackgroundDefault: Color { .beige300 } - static var buttonSecondaryBackgroundPressed: Color { .beige400 } - static var buttonSecondaryBorderDefault: Color { .beige600 } - static var buttonSecondaryBorderPressed: Color { .secondary500 } - static var buttonSecondaryTextDefault: Color { .neutral600 } - static var buttonSecondaryTextDisabled: Color { .neutral300 } - static var inputBorderActive: Color { .beige700 } - static var inputBorderDefault: Color { .beige600 } - static var inputBorderError: Color { .borderError } - static var inputSurfaceDefault: Color { .beige50 } - static var inputSurfaceDisabled: Color { .beige300 } - static var inputTextActive: Color { .neutral500 } - static var inputTextDefault: Color { .neutral300 } - static var inputTextError: Color { .statusError } - + static var buttonSecondaryBackgroundDefault: Color { .secondary50 } + static var buttonSecondaryBackgroundPressed: Color { .beige600 } + static var buttonSecondaryTextDefault: Color { .primary500 } + static var cardBaseBackgroundBeige: Color { .beige400 } + static var cardBaseBackgroundDefault: Color { .beige50 } + static var cardBaseBorderDefault: Color { .beige600 } + static var cardBaseBorderPrimary: Color { .primary500 } + static var cardBaseBorderSelected: Color { .secondary500 } + static var cardBaseTextBody: Color { .gray400 } + static var cardBaseTextDecription: Color { .gray300 } + static var cardBaseTextInverse: Color { .beige50 } + static var cardBaseTextPrimary: Color { .primary500 } + static var cardBaseTextSecondary: Color { .secondary500 } + static var cardBaseTextTitle: Color { .gray500 } + static var cardGrayBackgroundDefault: Color { .gray700 } + static var cardGrayBackgroundPressed: Color { .gray900 } + static var cardGrayBorderSelected: Color { .secondary500 } + static var cardOpinionBackgroundDefault: Color { .beige300 } + static var cardOpinionBackgroundPressed: Color { .beige400 } + static var cardOpinionBorderDefault: Color { .beige600 } + static var cardOpinionBorderPressed: Color { .secondary500 } + static var checkboxBackgroundDefault: Color { .primary50 } + static var checkboxBackgroundSelected: Color { .primary500 } + static var checkboxBorderDefault: Color { .primary100 } + static var checkboxBorderSelected: Color { .primary700 } + static var chipBackgroundDefault: Color { .primary500 } + static var chipBackgroundSelected: Color { .primary50 } + static var chipBorderDefault: Color { .primary500 } + static var chipTextDefault: Color { .beige50 } + static var chipTextSelected: Color { .primary500 } + static var inputTextareaBackgroundDefault: Color { .beige50 } + static var inputTextareaBackgroundDisabled: Color { .beige600 } + static var inputTextareaBorderError: Color { .init(hex: "C92D33", alpha: 0.4) } + static var inputTextareaTextDefault: Color { .gray300 } + static var inputTextareaTextError: Color { .errorDefault } + static var inputTextareaTextFocus: Color { .gray500 } + static var inputTextfieldBackgroundDefault: Color { .beige50 } + static var inputTextfieldBackgroundDisabled: Color { .beige400 } + static var inputTextfieldBorderDefault: Color { .beige600 } + static var inputTextfieldBorderError: Color { .primary100 } + static var inputTextfieldBorderFocus: Color { .beige700 } + static var inputTextfieldTextDefault: Color { .gray300 } + static var inputTextfieldTextError: Color { .errorDefault } + static var inputTextfieldTextFocus: Color { .gray500 } + static var listItemAgreementBackgroundDefault: Color { .bgDefault } + static var listItemAgreementBorderDefault: Color { .gray50 } + static var listItemAgreementBorderError: Color { .init(hex: "C92D33", alpha: 0.4) } + static var listItemAgreementTextDefualt: Color { .gray800 } + static var listItemAgreementTextError: Color { .errorDefault } + static var navigationTabBorderActive: Color { .primary500 } + static var navigationTabBorderDefault: Color { .gray100 } + static var navigationTabTextActive: Color { .primary500 } + static var navigationTabTextDefault: Color { .gray300 } + static var popupBackground: Color { .beige400 } + static var popupBorder: Color { .primary500 } + static var popupText: Color { .gray800 } + static var toggleThumbDefault: Color { .beige50 } + static var toggleTrackOff: Color { .gray100 } + static var toggleTrackOn: Color { .primary500 } } diff --git a/Projects/Shared/DesignSystem/Sources/Extension/CGFloat/CGFloat+Component+.swift b/Projects/Shared/DesignSystem/Sources/Extension/CGFloat/CGFloat+Component+.swift index 663eae5..1b27577 100644 --- a/Projects/Shared/DesignSystem/Sources/Extension/CGFloat/CGFloat+Component+.swift +++ b/Projects/Shared/DesignSystem/Sources/Extension/CGFloat/CGFloat+Component+.swift @@ -1,10 +1,45 @@ // AUTO-GENERATED by Tools/TokenGenerator.swift — DO NOT EDIT -// Source: Projects/Shared/DesignSystem/Resources/Mode 1.tokens.json +// Source: Projects/Shared/DesignSystem/Resources/{primitive,semantic,component}.json import CoreGraphics public extension CGFloat { // MARK: - Component - static let buttonRadius: CGFloat = .`default` + static let buttonIconSize: CGFloat = 36 + static let buttonPrimaryLargeHeight: CGFloat = 52 + static let buttonPrimaryLargeWidth: CGFloat = 343 + static let buttonPrimaryMediumHeight: CGFloat = 42 + static let buttonPrimaryMediumWidth: CGFloat = 343 + static let buttonPrimarySmallHeight: CGFloat = 42 + static let buttonPrimarySmallWidth: CGFloat = 100 + static let cardAvatarLg: CGFloat = 68 + static let cardAvatarMd: CGFloat = 40 + static let cardAvatarSm: CGFloat = 36 + static let checkboxHeight: CGFloat = 24 + static let checkboxWidth: CGFloat = 24 + static let inputTextfieldHeight: CGFloat = 44 + static let inputTextfieldWidth: CGFloat = 343 + static let paddingAppbarHorizontal: CGFloat = 16 + static let paddingAppbarVertical: CGFloat = 12 + static let paddingBadgeHorizontal: CGFloat = 6 + static let paddingBadgeVertical: CGFloat = 2 + static let paddingButtonPrimaryLargeHorizontal: CGFloat = 12 + static let paddingButtonPrimaryLargeVertical: CGFloat = 12 + static let paddingButtonPrimaryMediumHorizontal: CGFloat = 12 + static let paddingButtonPrimaryMediumVertical: CGFloat = 12 + static let paddingButtonPrimarySmallHorizontal: CGFloat = 12 + static let paddingButtonPrimarySmallVertical: CGFloat = 12 + static let paddingCardBase: CGFloat = 16 + static let paddingChipHorizontal: CGFloat = 12 + static let paddingChipVertical: CGFloat = 6 + static let paddingInputHorizontal: CGFloat = 8 + static let paddingNavigationTabHorizontal: CGFloat = 12 + static let paddingNavigationTabVertical: CGFloat = 12 + static let popupWidth: CGFloat = 393 + static let thumbnailHeight: CGFloat = 140 + static let thumbnailWidth: CGFloat = 196 + static let toggleHeight: CGFloat = 18 + static let toggleThumbSize: CGFloat = 14 + static let toggleWidth: CGFloat = 32 } diff --git a/Projects/Shared/DesignSystem/Sources/Extension/CGFloat/CGFloat+Radius+.swift b/Projects/Shared/DesignSystem/Sources/Extension/CGFloat/CGFloat+Radius+.swift index ead9896..ebdfce9 100644 --- a/Projects/Shared/DesignSystem/Sources/Extension/CGFloat/CGFloat+Radius+.swift +++ b/Projects/Shared/DesignSystem/Sources/Extension/CGFloat/CGFloat+Radius+.swift @@ -1,12 +1,11 @@ // AUTO-GENERATED by Tools/TokenGenerator.swift — DO NOT EDIT -// Source: Projects/Shared/DesignSystem/Resources/Mode 1.tokens.json +// Source: Projects/Shared/DesignSystem/Resources/{primitive,semantic,component}.json import CoreGraphics public extension CGFloat { // MARK: - Radius - static let none: CGFloat = 0 - static let `default`: CGFloat = 2 - static let full: CGFloat = 999 + static let radiusDefault: CGFloat = 2 + static let radiusMax: CGFloat = 999 } diff --git a/Projects/Shared/DesignSystem/Sources/Extension/CGFloat/CGFloat+Spacing+.swift b/Projects/Shared/DesignSystem/Sources/Extension/CGFloat/CGFloat+Spacing+.swift index 03bff40..7113d5d 100644 --- a/Projects/Shared/DesignSystem/Sources/Extension/CGFloat/CGFloat+Spacing+.swift +++ b/Projects/Shared/DesignSystem/Sources/Extension/CGFloat/CGFloat+Spacing+.swift @@ -1,16 +1,19 @@ // AUTO-GENERATED by Tools/TokenGenerator.swift — DO NOT EDIT -// Source: Projects/Shared/DesignSystem/Resources/Mode 1.tokens.json +// Source: Projects/Shared/DesignSystem/Resources/{primitive,semantic,component}.json import CoreGraphics public extension CGFloat { - // MARK: - Spacing + // MARK: - Primitive Spacing static let s0: CGFloat = 0 static let s2: CGFloat = 2 static let s4: CGFloat = 4 + static let s6: CGFloat = 6 static let s8: CGFloat = 8 + static let s12: CGFloat = 12 static let s16: CGFloat = 16 + static let s20: CGFloat = 20 static let s24: CGFloat = 24 static let s32: CGFloat = 32 static let s40: CGFloat = 40 @@ -18,4 +21,49 @@ public extension CGFloat { static let s64: CGFloat = 64 static let s80: CGFloat = 80 static let s96: CGFloat = 96 + + // MARK: - Primitive Sizing + static let iconLg: CGFloat = 24 + static let iconMd: CGFloat = 20 + static let iconSm: CGFloat = 16 + static let iconXs: CGFloat = 12 + static let avatarLg: CGFloat = 68 + static let avatarMd: CGFloat = 40 + static let avatarSm: CGFloat = 36 + static let controlMd: CGFloat = 52 + static let controlSm: CGFloat = 28 + + // MARK: - Semantic Gap + static let gap0: CGFloat = 0 + static let gap2: CGFloat = 2 + static let gap4: CGFloat = 4 + static let gap6: CGFloat = 6 + static let gap8: CGFloat = 8 + static let gap12: CGFloat = 12 + static let gap16: CGFloat = 16 + static let gap20: CGFloat = 20 + static let gap24: CGFloat = 24 + static let gap32: CGFloat = 32 + static let gap40: CGFloat = 40 + + // MARK: - Semantic Padding + static let paddingComponent12: CGFloat = 12 + static let paddingComponent16: CGFloat = 16 + static let paddingComponent2: CGFloat = 2 + static let paddingComponent4: CGFloat = 4 + static let paddingComponent6: CGFloat = 6 + static let paddingComponent8: CGFloat = 8 + static let paddingContainer12: CGFloat = 12 + static let paddingContainer16: CGFloat = 16 + static let paddingContainer20: CGFloat = 20 + static let paddingContainer24: CGFloat = 24 + static let paddingContainer32: CGFloat = 32 + static let paddingContainer40: CGFloat = 40 + static let paddingContainer8: CGFloat = 8 + static let paddingScreenHorizontal: CGFloat = 16 + + // MARK: - Semantic Border Width + static let borderWidthRegular: CGFloat = 1 + static let borderWidthMedium: CGFloat = 1.5 + static let borderWidthLarge: CGFloat = 4 } diff --git a/Projects/Shared/DesignSystem/Sources/UI/Token/ComponentToken.swift b/Projects/Shared/DesignSystem/Sources/UI/Token/ComponentToken.swift index 6b2e567..eb175eb 100644 --- a/Projects/Shared/DesignSystem/Sources/UI/Token/ComponentToken.swift +++ b/Projects/Shared/DesignSystem/Sources/UI/Token/ComponentToken.swift @@ -1,31 +1,49 @@ // AUTO-GENERATED by Tools/TokenGenerator.swift — DO NOT EDIT -// Source: Projects/Shared/DesignSystem/Resources/Mode 1.tokens.json +// Source: Projects/Shared/DesignSystem/Resources/{primitive,semantic,component}.json import SwiftUI public enum ComponentToken { - public enum Bedge { - public enum Filled { - public enum Background { - public static var `default`: Color { .bedgeFilledBackgroundDefault } - public static var inverse: Color { .bedgeFilledBackgroundInverse } - } + public enum Avatar { + public static var backround: Color { .avatarBackround } + } + + public enum Badge { + public enum Counter { + public static var background: Color { .badgeCounterBackground } public enum Text { - public static var `default`: Color { .bedgeFilledTextDefault } - public static var inverse: Color { .bedgeFilledTextInverse } + public static var active: Color { .badgeCounterTextActive } + public static var `default`: Color { .badgeCounterTextDefault } } } + public enum Filled { + public static var background: Color { .badgeFilledBackground } + public static var text: Color { .badgeFilledText } + } + public enum Outline { - public static var backround: Color { .bedgeOutlineBackround } - public static var border: Color { .bedgeOutlineBorder } - public static var text: Color { .bedgeOutlineText } + public static var background: Color { .badgeOutlineBackground } + public static var border: Color { .badgeOutlineBorder } + public static var text: Color { .badgeOutlineText } + } + + public enum Primary { + public static var background: Color { .badgePrimaryBackground } + public static var text: Color { .badgePrimaryText } } } public enum Button { - public static var radius: CGFloat { .buttonRadius } + public enum Icon { + public static var size: CGFloat { .buttonIconSize } + + public enum Background { + public static var `default`: Color { .buttonIconBackgroundDefault } + public static var disabled: Color { .buttonIconBackgroundDisabled } + } + } public enum Primary { public enum Background { @@ -34,6 +52,21 @@ public enum ComponentToken { public static var pressed: Color { .buttonPrimaryBackgroundPressed } } + public enum Large { + public static var height: CGFloat { .buttonPrimaryLargeHeight } + public static var width: CGFloat { .buttonPrimaryLargeWidth } + } + + public enum Medium { + public static var height: CGFloat { .buttonPrimaryMediumHeight } + public static var width: CGFloat { .buttonPrimaryMediumWidth } + } + + public enum Small { + public static var height: CGFloat { .buttonPrimarySmallHeight } + public static var width: CGFloat { .buttonPrimarySmallWidth } + } + public enum Text { public static var `default`: Color { .buttonPrimaryTextDefault } } @@ -45,34 +78,244 @@ public enum ComponentToken { public static var pressed: Color { .buttonSecondaryBackgroundPressed } } + public enum Text { + public static var `default`: Color { .buttonSecondaryTextDefault } + } + } + } + + public enum Card { + public enum Avatar { + public static var lg: CGFloat { .cardAvatarLg } + public static var md: CGFloat { .cardAvatarMd } + public static var sm: CGFloat { .cardAvatarSm } + } + + public enum Base { + public enum Background { + public static var beige: Color { .cardBaseBackgroundBeige } + public static var `default`: Color { .cardBaseBackgroundDefault } + } + public enum Border { - public static var `default`: Color { .buttonSecondaryBorderDefault } - public static var pressed: Color { .buttonSecondaryBorderPressed } + public static var `default`: Color { .cardBaseBorderDefault } + public static var primary: Color { .cardBaseBorderPrimary } + public static var selected: Color { .cardBaseBorderSelected } } public enum Text { - public static var `default`: Color { .buttonSecondaryTextDefault } - public static var disabled: Color { .buttonSecondaryTextDisabled } + public static var body: Color { .cardBaseTextBody } + public static var decription: Color { .cardBaseTextDecription } + public static var inverse: Color { .cardBaseTextInverse } + public static var primary: Color { .cardBaseTextPrimary } + public static var secondary: Color { .cardBaseTextSecondary } + public static var title: Color { .cardBaseTextTitle } + } + } + + public enum Gray { + public enum Background { + public static var `default`: Color { .cardGrayBackgroundDefault } + public static var pressed: Color { .cardGrayBackgroundPressed } + } + + public enum Border { + public static var selected: Color { .cardGrayBorderSelected } + } + } + + public enum Opinion { + public enum Background { + public static var `default`: Color { .cardOpinionBackgroundDefault } + public static var pressed: Color { .cardOpinionBackgroundPressed } + } + + public enum Border { + public static var `default`: Color { .cardOpinionBorderDefault } + public static var pressed: Color { .cardOpinionBorderPressed } } } } - public enum Input { + public enum Checkbox { + public static var height: CGFloat { .checkboxHeight } + public static var width: CGFloat { .checkboxWidth } + + public enum Background { + public static var `default`: Color { .checkboxBackgroundDefault } + public static var selected: Color { .checkboxBackgroundSelected } + } + public enum Border { - public static var active: Color { .inputBorderActive } - public static var `default`: Color { .inputBorderDefault } - public static var error: Color { .inputBorderError } + public static var `default`: Color { .checkboxBorderDefault } + public static var selected: Color { .checkboxBorderSelected } + } + } + + public enum Chip { + public enum Background { + public static var `default`: Color { .chipBackgroundDefault } + public static var selected: Color { .chipBackgroundSelected } } - public enum Surface { - public static var `default`: Color { .inputSurfaceDefault } - public static var disabled: Color { .inputSurfaceDisabled } + public enum Border { + public static var `default`: Color { .chipBorderDefault } } public enum Text { - public static var active: Color { .inputTextActive } - public static var `default`: Color { .inputTextDefault } - public static var error: Color { .inputTextError } + public static var `default`: Color { .chipTextDefault } + public static var selected: Color { .chipTextSelected } + } + } + + public enum Input { + public enum Textarea { + public enum Background { + public static var `default`: Color { .inputTextareaBackgroundDefault } + public static var disabled: Color { .inputTextareaBackgroundDisabled } + } + + public enum Border { + public static var error: Color { .inputTextareaBorderError } + } + + public enum Text { + public static var `default`: Color { .inputTextareaTextDefault } + public static var error: Color { .inputTextareaTextError } + public static var focus: Color { .inputTextareaTextFocus } + } + } + + public enum Textfield { + public static var height: CGFloat { .inputTextfieldHeight } + public static var width: CGFloat { .inputTextfieldWidth } + + public enum Background { + public static var `default`: Color { .inputTextfieldBackgroundDefault } + public static var disabled: Color { .inputTextfieldBackgroundDisabled } + } + + public enum Border { + public static var `default`: Color { .inputTextfieldBorderDefault } + public static var error: Color { .inputTextfieldBorderError } + public static var focus: Color { .inputTextfieldBorderFocus } + } + + public enum Text { + public static var `default`: Color { .inputTextfieldTextDefault } + public static var error: Color { .inputTextfieldTextError } + public static var focus: Color { .inputTextfieldTextFocus } + } + } + } + + public enum ListItem { + public enum Agreement { + public enum Background { + public static var `default`: Color { .listItemAgreementBackgroundDefault } + } + + public enum Border { + public static var `default`: Color { .listItemAgreementBorderDefault } + public static var error: Color { .listItemAgreementBorderError } + } + + public enum Text { + public static var defualt: Color { .listItemAgreementTextDefualt } + public static var error: Color { .listItemAgreementTextError } + } + } + } + + public enum Navigation { + public enum Tab { + public enum Border { + public static var active: Color { .navigationTabBorderActive } + public static var `default`: Color { .navigationTabBorderDefault } + } + + public enum Text { + public static var active: Color { .navigationTabTextActive } + public static var `default`: Color { .navigationTabTextDefault } + } + } + } + + public enum Padding { + public enum Appbar { + public static var horizontal: CGFloat { .paddingAppbarHorizontal } + public static var vertical: CGFloat { .paddingAppbarVertical } + } + + public enum Badge { + public static var horizontal: CGFloat { .paddingBadgeHorizontal } + public static var vertical: CGFloat { .paddingBadgeVertical } + } + + public enum Button { + public enum Primary { + public enum Large { + public static var horizontal: CGFloat { .paddingButtonPrimaryLargeHorizontal } + public static var vertical: CGFloat { .paddingButtonPrimaryLargeVertical } + } + + public enum Medium { + public static var horizontal: CGFloat { .paddingButtonPrimaryMediumHorizontal } + public static var vertical: CGFloat { .paddingButtonPrimaryMediumVertical } + } + + public enum Small { + public static var horizontal: CGFloat { .paddingButtonPrimarySmallHorizontal } + public static var vertical: CGFloat { .paddingButtonPrimarySmallVertical } + } + } + } + + public enum Card { + public static var base: CGFloat { .paddingCardBase } + } + + public enum Chip { + public static var horizontal: CGFloat { .paddingChipHorizontal } + public static var vertical: CGFloat { .paddingChipVertical } + } + + public enum Input { + public static var horizontal: CGFloat { .paddingInputHorizontal } + } + + public enum Navigation { + public enum Tab { + public static var horizontal: CGFloat { .paddingNavigationTabHorizontal } + public static var vertical: CGFloat { .paddingNavigationTabVertical } + } + } + } + + public enum Popup { + public static var background: Color { .popupBackground } + public static var border: Color { .popupBorder } + public static var text: Color { .popupText } + public static var width: CGFloat { .popupWidth } + } + + public enum Thumbnail { + public static var height: CGFloat { .thumbnailHeight } + public static var width: CGFloat { .thumbnailWidth } + } + + public enum Toggle { + public static var height: CGFloat { .toggleHeight } + public static var width: CGFloat { .toggleWidth } + + public enum Thumb { + public static var `default`: Color { .toggleThumbDefault } + public static var size: CGFloat { .toggleThumbSize } + } + + public enum Track { + public static var off: Color { .toggleTrackOff } + public static var on: Color { .toggleTrackOn } } } } diff --git a/Tools/TokenGenerator.swift b/Tools/TokenGenerator.swift index a73f300..bdb04fe 100644 --- a/Tools/TokenGenerator.swift +++ b/Tools/TokenGenerator.swift @@ -1,7 +1,7 @@ #!/usr/bin/env swift // // TokenGenerator.swift -// Reads Mode 1.tokens.json and emits Swift token files. +// Reads {primitive,semantic,component}.json (Tokens Studio format) and emits Swift token files. // Run from repo root: swift Tools/TokenGenerator.swift // @@ -10,67 +10,108 @@ import Foundation // MARK: - Paths let cwd = FileManager.default.currentDirectoryPath -let jsonURL = URL(fileURLWithPath: "\(cwd)/Projects/Shared/DesignSystem/Resources/Mode 1.tokens.json") +let resourcesDir = "\(cwd)/Projects/Shared/DesignSystem/Resources" +let primitiveURL = URL(fileURLWithPath: "\(resourcesDir)/primitive.json") +let semanticURL = URL(fileURLWithPath: "\(resourcesDir)/semantic.json") +let componentURL = URL(fileURLWithPath: "\(resourcesDir)/component.json") + let sourcesDir = "\(cwd)/Projects/Shared/DesignSystem/Sources" let colorOut = "\(sourcesDir)/Color/ShapeStyle+.swift" let cgfloatDir = "\(sourcesDir)/Extension/CGFloat" let radiusOut = "\(cgfloatDir)/CGFloat+Radius+.swift" let spacingOut = "\(cgfloatDir)/CGFloat+Spacing+.swift" -let componentOut = "\(sourcesDir)/UI/Token/ComponentToken.swift" // legacy nested file (deleted at end) let componentNumberOut = "\(cgfloatDir)/CGFloat+Component+.swift" +let componentTokenOut = "\(sourcesDir)/UI/Token/ComponentToken.swift" + try? FileManager.default.createDirectory(atPath: "\(sourcesDir)/UI/Token", withIntermediateDirectories: true) try? FileManager.default.createDirectory(atPath: cgfloatDir, withIntermediateDirectories: true) -let data: Data -do { - data = try Data(contentsOf: jsonURL) -} catch { - fputs("[token-gen] cannot read JSON: \(jsonURL.path)\n", stderr) - exit(1) -} +// MARK: - Loading -guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { - fputs("[token-gen] invalid JSON root\n", stderr); exit(1) +func loadJSON(_ url: URL) -> [String: Any] { + guard let data = try? Data(contentsOf: url), + let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] + else { + fputs("[token-gen] cannot read \(url.path)\n", stderr) + exit(1) + } + return obj } -// MARK: - Helpers +let primitive = loadJSON(primitiveURL) +let semantic = loadJSON(semanticURL) +let component = loadJSON(componentURL) + +// MARK: - Registry & reference resolution -func valueOf(_ any: Any) -> Any? { - (any as? [String: Any])?["$value"] +typealias TokenNode = [String: Any] +typealias Registry = [String: TokenNode] + +/// Flattens a token tree by dotted path. Leaves are `{$type, $value, ...}` dicts. +func flatten(_ tree: [String: Any], path: [String], into registry: inout Registry) { + for (key, value) in tree { + guard let dict = value as? [String: Any] else { continue } + let newPath = path + [key] + if dict["$type"] != nil, dict["$value"] != nil { + registry[newPath.joined(separator: ".")] = dict + } else { + flatten(dict, path: newPath, into: ®istry) + } + } } -func hexAlpha(_ value: Any) -> (hex: String, alpha: Double)? { - guard let d = value as? [String: Any], let hex = d["hex"] as? String else { return nil } - let raw = hex.hasPrefix("#") ? String(hex.dropFirst()) : hex - let a = (d["alpha"] as? Double) ?? 1.0 - return (raw.uppercased(), a) +var registry: Registry = [:] +flatten(primitive, path: [], into: ®istry) +flatten(semantic, path: [], into: ®istry) +flatten(component, path: [], into: ®istry) + +/// `"{primary.500}"` → `"primary.500"`. Nil for non-references. +func referencePath(_ s: String) -> String? { + guard s.hasPrefix("{"), s.hasSuffix("}") else { return nil } + return String(s.dropFirst().dropLast()) } -func aliasToSwiftName(_ alias: String) -> String? { - var s = alias - if s.hasPrefix("{"), s.hasSuffix("}") { s = String(s.dropFirst().dropLast()) } - let p = s.split(separator: ".").map(String.init) - if p.count >= 4, p[0] == "Colors", p[1] == "brand" { - if p.count == 5, p[3] == "Alpha" { return "\(p[2])Alpha\(p[4])" } - return "\(p[2])\(p[3])" +/// Recursively resolves a `$value`, following `{...}` references until a literal is reached. +/// Returns nil only on cycle. +func resolveValue(_ value: Any, visited: Set = []) -> Any? { + if let s = value as? String, let ref = referencePath(s) { + if visited.contains(ref) { return nil } + guard let node = registry[ref], let nested = node["$value"] else { return s } + return resolveValue(nested, visited: visited.union([ref])) } - if p.count >= 5, p[0] == "Colors", p[1] == "semantic", p[2] == "status" { - let bucket = p[3].prefix(1).uppercased() + p[3].dropFirst() - let leaf = p[4] - return leaf == "Alpha" ? "status\(bucket)Alpha" : "status\(bucket)" + return value +} + +/// Resolves to Double for number/spacing/sizing/borderRadius/borderWidth/fontSizes. +func resolveNumber(_ value: Any) -> Double? { + let resolved = resolveValue(value) ?? value + if let n = resolved as? Double { return n } + if let n = resolved as? Int { return Double(n) } + if let s = resolved as? String, let n = Double(s) { return n } + return nil +} + +/// Resolves to a hex color `(uppercase, no '#')` plus alpha. Handles `#RRGGBB` and `rgba(r,g,b,a)`. +func resolveHex(_ value: Any) -> (hex: String, alpha: Double)? { + let resolved = resolveValue(value) ?? value + guard let s = resolved as? String else { return nil } + if s.hasPrefix("#") { + return (String(s.dropFirst()).uppercased(), 1.0) } - if p.count == 4, p[0] == "Colors", p[1] == "semantic" { - let key = p[2] - let prefix = (key == "background") ? "bg" : key - return "\(prefix)\(capitalizeFirst(p[3]))" + if s.hasPrefix("rgba(") { + let inner = s.replacingOccurrences(of: "rgba(", with: "").replacingOccurrences(of: ")", with: "") + let parts = inner.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) } + if parts.count == 4, + let r = Double(parts[0]), let g = Double(parts[1]), let b = Double(parts[2]), let a = Double(parts[3]) + { + let hex = String(format: "%02X%02X%02X", Int(r), Int(g), Int(b)) + return (hex, a) + } } return nil } -// 'Primary200' → 'primary200' / 'BorderError' → 'borderError' -func lowerFirst(_ s: String) -> String { - s.prefix(1).lowercased() + s.dropFirst() -} +// MARK: - Naming helpers let swiftKeywords: Set = [ "default", "case", "enum", "class", "struct", "var", "let", "func", "init", @@ -84,125 +125,26 @@ func swiftKey(_ s: String) -> String { swiftKeywords.contains(s) ? "`\(s)`" : s } -// hex→Swift 변수명 인덱스 (alpha=1 brand/semantic만). Component이 inline hex로 export 됐을 때 fallback 매칭용. -var hexIndex: [String: String] = [:] -var knownColorNames: Set = [] - -func resolveComponentColor(_ node: [String: Any]) -> String? { - if let str = node["$value"] as? String, let name = aliasToSwiftName(str) { - return ".\(name)" - } - if let v = node["$value"] as? [String: Any], let hex = v["hex"] as? String { - let raw = hex.hasPrefix("#") ? String(hex.dropFirst()) : hex - let normalized = raw.uppercased() - let alpha = (v["alpha"] as? Double) ?? 1.0 - // 1) aliasData.targetVariableName — 우리 토큰셋에 존재할 때만 사용 - if let exts = node["$extensions"] as? [String: Any], - let alias = exts["com.figma.aliasData"] as? [String: Any], - let target = alias["targetVariableName"] as? String, !target.isEmpty - { - let camel = lowerFirst(target) - if knownColorNames.contains(camel) { return ".\(camel)" } - } - // 2) hex 매칭 — 같은 hex의 brand/semantic 변수가 있으면 그쪽으로 묶기 - if alpha >= 1.0, let matched = hexIndex[normalized] { - return ".\(matched)" - } - // 3) fallback: inline hex - return colorBody(hex: normalized, alpha: alpha) - } - return nil -} - -func resolveComponentNumber(_ node: [String: Any]) -> String? { - if let str = node["$value"] as? String { - var s = str - if s.hasPrefix("{"), s.hasSuffix("}") { s = String(s.dropFirst().dropLast()) } - let p = s.split(separator: ".").map(String.init) - if p.count == 2, p[0] == "Radius" { return ".\(swiftKey(p[1]))" } - } - if let n = node["$value"] as? Double { return formatNumber(n) } - return nil +func capitalizeFirst(_ s: String) -> String { + s.prefix(1).uppercased() + s.dropFirst() } -// Component subtree 를 flat path 로 풀어 ShapeStyle / CGFloat 확장에 직접 추가한다. -// Component.button.primary.background.default → buttonPrimaryBackgroundDefault -// Component.button.radius → buttonRadius -func walkComponentFlat( - _ node: [String: Any], - pathPrefix: [String], - colorLines: inout [String], - numberLines: inout [String] -) { - let keys = node.keys.sorted() - let leafKeys = keys.filter { (node[$0] as? [String: Any])?["$type"] != nil } - let groupKeys = keys.filter { (node[$0] as? [String: Any])?["$type"] == nil } - for key in leafKeys { - guard let child = node[key] as? [String: Any], let type = child["$type"] as? String else { continue } - let propName = flatPropertyName(pathPrefix + [key]) - switch type { - case "color": - if let expr = resolveComponentColor(child) { - colorLines.append(" static var \(propName): Color { \(expr) }") - } - case "number": - if let expr = resolveComponentNumber(child) { - numberLines.append(" static let \(propName): CGFloat = \(expr)") - } - default: continue - } - } - for key in groupKeys { - guard let child = node[key] as? [String: Any] else { continue } - walkComponentFlat(child, pathPrefix: pathPrefix + [key], colorLines: &colorLines, numberLines: &numberLines) - } +func lowerFirst(_ s: String) -> String { + s.prefix(1).lowercased() + s.dropFirst() } -// Component subtree 의 nested ComponentToken enum. 값은 flat 정의를 forwarding 하므로 -// source of truth 는 항상 ShapeStyle / CGFloat 확장 한 곳. -// public static var `default`: Color { .buttonPrimaryBackgroundDefault } -func walkComponentNested( - _ node: [String: Any], - pathPrefix: [String], - indent: String, - out: inout [String] -) { - let keys = node.keys.sorted() - let leafKeys = keys.filter { (node[$0] as? [String: Any])?["$type"] != nil } - let groupKeys = keys.filter { (node[$0] as? [String: Any])?["$type"] == nil } - for key in leafKeys { - guard let child = node[key] as? [String: Any], let type = child["$type"] as? String else { continue } - let flatName = flatPropertyName(pathPrefix + [key]) - switch type { - case "color": - out.append("\(indent)public static var \(swiftKey(key)): Color { .\(flatName) }") - case "number": - out.append("\(indent)public static var \(swiftKey(key)): CGFloat { .\(flatName) }") - default: continue - } - } - for (i, key) in groupKeys.enumerated() { - guard let child = node[key] as? [String: Any] else { continue } - if i == 0, !leafKeys.isEmpty { out.append("") } - if i > 0 { out.append("") } - out.append("\(indent)public enum \(capitalizeFirst(key)) {") - walkComponentNested(child, pathPrefix: pathPrefix + [key], indent: indent + " ", out: &out) - out.append("\(indent)}") - } -} +/// `"radius-default"` / `"font-weight-bold"` → `["radius", "default"]` / `["font", "weight", "bold"]`. +/// Also handles known typos like `"spscing-12"` → `["spscing", "12"]` (preserved verbatim). +func splitDashed(_ s: String) -> [String] { s.split(separator: "-").map(String.init) } -// ["button", "primary", "background", "default"] → "buttonPrimaryBackgroundDefault" -func flatPropertyName(_ segs: [String]) -> String { +/// `["gap", "0"]` → `"gap0"`, `["padding", "container", "8"]` → `"paddingContainer8"`. +func camelCase(_ segs: [String]) -> String { guard let first = segs.first else { return "" } - let head = first.prefix(1).lowercased() + first.dropFirst() + let head = lowerFirst(first) let tail = segs.dropFirst().map(capitalizeFirst).joined() return swiftKey(head + tail) } -func capitalizeFirst(_ s: String) -> String { - s.prefix(1).uppercased() + s.dropFirst() -} - func formatNumber(_ d: Double) -> String { if d == d.rounded() { return "\(Int(d))" } let rounded = (d * 100).rounded() / 100 @@ -220,150 +162,271 @@ func writeFile(_ path: String, _ contents: String) throws { let header = """ // AUTO-GENERATED by Tools/TokenGenerator.swift — DO NOT EDIT -// Source: Projects/Shared/DesignSystem/Resources/Mode 1.tokens.json +// Source: Projects/Shared/DesignSystem/Resources/{primitive,semantic,component}.json """ -// MARK: - Colors +// MARK: - Color emission -let colors = json["Colors"] as! [String: Any] -let brand = colors["brand"] as! [String: Any] -let semantic = colors["semantic"] as! [String: Any] -let scales = ["50", "100", "200", "300", "400", "500", "600", "700", "800", "900"] -let brandGroups = ["primary", "secondary", "beige", "neutral"] +/// Tracks emitted color symbols so downstream emissions can prefer aliases over inline hex. +var hexToColorName: [String: String] = [:] + +func emitColor(name: String, value: Any, into lines: inout [String]) { + guard let parts = resolveHex(value) else { return } + if parts.alpha >= 1.0, let existing = hexToColorName[parts.hex] { + lines.append(" static var \(name): Color { .\(existing) }") + } else { + lines.append(" static var \(name): Color { \(colorBody(hex: parts.hex, alpha: parts.alpha)) }") + if parts.alpha >= 1.0 { hexToColorName[parts.hex] = name } + } +} -var lines: [String] = [header, "", "import SwiftUI", "", "public extension ShapeStyle where Self == Color {", ""] +var colorLines: [String] = [header, "", "import SwiftUI", "", "public extension ShapeStyle where Self == Color {", ""] -// brand -for group in brandGroups { - guard let g = brand[group] as? [String: Any] else { continue } - lines.append(" // MARK: - Brand / \(capitalizeFirst(group))") +// Primitive color scales: primary / secondary / beige / gray (50..900) +let primitiveColorGroups = ["primary", "secondary", "beige", "gray"] +let scales = ["50", "100", "200", "300", "400", "500", "600", "700", "800", "900"] +for group in primitiveColorGroups { + guard let g = primitive[group] as? [String: Any] else { continue } + colorLines.append(" // MARK: - Primitive / \(capitalizeFirst(group))") for s in scales { - if let node = g[s] as? [String: Any], let v = valueOf(node), let h = hexAlpha(v) { - let name = "\(group)\(s)" - lines.append(" static var \(name): Color { \(colorBody(hex: h.hex, alpha: h.alpha)) }") - knownColorNames.insert(name) - if h.alpha >= 1.0 { hexIndex[h.hex] = name } - } + guard let node = g[s] as? [String: Any], let v = node["$value"] else { continue } + emitColor(name: "\(group)\(s)", value: v, into: &colorLines) + } + colorLines.append("") +} + +// Primitive status colors (error / warning, including alpha variants — typo "slpha" normalized). +colorLines.append(" // MARK: - Primitive / Status") +for bucket in ["error", "warning"] { + guard let b = primitive[bucket] as? [String: Any] else { continue } + for (rawKey, value) in b.sorted(by: { $0.key < $1.key }) { + guard let node = value as? [String: Any], let v = node["$value"] else { continue } + let key = (rawKey == "slpha") ? "alpha" : rawKey + emitColor(name: camelCase([bucket, key]), value: v, into: &colorLines) } - if let alpha = g["Alpha"] as? [String: Any] { - for (k, v) in alpha.sorted(by: { $0.key < $1.key }) { - if let node = v as? [String: Any], let val = valueOf(node), let h = hexAlpha(val) { - let name = "\(group)Alpha\(k)" - lines.append(" static var \(name): Color { \(colorBody(hex: h.hex, alpha: h.alpha)) }") - knownColorNames.insert(name) +} + +colorLines.append("") + +// Semantic colors — flat names walked from the tree. +func walkColorLeaves( + _ node: [String: Any], + path: [String], + emit: (_ flatName: String, _ value: Any) -> Void +) { + for (key, value) in node.sorted(by: { $0.key < $1.key }) { + guard let dict = value as? [String: Any] else { continue } + if let type = dict["$type"] as? String, let val = dict["$value"] { + if type == "color" { + emit(camelCase(path + [key]), val) } + } else { + walkColorLeaves(dict, path: path + [key], emit: emit) } } - lines.append("") } -// semantic prefixed groups -let semGroups: [(jsonKey: String, swiftPrefix: String)] = [ +let semanticColorRoots: [(jsonKey: String, prefix: String)] = [ + ("background", "bg"), ("text", "text"), ("border", "border"), ("surface", "surface"), - ("background", "bg"), + ("action", "action"), + ("icon", "icon"), ] -for (key, prefix) in semGroups { - guard let group = semantic[key] as? [String: Any] else { continue } - lines.append(" // MARK: - Semantic / \(capitalizeFirst(key))") - for (rawName, val) in group.sorted(by: { $0.key < $1.key }) { - guard let node = val as? [String: Any], let v = valueOf(node) else { continue } - let name = "\(prefix)\(capitalizeFirst(rawName))" - if let h = hexAlpha(v) { - lines.append(" static var \(name): Color { \(colorBody(hex: h.hex, alpha: h.alpha)) }") - if h.alpha >= 1.0 { hexIndex[h.hex] = name } - } else if let aliasStr = v as? String, let target = aliasToSwiftName(aliasStr) { - lines.append(" static var \(name): Color { .\(target) }") - } - knownColorNames.insert(name) +for (root, prefix) in semanticColorRoots { + guard let group = semantic[root] as? [String: Any] else { continue } + colorLines.append(" // MARK: - Semantic / \(capitalizeFirst(root))") + walkColorLeaves(group, path: [prefix]) { name, val in + emitColor(name: name, value: val, into: &colorLines) } - lines.append("") + colorLines.append("") } -// status nested (status.error.error / status.error.Alpha / status.warning.warning / status.warning.Alpha) -if let status = semantic["status"] as? [String: Any] { - lines.append(" // MARK: - Semantic / Status") - for bucket in ["error", "warning"] { - guard let b = status[bucket] as? [String: Any] else { continue } - for (k, v) in b.sorted(by: { $0.key < $1.key }) { - guard let node = v as? [String: Any], let val = valueOf(node), let h = hexAlpha(val) else { continue } - let bucketCap = capitalizeFirst(bucket) - let name = (k == "Alpha") ? "status\(bucketCap)Alpha" : "status\(bucketCap)" - lines.append(" static var \(name): Color { \(colorBody(hex: h.hex, alpha: h.alpha)) }") - knownColorNames.insert(name) - if h.alpha >= 1.0 { hexIndex[h.hex] = name } +// Component colors — flat. +colorLines.append(" // MARK: - Component") +var componentNumberEntries: [(name: String, value: String)] = [] + +func walkComponent(_ node: [String: Any], path: [String]) { + for (key, value) in node.sorted(by: { $0.key < $1.key }) { + guard let dict = value as? [String: Any] else { continue } + if let type = dict["$type"] as? String, let val = dict["$value"] { + let name = camelCase(path + [key]) + switch type { + case "color": + emitColor(name: name, value: val, into: &colorLines) + case "sizing", "spacing", "borderRadius", "borderWidth", "number": + if let n = resolveNumber(val) { + componentNumberEntries.append((name, formatNumber(n))) + } + default: + break + } + } else { + walkComponent(dict, path: path + [key]) } } } -// Component colors — flat ShapeStyle 확장에 직접 합쳐 ComponentToken 중첩 enum 을 폐기. -let component = json["Component"] as! [String: Any] -var componentColorLines: [String] = [] -var componentNumberLines: [String] = [] -walkComponentFlat(component, pathPrefix: [], colorLines: &componentColorLines, numberLines: &componentNumberLines) -if !componentColorLines.isEmpty { - lines.append(" // MARK: - Component") - lines.append(contentsOf: componentColorLines) - lines.append("") +walkComponent(component, path: []) + +colorLines.append("}") +colorLines.append("") +try writeFile(colorOut, colorLines.joined(separator: "\n")) + +// MARK: - Spacing (primitive + semantic gap/padding/border-width) + +var spacingLines: [String] = [header, "", "import CoreGraphics", "", "public extension CGFloat {", ""] + +// Primitive spacing-N (note: source has typo "spscing-12" — emit canonical s12 from its resolved value). +spacingLines.append(" // MARK: - Primitive Spacing") +var spacingPairs: [(value: Int, line: String)] = [] +for (key, value) in primitive { + guard key.hasPrefix("spacing-") || key.hasPrefix("spscing-") else { continue } + guard let node = value as? [String: Any], + let v = node["$value"], + let n = resolveNumber(v) else { continue } + let suffix = key.replacingOccurrences(of: "spacing-", with: "").replacingOccurrences(of: "spscing-", with: "") + guard let intVal = Int(suffix) else { continue } + spacingPairs.append((intVal, " static let s\(intVal): CGFloat = \(formatNumber(n))")) } -lines.append("}") -lines.append("") -try writeFile(colorOut, lines.joined(separator: "\n")) +for (_, line) in spacingPairs.sorted(by: { $0.value < $1.value }) { + spacingLines.append(line) +} -// MARK: - Radius +spacingLines.append("") -let radius = json["Radius"] as! [String: Any] -let radiusOrder = ["none", "default", "full"] -var rLines: [String] = [header, "", "import CoreGraphics", "", "public extension CGFloat {", ""] -rLines.append(" // MARK: - Radius") -for k in radiusOrder { - guard let node = radius[k] as? [String: Any], let v = valueOf(node), let n = v as? Double else { continue } - let safe = (k == "default") ? "`default`" : k - rLines.append(" static let \(safe): CGFloat = \(formatNumber(n))") +// Primitive sizing (icon / avatar / control). +spacingLines.append(" // MARK: - Primitive Sizing") +for group in ["icon", "avatar", "control"] { + guard let g = primitive[group] as? [String: Any] else { continue } + for (sizeKey, value) in g.sorted(by: { $0.key < $1.key }) { + guard let node = value as? [String: Any], let v = node["$value"], let n = resolveNumber(v) else { continue } + spacingLines.append(" static let \(camelCase([group, sizeKey])): CGFloat = \(formatNumber(n))") + } } -rLines.append("}") -rLines.append("") -try writeFile(radiusOut, rLines.joined(separator: "\n")) +spacingLines.append("") + +// Semantic gap. +if let gap = semantic["gap"] as? [String: Any] { + spacingLines.append(" // MARK: - Semantic Gap") + var gapPairs: [(Int, String)] = [] + for (key, value) in gap { + guard let node = value as? [String: Any], let v = node["$value"], let n = resolveNumber(v), + let intKey = Int(key) else { continue } + gapPairs.append((intKey, " static let gap\(intKey): CGFloat = \(formatNumber(n))")) + } + for (_, line) in gapPairs.sorted(by: { $0.0 < $1.0 }) { + spacingLines.append(line) + } + spacingLines.append("") +} -// MARK: - Spacing +// Semantic padding. +if let padding = semantic["padding"] as? [String: Any] { + spacingLines.append(" // MARK: - Semantic Padding") + var paddingLines: [String] = [] + walkColorLeaves(padding, path: ["padding"]) { _, _ in } // unused, just to keep walker available + func walkPadding(_ node: [String: Any], path: [String]) { + for (key, value) in node.sorted(by: { $0.key < $1.key }) { + guard let dict = value as? [String: Any] else { continue } + if let type = dict["$type"] as? String, let val = dict["$value"] { + if type == "spacing", let n = resolveNumber(val) { + paddingLines.append(" static let \(camelCase(path + [key])): CGFloat = \(formatNumber(n))") + } + } else { + walkPadding(dict, path: path + [key]) + } + } + } + walkPadding(padding, path: ["padding"]) + spacingLines.append(contentsOf: paddingLines) + spacingLines.append("") +} -let spacing = json["Spacing"] as! [String: Any] -let spacingKeys = spacing.keys.compactMap(Int.init).sorted() -var sLines: [String] = [header, "", "import CoreGraphics", "", "public extension CGFloat {", ""] -sLines.append(" // MARK: - Spacing") -for k in spacingKeys { - guard let node = spacing[String(k)] as? [String: Any], let v = valueOf(node), let n = v as? Double else { continue } - sLines.append(" static let s\(k): CGFloat = \(formatNumber(n))") +// Semantic border-width-{regular,medium,large}. +spacingLines.append(" // MARK: - Semantic Border Width") +for variant in ["regular", "medium", "large"] { + let key = "border-width-\(variant)" + guard let node = semantic[key] as? [String: Any], let v = node["$value"], let n = resolveNumber(v) else { continue } + spacingLines.append(" static let \(camelCase(["border", "width", variant])): CGFloat = \(formatNumber(n))") } -sLines.append("}") -sLines.append("") -try writeFile(spacingOut, sLines.joined(separator: "\n")) +spacingLines.append("}") +spacingLines.append("") +try writeFile(spacingOut, spacingLines.joined(separator: "\n")) + +// MARK: - Radius + +var radiusLines: [String] = [header, "", "import CoreGraphics", "", "public extension CGFloat {", ""] +radiusLines.append(" // MARK: - Radius") +for key in ["radius-default", "radius-max"] { + guard let node = semantic[key] as? [String: Any], let v = node["$value"], let n = resolveNumber(v) else { continue } + let suffix = key.replacingOccurrences(of: "radius-", with: "") + radiusLines.append(" static let \(camelCase(["radius", suffix])): CGFloat = \(formatNumber(n))") +} -// MARK: - Component (numbers) +radiusLines.append("}") +radiusLines.append("") +try writeFile(radiusOut, radiusLines.joined(separator: "\n")) -// 색상은 위에서 ShapeStyle+.swift 에 이미 추가됨. 숫자만 CGFloat 확장으로 별도 출력. +// MARK: - Component numerics (flat) -if !componentNumberLines.isEmpty { - var cLines: [String] = [header, "", "import CoreGraphics", "", "public extension CGFloat {", ""] - cLines.append(" // MARK: - Component") - cLines.append(contentsOf: componentNumberLines) - cLines.append("}") - cLines.append("") - try writeFile(componentNumberOut, cLines.joined(separator: "\n")) +if !componentNumberEntries.isEmpty { + var cnLines: [String] = [header, "", "import CoreGraphics", "", "public extension CGFloat {", ""] + cnLines.append(" // MARK: - Component") + for entry in componentNumberEntries.sorted(by: { $0.name < $1.name }) { + cnLines.append(" static let \(entry.name): CGFloat = \(entry.value)") + } + cnLines.append("}") + cnLines.append("") + try writeFile(componentNumberOut, cnLines.joined(separator: "\n")) } -// MARK: - Component (nested ComponentToken) +// MARK: - ComponentToken (nested forwarding enum) + +func walkComponentNested( + _ node: [String: Any], + path: [String], + indent: String, + out: inout [String] +) { + let leafKeys = node.keys.sorted().filter { + guard let d = node[$0] as? [String: Any] else { return false } + return d["$type"] != nil + } + let groupKeys = node.keys.sorted().filter { + guard let d = node[$0] as? [String: Any] else { return false } + return d["$type"] == nil + } + for key in leafKeys { + guard let child = node[key] as? [String: Any], let type = child["$type"] as? String else { continue } + let flat = camelCase(path + [key]) + switch type { + case "color": + out.append("\(indent)public static var \(swiftKey(key)): Color { .\(flat) }") + case "sizing", "spacing", "borderRadius", "borderWidth", "number": + out.append("\(indent)public static var \(swiftKey(key)): CGFloat { .\(flat) }") + default: + continue + } + } + for (i, key) in groupKeys.enumerated() { + guard let child = node[key] as? [String: Any] else { continue } + if i == 0, !leafKeys.isEmpty { out.append("") } + if i > 0 { out.append("") } + out.append("\(indent)public enum \(capitalizeFirst(key)) {") + walkComponentNested(child, path: path + [key], indent: indent + " ", out: &out) + out.append("\(indent)}") + } +} -// flat ShapeStyle / CGFloat 확장을 forwarding 하는 구조적 접근용 enum. -// 그룹 단위 캡처/자동완성 탐색에 사용. 값의 source of truth 는 flat 정의 한 곳. var ctLines: [String] = [header, "", "import SwiftUI", "", "public enum ComponentToken {"] -walkComponentNested(component, pathPrefix: [], indent: " ", out: &ctLines) +walkComponentNested(component, path: [], indent: " ", out: &ctLines) ctLines.append("}") ctLines.append("") -try writeFile(componentOut, ctLines.joined(separator: "\n")) +try writeFile(componentTokenOut, ctLines.joined(separator: "\n")) print("[token-gen] done.") From 1ad7f09e7de19a98ad71b942908d83771babebb3 Mon Sep 17 00:00:00 2001 From: Roy Date: Tue, 19 May 2026 22:48:08 +0900 Subject: [PATCH 15/21] =?UTF-8?q?feat:=20#4=20Poll=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=ED=92=80=EC=B2=B4=EC=9D=B8=20+=20=EC=82=AC?= =?UTF-8?q?=EC=A0=84=20=ED=88=AC=ED=91=9C=EC=B0=BD=20API=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99=20+=20Skeleton=20Loader?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Poll 도메인 추가 — Entity (PollDetail · PollOption · PollStatus + percentage helper), DTO+Mapper (yyyy-MM-dd · displayOrder 정렬), PollInterface · DefaultPollRepositoryImpl, PollRepositoryImpl (MoyaProvider.authorized 패턴) - API 도메인 PieckeDomain 에 case poll 추가 + AppDIManager 에 PollRepositoryImpl 등록 - PreVoteFeature: State 에 poll · isLoading · pollId 추가, Result + mapError 단일 Response 패턴으로 fetchPoll 비동기 액션 구성 - PreVoteView: onAppear 시 fetchPoll, 로딩 중에는 PreVoteSkeletonView 분기 표시 - PreVoteSkeletonView 신규 — .pen `사전 투표창 - Skeleton Loader` (nTffe) 의 10개 Rectangle 절대 좌표 1:1 매핑, 375pt 디자인 기준 GeometryReader 로 비례 scale --- Projects/App/Sources/Di/DiRegister.swift | 1 + .../Data/API/Sources/Base/PieckeDomain.swift | 3 + Projects/Data/API/Sources/Poll/PollAPI.swift | 19 ++++ .../Model/Sources/Poll/DTO/PollDataDTO.swift | 29 ++++++ .../Sources/Poll/Mapper/PollDataDTO+.swift | 43 ++++++++ .../Sources/Poll/PollRepositoryImpl.swift | 38 ++++++++ .../Service/Sources/Poll/PollService.swift | 45 +++++++++ .../Poll/DefaultPollRepositoryImpl.swift | 15 +++ .../Sources/Poll/PollInterface.swift | 31 ++++++ .../Entity/Sources/Poll/PollDetail.swift | 97 +++++++++++++++++++ .../Sources/Vote/Reducer/PreVoteFeature.swift | 65 +++++++++++-- .../Home/Sources/Vote/View/PreVoteView.swift | 32 ++++-- 12 files changed, 400 insertions(+), 18 deletions(-) create mode 100644 Projects/Data/API/Sources/Poll/PollAPI.swift create mode 100644 Projects/Data/Model/Sources/Poll/DTO/PollDataDTO.swift create mode 100644 Projects/Data/Model/Sources/Poll/Mapper/PollDataDTO+.swift create mode 100644 Projects/Data/Repository/Sources/Poll/PollRepositoryImpl.swift create mode 100644 Projects/Data/Service/Sources/Poll/PollService.swift create mode 100644 Projects/Domain/DomainInterface/Sources/Poll/DefaultPollRepositoryImpl.swift create mode 100644 Projects/Domain/DomainInterface/Sources/Poll/PollInterface.swift create mode 100644 Projects/Domain/Entity/Sources/Poll/PollDetail.swift diff --git a/Projects/App/Sources/Di/DiRegister.swift b/Projects/App/Sources/Di/DiRegister.swift index d47613f..0576dda 100644 --- a/Projects/App/Sources/Di/DiRegister.swift +++ b/Projects/App/Sources/Di/DiRegister.swift @@ -36,6 +36,7 @@ public final class AppDIManager: Sendable { // 🏗️ Repository 계층 (Clean Architecture + PFW) .register { AuthRepositoryImpl() as AuthInterface } .register { HomeRepositoryImpl() as HomeInterface } + .register { PollRepositoryImpl() as PollInterface } // .register { ProfileRepositoryImpl() as ProfileInterface } // .register { AppUpdateRepositoryImpl() as AppUpdateInterface } diff --git a/Projects/Data/API/Sources/Base/PieckeDomain.swift b/Projects/Data/API/Sources/Base/PieckeDomain.swift index 74a9302..5d26192 100644 --- a/Projects/Data/API/Sources/Base/PieckeDomain.swift +++ b/Projects/Data/API/Sources/Base/PieckeDomain.swift @@ -13,6 +13,7 @@ public enum PieckeDomain { case auth case profile case home + case poll } extension PieckeDomain: DomainType { @@ -28,6 +29,8 @@ extension PieckeDomain: DomainType { return"api/v1/me/" case .home: return"api/v1/home" + case .poll: + return"api/v1/poll" } } } diff --git a/Projects/Data/API/Sources/Poll/PollAPI.swift b/Projects/Data/API/Sources/Poll/PollAPI.swift new file mode 100644 index 0000000..1ae62d7 --- /dev/null +++ b/Projects/Data/API/Sources/Poll/PollAPI.swift @@ -0,0 +1,19 @@ +// +// PollAPI.swift +// API +// +// Created by Wonji Suh on 5/19/26. +// + +import Foundation + +public enum PollAPI { + case detailPoll(pollId: Int) + + public var description: String { + switch self { + case .detailPoll(let pollId): + return "/\(pollId)" + } + } +} diff --git a/Projects/Data/Model/Sources/Poll/DTO/PollDataDTO.swift b/Projects/Data/Model/Sources/Poll/DTO/PollDataDTO.swift new file mode 100644 index 0000000..cb55823 --- /dev/null +++ b/Projects/Data/Model/Sources/Poll/DTO/PollDataDTO.swift @@ -0,0 +1,29 @@ +// +// PollDataDTO.swift +// Model +// +// `GET /api/v1/polls/{pollId}` 의 data 페이로드. +// + +import Foundation + +public struct PollDataDTO: Decodable { + public let pollId: Int + public let titlePrefix: String + public let titleSuffix: String + public let targetDate: String? + public let status: String + public let options: [PollOptionDTO] +} + +public struct PollOptionDTO: Decodable, Identifiable { + public let optionId: Int + public let label: String + public let title: String + public let displayOrder: Int + public let voteCount: Int + + public var id: Int { optionId } +} + +public typealias PollResponseDTO = BaseResponseDTO diff --git a/Projects/Data/Model/Sources/Poll/Mapper/PollDataDTO+.swift b/Projects/Data/Model/Sources/Poll/Mapper/PollDataDTO+.swift new file mode 100644 index 0000000..a4c1e0f --- /dev/null +++ b/Projects/Data/Model/Sources/Poll/Mapper/PollDataDTO+.swift @@ -0,0 +1,43 @@ +// +// PollDataDTO+.swift +// Model +// + +import Entity +import Foundation + +private let pollDateFormatter: DateFormatter = { + let f = DateFormatter() + f.calendar = Calendar(identifier: .gregorian) + f.locale = Locale(identifier: "en_US_POSIX") + f.timeZone = TimeZone(secondsFromGMT: 0) + f.dateFormat = "yyyy-MM-dd" + return f +}() + +public extension PollOptionDTO { + func toDomain() -> PollOption { + PollOption( + optionId: optionId, + label: label, + title: title, + displayOrder: displayOrder, + voteCount: voteCount + ) + } +} + +public extension PollDataDTO { + func toDomain() -> PollDetail { + PollDetail( + pollId: pollId, + titlePrefix: titlePrefix, + titleSuffix: titleSuffix, + targetDate: targetDate.flatMap { pollDateFormatter.date(from: $0) }, + status: PollStatus(rawValue: status), + options: options + .sorted { $0.displayOrder < $1.displayOrder } + .map { $0.toDomain() } + ) + } +} diff --git a/Projects/Data/Repository/Sources/Poll/PollRepositoryImpl.swift b/Projects/Data/Repository/Sources/Poll/PollRepositoryImpl.swift new file mode 100644 index 0000000..d05597a --- /dev/null +++ b/Projects/Data/Repository/Sources/Poll/PollRepositoryImpl.swift @@ -0,0 +1,38 @@ +// +// PollRepositoryImpl.swift +// Repository +// + +import Foundation + +import DomainInterface +import Entity +import Model +import Service + +import LogMacro +import Moya + +@preconcurrency import AsyncMoya + +public final class PollRepositoryImpl: PollInterface, @unchecked Sendable { + private let provider: MoyaProvider + + public init( + provider: MoyaProvider = MoyaProvider.authorized + ) { + self.provider = provider + } + + public func fetchPoll(pollId: Int) async throws -> PollDetail { + let dto: PollResponseDTO = try await provider.request(.detailPoll(pollId: pollId)) + + guard let data = dto.data else { + let message = dto.error?.message ?? "투표 데이터 응답이 비어 있습니다" + Log.error("[PollRepositoryImpl] empty poll payload: \(message)") + throw AuthError.backendError(message) + } + + return data.toDomain() + } +} diff --git a/Projects/Data/Service/Sources/Poll/PollService.swift b/Projects/Data/Service/Sources/Poll/PollService.swift new file mode 100644 index 0000000..d77d8f0 --- /dev/null +++ b/Projects/Data/Service/Sources/Poll/PollService.swift @@ -0,0 +1,45 @@ +// +// PollService.swift +// Service +// +// Created by Wonji Suh on 5/19/26. +// + +import Foundation + +import API +import Foundations + +import AsyncMoya + +public enum PollService { + case detailPoll(pollId: Int) +} + +extension PollService: BaseTargetType { + public typealias Domain = PieckeDomain + + public var domain: PieckeDomain { .poll } + + public var urlPath: String { + switch self { + case let .detailPoll(pollId): + return PollAPI.detailPoll(pollId: pollId).description + } + } + + public var error: [Int: AsyncMoya.NetworkError]? { nil } + + public var method: Moya.Method { + switch self { + case .detailPoll: + .get + } + } + + public var parameters: [String: Any]? { nil } + + public var headers: [String: String]? { + APIHeader.baseHeader + } +} diff --git a/Projects/Domain/DomainInterface/Sources/Poll/DefaultPollRepositoryImpl.swift b/Projects/Domain/DomainInterface/Sources/Poll/DefaultPollRepositoryImpl.swift new file mode 100644 index 0000000..b0034fd --- /dev/null +++ b/Projects/Domain/DomainInterface/Sources/Poll/DefaultPollRepositoryImpl.swift @@ -0,0 +1,15 @@ +// +// DefaultPollRepositoryImpl.swift +// DomainInterface +// + +import Entity +import Foundation + +public struct DefaultPollRepositoryImpl: PollInterface { + public init() {} + + public func fetchPoll(pollId _: Int) async throws -> PollDetail { + .mock + } +} diff --git a/Projects/Domain/DomainInterface/Sources/Poll/PollInterface.swift b/Projects/Domain/DomainInterface/Sources/Poll/PollInterface.swift new file mode 100644 index 0000000..81df867 --- /dev/null +++ b/Projects/Domain/DomainInterface/Sources/Poll/PollInterface.swift @@ -0,0 +1,31 @@ +// +// PollInterface.swift +// DomainInterface +// + +import Entity +import Foundation +import WeaveDI + +public protocol PollInterface: Sendable { + func fetchPoll(pollId: Int) async throws -> PollDetail +} + +public struct PollRepositoryDependency: DependencyKey { + public static var liveValue: PollInterface { + UnifiedDI.resolve(PollInterface.self) ?? DefaultPollRepositoryImpl() + } + + public static var testValue: PollInterface { + UnifiedDI.resolve(PollInterface.self) ?? DefaultPollRepositoryImpl() + } + + public static var previewValue: PollInterface = liveValue +} + +public extension DependencyValues { + var pollRepository: PollInterface { + get { self[PollRepositoryDependency.self] } + set { self[PollRepositoryDependency.self] = newValue } + } +} diff --git a/Projects/Domain/Entity/Sources/Poll/PollDetail.swift b/Projects/Domain/Entity/Sources/Poll/PollDetail.swift new file mode 100644 index 0000000..cb81e59 --- /dev/null +++ b/Projects/Domain/Entity/Sources/Poll/PollDetail.swift @@ -0,0 +1,97 @@ +// +// PollDetail.swift +// Entity +// +// `GET /api/v1/polls/{pollId}` 응답에서 사용하는 도메인 모델. +// + +import Foundation + +public struct PollDetail: Equatable, Identifiable { + public let pollId: Int + public let titlePrefix: String + public let titleSuffix: String + public let targetDate: Date? + public let status: PollStatus + public let options: [PollOption] + + public var id: Int { pollId } + + public init( + pollId: Int, + titlePrefix: String, + titleSuffix: String, + targetDate: Date?, + status: PollStatus, + options: [PollOption] + ) { + self.pollId = pollId + self.titlePrefix = titlePrefix + self.titleSuffix = titleSuffix + self.targetDate = targetDate + self.status = status + self.options = options + } +} + +public struct PollOption: Equatable, Identifiable, Hashable { + public let optionId: Int + public let label: String + public let title: String + public let displayOrder: Int + public let voteCount: Int + + public var id: Int { optionId } + + public init( + optionId: Int, + label: String, + title: String, + displayOrder: Int, + voteCount: Int + ) { + self.optionId = optionId + self.label = label + self.title = title + self.displayOrder = displayOrder + self.voteCount = voteCount + } +} + +public enum PollStatus: String, Equatable, Hashable, CaseIterable { + case pending = "PENDING" + case active = "ACTIVE" + case closed = "CLOSED" + case unknown + + public init(rawValue: String) { + self = PollStatus.allCases.first { $0.rawValue == rawValue } ?? .unknown + } +} + +public extension PollDetail { + /// 전체 참여 수 = options.voteCount 의 합 + var totalVoteCount: Int { options.reduce(0) { $0 + $1.voteCount } } + + /// 옵션별 비율 (0~100). totalVoteCount 가 0 이면 모두 0. + func percentage(for option: PollOption) -> Int { + guard totalVoteCount > 0 else { return 0 } + return Int((Double(option.voteCount) / Double(totalVoteCount)) * 100) + } +} + +public extension PollDetail { + static let mock = PollDetail( + pollId: 1, + titlePrefix: "도덕의 기준은", + titleSuffix: "이다", + targetDate: nil, + status: .active, + options: [ + .init(optionId: 1, label: "A", title: "결과", displayOrder: 1, voteCount: 45), + .init(optionId: 2, label: "B", title: "의도", displayOrder: 2, voteCount: 25), + .init(optionId: 3, label: "C", title: "규칙", displayOrder: 3, voteCount: 20), + .init(optionId: 4, label: "D", title: "덕", displayOrder: 4, voteCount: 10), + ] + ) +} diff --git a/Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift b/Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift index 489ccd3..265b0a3 100644 --- a/Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift +++ b/Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift @@ -8,7 +8,9 @@ import Foundation import ComposableArchitecture +import DomainInterface import Entity +import LogMacro @Reducer public struct PreVoteFeature { @@ -17,9 +19,12 @@ public struct PreVoteFeature { @ObservableState public struct State: Equatable { public var battle: PreVoteBattle = .mock + public var poll: PollDetail? public var selectedSide: PhilosopherAvatar? + public var isLoading: Bool = false public var isSubmitting: Bool = false public var shareItem: ShareItem? + public var pollId: Int = 1 public var isPrimaryButtonEnabled: Bool { selectedSide != nil && !isSubmitting @@ -49,20 +54,32 @@ public struct PreVoteFeature { @CasePathable public enum View { + case onAppear case backButtonTapped case shareTapped case optionTapped(PhilosopherAvatar) case primaryButtonTapped } - public enum AsyncAction: Equatable {} - public enum InnerAction: Equatable {} + public enum AsyncAction: Equatable { + case fetchPoll + } + + public enum InnerAction: Equatable { + case pollResponse(Result) + } public enum DelegateAction: Equatable { case dismiss - case submit(battleId: Int, side: PhilosopherAvatar) + case submit(pollId: Int, side: PhilosopherAvatar) + } + + nonisolated enum CancelID: Hashable { + case fetchPoll } + @Dependency(\.pollRepository) private var pollRepository + public var body: some Reducer { BindingReducer() Reduce { state, action in @@ -92,14 +109,20 @@ extension PreVoteFeature { action: View ) -> Effect { switch action { + case .onAppear: + guard state.poll == nil, !state.isLoading else { return .none } + return .send(.async(.fetchPoll)) + case .backButtonTapped: return .send(.delegate(.dismiss)) case .shareTapped: + let title = state.poll.map { "\($0.titlePrefix) \($0.titleSuffix)" } + ?? "\(state.battle.titleLine1) \(state.battle.titleLine2)" state.shareItem = ShareItem( items: [ - "\(state.battle.titleLine1) \(state.battle.titleLine2)", - "https://picke.store/battle/\(state.battle.battleId)", + title, + "https://picke.store/poll/\(state.pollId)", ] ) return .none @@ -111,22 +134,44 @@ extension PreVoteFeature { case .primaryButtonTapped: guard let side = state.selectedSide else { return .none } state.isSubmitting = true - return .send(.delegate(.submit(battleId: state.battle.battleId, side: side))) + return .send(.delegate(.submit(pollId: state.pollId, side: side))) } } private func handleAsyncAction( - state _: inout State, + state: inout State, action: AsyncAction ) -> Effect { - switch action {} + switch action { + case .fetchPoll: + state.isLoading = true + let pollId = state.pollId + return .run { [repository = pollRepository] send in + let result = await Result { + try await repository.fetchPoll(pollId: pollId) + } + .mapError(AuthError.from) + return await send(.inner(.pollResponse(result))) + } + .cancellable(id: CancelID.fetchPoll, cancelInFlight: true) + } } private func handleInnerAction( - state _: inout State, + state: inout State, action: InnerAction ) -> Effect { - switch action {} + switch action { + case let .pollResponse(result): + state.isLoading = false + switch result { + case let .success(poll): + state.poll = poll + case let .failure(error): + Log.error("[PreVoteFeature] fetchPoll failed: \(error.localizedDescription)") + } + return .none + } } private func handleDelegateAction( diff --git a/Projects/Presentation/Home/Sources/Vote/View/PreVoteView.swift b/Projects/Presentation/Home/Sources/Vote/View/PreVoteView.swift index 1df5b7f..148bd98 100644 --- a/Projects/Presentation/Home/Sources/Vote/View/PreVoteView.swift +++ b/Projects/Presentation/Home/Sources/Vote/View/PreVoteView.swift @@ -21,25 +21,41 @@ public struct PreVoteView: View { } public var body: some View { - ZStack(alignment: .top) { - backgroundImage - - VStack(spacing: 0) { - navigationBar - Spacer(minLength: 0) - contentArea + Group { + if shouldShowSkeleton { + PreVoteSkeletonView() + } else { + loadedContent } } .background(Color.beige50.ignoresSafeArea()) .navigationBarHidden(true) .toolbar(.hidden, for: .navigationBar) .toolbar(.hidden, for: .tabBar) + .onAppear { send(.onAppear) } .sheet(item: $store.shareItem) { item in ShareSheet(items: item.items) .presentationDetents([.fraction(0.6)]) .toolbar(.hidden, for: .navigationBar) } } + + private var shouldShowSkeleton: Bool { + store.isLoading && store.poll == nil + } + + @ViewBuilder + private var loadedContent: some View { + ZStack(alignment: .top) { + backgroundImage + + VStack(spacing: 0) { + navigationBar + Spacer(minLength: 0) + contentArea + } + } + } } // MARK: - Background @@ -52,7 +68,7 @@ extension PreVoteView { let url = URL(string: urlString) { KFImage(url) - .placeholder { Color.neutral200 } + .placeholder { SkeletonView() } .resizable() .scaledToFill() } else { From a60f267d3f96c3af57f78cde4853fb76e0b3505d Mon Sep 17 00:00:00 2001 From: Roy Date: Tue, 19 May 2026 22:48:22 +0900 Subject: [PATCH 16/21] =?UTF-8?q?feat:=20#18=20=EA=B3=B5=EC=9A=A9=20Skelet?= =?UTF-8?q?onView=20+=20KFImage=20=EB=A1=9C=EB=94=A9=20placeholder=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DesignSystem 에 SkeletonView 신규 — beige600 base + 좌→우 shimmer 그라데이션 1.2s 무한 반복 (cornerRadius 옵션) - HeroCarouselView · HotBattleCardView · NewBattleCardView 의 KFImage 에 .placeholder { SkeletonView() } 적용해 이미지 로딩 중 shimmer 표시 - Kingfisher 의 기본 캐싱(memory + disk) 정책은 그대로 사용 --- .../View/Components/HeroCarouselView.swift | 4 +- .../View/Components/HotBattleCardView.swift | 1 + .../View/Components/NewBattleCardView.swift | 1 + .../Sources/UI/Skeleton/SkeletonView.swift | 46 +++++++++++++++++++ 4 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 Projects/Shared/DesignSystem/Sources/UI/Skeleton/SkeletonView.swift diff --git a/Projects/Presentation/Home/Sources/Main/View/Components/HeroCarouselView.swift b/Projects/Presentation/Home/Sources/Main/View/Components/HeroCarouselView.swift index 6e427f5..4ad31fd 100644 --- a/Projects/Presentation/Home/Sources/Main/View/Components/HeroCarouselView.swift +++ b/Projects/Presentation/Home/Sources/Main/View/Components/HeroCarouselView.swift @@ -38,7 +38,8 @@ struct HeroCarouselView: View { } } .tabViewStyle(.page(indexDisplayMode: .never)) - .frame(height: Self.controlHeight + Self.thumbnailHeight + Self.subjectHeight) // .pen 합: control(53) + thumbnail(167) + subject(121) + .frame(height: Self.controlHeight + Self.thumbnailHeight + Self + .subjectHeight) // .pen 합: control(53) + thumbnail(167) + subject(121) .background(Color.neutral800) .onReceive(timer) { _ in advance() } } @@ -106,6 +107,7 @@ struct HeroCardView: View { if let url = hero.thumbnailURL { KFImage(url) + .placeholder { SkeletonView() } .resizable() .aspectRatio(contentMode: .fill) .frame( diff --git a/Projects/Presentation/Home/Sources/Main/View/Components/HotBattleCardView.swift b/Projects/Presentation/Home/Sources/Main/View/Components/HotBattleCardView.swift index 610abcc..30c78ec 100644 --- a/Projects/Presentation/Home/Sources/Main/View/Components/HotBattleCardView.swift +++ b/Projects/Presentation/Home/Sources/Main/View/Components/HotBattleCardView.swift @@ -47,6 +47,7 @@ struct HotBattleCardView: View { private var thumbnail: some View { if let url = battle.thumbnailURL { KFImage(url) + .placeholder { SkeletonView() } .resizable() .scaledToFill() .frame(width: 196, height: 124) diff --git a/Projects/Presentation/Home/Sources/Main/View/Components/NewBattleCardView.swift b/Projects/Presentation/Home/Sources/Main/View/Components/NewBattleCardView.swift index e1cf34b..9c48c2d 100644 --- a/Projects/Presentation/Home/Sources/Main/View/Components/NewBattleCardView.swift +++ b/Projects/Presentation/Home/Sources/Main/View/Components/NewBattleCardView.swift @@ -142,6 +142,7 @@ extension NewBattleCardView { .frame(width: 40, height: 40) if let imageURL { KFImage(imageURL) + .placeholder { SkeletonView(cornerRadius: 20) } .resizable() .scaledToFit() .frame(width: 20, height: 38) diff --git a/Projects/Shared/DesignSystem/Sources/UI/Skeleton/SkeletonView.swift b/Projects/Shared/DesignSystem/Sources/UI/Skeleton/SkeletonView.swift new file mode 100644 index 0000000..55cdc86 --- /dev/null +++ b/Projects/Shared/DesignSystem/Sources/UI/Skeleton/SkeletonView.swift @@ -0,0 +1,46 @@ +// +// SkeletonView.swift +// DesignSystem +// +// KFImage 등 비동기 이미지 로딩 placeholder 용 공용 skeleton. +// shimmer 그라데이션을 좌→우로 반복해 로딩 중임을 시각화한다. +// + +import SwiftUI + +public struct SkeletonView: View { + private let cornerRadius: CGFloat + + public init(cornerRadius: CGFloat = 2) { + self.cornerRadius = cornerRadius + } + + @State private var phase: CGFloat = -1 + + private let baseColor = Color(red: 239 / 255, green: 234 / 255, blue: 224 / 255) // beige600 #EFEAE0 + private let shimmerColor = Color(red: 254 / 255, green: 254 / 255, blue: 253 / 255) // beige50 #FEFEFD + + public var body: some View { + RoundedRectangle(cornerRadius: cornerRadius) + .fill(baseColor) + .overlay { + LinearGradient( + stops: [ + .init(color: baseColor.opacity(0), location: 0), + .init(color: shimmerColor.opacity(0.6), location: 0.5), + .init(color: baseColor.opacity(0), location: 1), + ], + startPoint: UnitPoint(x: phase, y: 0.5), + endPoint: UnitPoint(x: phase + 1, y: 0.5) + ) + } + .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) + .onAppear { + withAnimation( + .linear(duration: 1.2).repeatForever(autoreverses: false) + ) { + phase = 2 + } + } + } +} From 016a1cb29dff134ebf6b366c058ce3fddce0c3f3 Mon Sep 17 00:00:00 2001 From: Roy Date: Tue, 19 May 2026 22:48:36 +0900 Subject: [PATCH 17/21] =?UTF-8?q?chore:=20#24=20DesignSystem=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EC=BD=94=EB=93=9C=EC=A0=A0=20=EC=A0=95=EB=A6=AC=20?= =?UTF-8?q?+=20LoginView=20=EB=88=84=EB=9D=BD=20=EC=83=89=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20alias=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Tokens Studio 단일 파일 (`Mode 1.tokens.json`) 적용에 맞춰 다중 분할 (component/primitive/semantic/$metadata) JSON 4개 정리 - ShapeStyle+ / ComponentToken / CGFloat 토큰 코드젠 결과 일괄 갱신 - LoginView: 정의되지 않은 `.gray200` / `.borderGray` → 동일 색의 alias `.neutral200` / `.borderGrayDefault` 로 교체 --- .../Auth/Sources/Main/View/LoginView.swift | 43 +- .../DesignSystem/Resources/$metadata.json | 7 - .../DesignSystem/Resources/component.json | 598 --------------- .../DesignSystem/Resources/primitive.json | 510 ------------ .../DesignSystem/Resources/semantic.json | 725 ------------------ .../Sources/Color/ShapeStyle+.swift | 14 +- .../CGFloat/CGFloat+Component+.swift | 2 +- .../Extension/CGFloat/CGFloat+Radius+.swift | 2 +- .../Extension/CGFloat/CGFloat+Spacing+.swift | 2 +- .../UI/Button/CustomButtonConfig.swift | 2 +- .../Sources/UI/Toast/ToastType.swift | 10 +- .../Sources/UI/Token/ComponentToken.swift | 2 +- Tools/TokenGenerator.swift | 72 +- 13 files changed, 92 insertions(+), 1897 deletions(-) delete mode 100644 Projects/Shared/DesignSystem/Resources/$metadata.json delete mode 100644 Projects/Shared/DesignSystem/Resources/component.json delete mode 100644 Projects/Shared/DesignSystem/Resources/primitive.json delete mode 100644 Projects/Shared/DesignSystem/Resources/semantic.json diff --git a/Projects/Presentation/Auth/Sources/Main/View/LoginView.swift b/Projects/Presentation/Auth/Sources/Main/View/LoginView.swift index 418bd22..b18d343 100644 --- a/Projects/Presentation/Auth/Sources/Main/View/LoginView.swift +++ b/Projects/Presentation/Auth/Sources/Main/View/LoginView.swift @@ -5,32 +5,30 @@ // Created by Wonji Suh on 5/11/26. // -import SwiftUI import ComposableArchitecture +import SwiftUI import DesignSystem import Entity -public struct LoginView : View { +public struct LoginView: View { @Bindable var store: StoreOf - - - + public var body: some View { ZStack { - Color.neutral50 + Color.gray50 .edgesIgnoringSafeArea(.all) - + VStack { logoView() - + Spacer() .frame(height: 200) - + loginSNSButtonText() - + logjnButton() - + Spacer() .frame(height: UIScreen.screenHeight * 0.12) } @@ -39,46 +37,43 @@ public struct LoginView : View { } } - extension LoginView { @ViewBuilder - private func logoView() -> some View { + private func logoView() -> some View { VStack(alignment: .center) { Spacer() - + Text(" 당신의 생각을") .pretendardCustomFont(textStyle: .headingMedium) .foregroundStyle(.neutral200) - + Image(asset: .loginLogo) .resizable() .scaledToFit() .frame(width: 106, height: 90) } } - + @ViewBuilder private func loginSNSButtonText() -> some View { HStack { Rectangle() - .fill(.borderGray) + .fill(.borderGrayDefault) .frame(width: 64, height: 1) - + Spacer() .frame(width: 12) - + Text("SNS 계정으로 로그인") .pretendardFont(family: .Medium, size: 15) .foregroundStyle(.neutral300) - - + Rectangle() - .fill(.borderGray) + .fill(.borderGrayDefault) .frame(width: 64, height: 1) - } } - + @ViewBuilder private func logjnButton() -> some View { HStack(alignment: .center, spacing: 32) { diff --git a/Projects/Shared/DesignSystem/Resources/$metadata.json b/Projects/Shared/DesignSystem/Resources/$metadata.json deleted file mode 100644 index ccc8a53..0000000 --- a/Projects/Shared/DesignSystem/Resources/$metadata.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "tokenSetOrder": [ - "primitive", - "semantic", - "component" - ] -} \ No newline at end of file diff --git a/Projects/Shared/DesignSystem/Resources/component.json b/Projects/Shared/DesignSystem/Resources/component.json deleted file mode 100644 index f739d1e..0000000 --- a/Projects/Shared/DesignSystem/Resources/component.json +++ /dev/null @@ -1,598 +0,0 @@ -{ - "button": { - "primary": { - "background": { - "default": { - "$type": "color", - "$value": "{action.primary.default}" - }, - "pressed": { - "$type": "color", - "$value": "{action.primary.pressed}" - }, - "disabled": { - "$type": "color", - "$value": "{action.primary.disabled}" - } - }, - "text": { - "default": { - "$type": "color", - "$value": "{text.inverse}" - } - }, - "large": { - "width": { - "$type": "sizing", - "$value": "343" - }, - "height": { - "$type": "sizing", - "$value": "52" - } - }, - "medium": { - "width": { - "$type": "sizing", - "$value": "343" - }, - "height": { - "$type": "sizing", - "$value": "42" - } - }, - "small": { - "width": { - "$type": "sizing", - "$value": "100" - }, - "height": { - "$type": "sizing", - "$value": "42" - } - } - }, - "icon": { - "background": { - "default": { - "$type": "color", - "$value": "{action.primary.default}" - }, - "disabled": { - "$type": "color", - "$value": "{action.primary.disabled}" - } - }, - "size": { - "$type": "sizing", - "$value": "{number-36}" - } - }, - "secondary": { - "background": { - "default": { - "$type": "color", - "$value": "{action.secondary.default}" - }, - "pressed": { - "$type": "color", - "$value": "{action.beige.strong}" - } - }, - "text": { - "default": { - "$type": "color", - "$value": "{text.primary}" - } - } - } - }, - "badge": { - "filled": { - "background": { - "$type": "color", - "$value": "{action.beige.strong}" - }, - "text": { - "$type": "color", - "$value": "{text.primary}" - } - }, - "outline": { - "background": { - "$type": "color", - "$value": "{action.beige.subtle}" - }, - "border": { - "$type": "color", - "$value": "{action.primary.subtle}" - }, - "text": { - "$type": "color", - "$value": "{primary.500}" - } - }, - "primary": { - "background": { - "$type": "color", - "$value": "{action.primary.default}" - }, - "text": { - "$type": "color", - "$value": "{text.inverse}" - } - }, - "counter": { - "background": { - "$type": "color", - "$value": "{gray.500}" - }, - "text": { - "active": { - "$type": "color", - "$value": "{text.inverse}" - }, - "default": { - "$type": "color", - "$value": "{text.muted}" - } - } - } - }, - "chip": { - "background": { - "default": { - "$type": "color", - "$value": "{action.primary.default}" - }, - "selected": { - "$type": "color", - "$value": "{primary.50}" - } - }, - "border": { - "default": { - "$type": "color", - "$value": "{action.primary.default}" - } - }, - "text": { - "default": { - "$type": "color", - "$value": "{text.inverse}" - }, - "selected": { - "$type": "color", - "$value": "{text.primary}" - } - } - }, - "toggle": { - "track": { - "on": { - "$type": "color", - "$value": "{action.primary.default}" - }, - "off": { - "$type": "color", - "$value": "{gray.100}" - } - }, - "thumb": { - "default": { - "$type": "color", - "$value": "{action.beige.subtle}" - }, - "size": { - "$type": "sizing", - "$value": "14" - } - }, - "width": { - "$type": "sizing", - "$value": "{number-32}" - }, - "height": { - "$type": "sizing", - "$value": "18" - } - }, - "card": { - "base": { - "background": { - "default": { - "$type": "color", - "$value": "{action.beige.subtle}" - }, - "beige": { - "$type": "color", - "$value": "{surface.beige.strong}" - } - }, - "border": { - "default": { - "$type": "color", - "$value": "{border.beige.default}" - }, - "selected": { - "$type": "color", - "$value": "{border.secondary.selected}" - }, - "primary": { - "$type": "color", - "$value": "{border.primary.default}" - } - }, - "text": { - "title": { - "$type": "color", - "$value": "{text.subtler}" - }, - "decription": { - "$type": "color", - "$value": "{text.muted}" - }, - "body": { - "$type": "color", - "$value": "{text.body}" - }, - "secondary": { - "$type": "color", - "$value": "{text.secondary}" - }, - "primary": { - "$type": "color", - "$value": "{text.primary}" - }, - "inverse": { - "$type": "color", - "$value": "{text.inverse}" - } - } - }, - "gray": { - "background": { - "default": { - "$type": "color", - "$value": "{action.gray.default}" - }, - "pressed": { - "$type": "color", - "$value": "{action.gray.pressed}" - } - }, - "border": { - "selected": { - "$type": "color", - "$value": "{border.secondary.selected}" - } - } - }, - "opinion": { - "background": { - "default": { - "$type": "color", - "$value": "{action.beige.default}" - }, - "pressed": { - "$type": "color", - "$value": "{action.beige.pressed}" - } - }, - "border": { - "default": { - "$type": "color", - "$value": "{border.beige.default}" - }, - "pressed": { - "$type": "color", - "$value": "{border.secondary.selected}" - } - } - }, - "avatar": { - "md": { - "$type": "sizing", - "$value": "{avatar.md}" - }, - "lg": { - "$type": "sizing", - "$value": "{avatar.lg}" - }, - "sm": { - "$type": "sizing", - "$value": "{avatar.sm}" - } - } - }, - "input": { - "textfield": { - "background": { - "default": { - "$type": "color", - "$value": "{action.beige.subtle}" - }, - "disabled": { - "$type": "color", - "$value": "{action.beige.pressed}" - } - }, - "border": { - "default": { - "$type": "color", - "$value": "{border.beige.default}" - }, - "focus": { - "$type": "color", - "$value": "{border.beige.focus}" - }, - "error": { - "$type": "color", - "$value": "{badge.outline.border}" - } - }, - "text": { - "default": { - "$type": "color", - "$value": "{text.muted}" - }, - "focus": { - "$type": "color", - "$value": "{text.subtler}" - }, - "error": { - "$type": "color", - "$value": "{text.error}" - } - }, - "width": { - "$type": "sizing", - "$value": "343" - }, - "height": { - "$type": "sizing", - "$value": "{number-44}" - } - }, - "textarea": { - "background": { - "default": { - "$type": "color", - "$value": "{action.beige.subtle}" - }, - "disabled": { - "$type": "color", - "$value": "{action.beige.strong}" - } - }, - "text": { - "default": { - "$type": "color", - "$value": "{text.muted}" - }, - "focus": { - "$type": "color", - "$value": "{text.subtler}" - }, - "error": { - "$type": "color", - "$value": "{text.error}" - } - }, - "border": { - "error": { - "$type": "color", - "$value": "{border.error.default}" - } - } - } - }, - "navigation": { - "tab": { - "text": { - "default": { - "$type": "color", - "$value": "{text.muted}" - }, - "active": { - "$type": "color", - "$value": "{text.primary}" - } - }, - "border": { - "default": { - "$type": "color", - "$value": "{border.gray.default}" - }, - "active": { - "$type": "color", - "$value": "{border.primary.default}" - } - } - } - }, - "popup": { - "background": { - "$type": "color", - "$value": "{surface.beige.strong}" - }, - "border": { - "$type": "color", - "$value": "{border.primary.default}" - }, - "text": { - "$type": "color", - "$value": "{text.default}" - }, - "width": { - "$type": "sizing", - "$value": "393" - } - }, - "thumbnail": { - "width": { - "$type": "sizing", - "$value": "196" - }, - "height": { - "$type": "sizing", - "$value": "140" - } - }, - "padding": { - "button": { - "primary": { - "large": { - "horizontal": { - "$type": "spacing", - "$value": "{padding.component.12}" - }, - "vertical": { - "$type": "spacing", - "$value": "{padding.component.12}" - } - }, - "medium": { - "horizontal": { - "$type": "spacing", - "$value": "{padding.component.12}" - }, - "vertical": { - "$type": "spacing", - "$value": "{padding.component.12}" - } - }, - "small": { - "horizontal": { - "$type": "spacing", - "$value": "{padding.component.12}" - }, - "vertical": { - "$type": "spacing", - "$value": "{padding.component.12}" - } - } - } - }, - "chip": { - "horizontal": { - "$type": "spacing", - "$value": "{padding.component.12}" - }, - "vertical": { - "$type": "spacing", - "$value": "{padding.component.6}" - } - }, - "badge": { - "horizontal": { - "$type": "spacing", - "$value": "{padding.component.6}" - }, - "vertical": { - "$type": "spacing", - "$value": "{padding.component.2}" - } - }, - "card": { - "base": { - "$type": "spacing", - "$value": "{padding.component.16}" - } - }, - "input": { - "horizontal": { - "$type": "spacing", - "$value": "{padding.component.8}" - } - }, - "navigation": { - "tab": { - "horizontal": { - "$type": "spacing", - "$value": "{padding.component.12}" - }, - "vertical": { - "$type": "spacing", - "$value": "{padding.component.12}" - } - } - }, - "appbar": { - "horizontal": { - "$type": "spacing", - "$value": "{padding.component.16}" - }, - "vertical": { - "$type": "spacing", - "$value": "{padding.component.12}" - } - } - }, - "listItem": { - "agreement": { - "background": { - "default": { - "$type": "color", - "$value": "{background.default}" - } - }, - "border": { - "default": { - "$type": "color", - "$value": "{border.gray.subtle}" - }, - "error": { - "$type": "color", - "$value": "{border.error.default}" - } - }, - "text": { - "defualt": { - "$type": "color", - "$value": "{text.default}" - }, - "error": { - "$type": "color", - "$value": "{text.error}" - } - } - } - }, - "checkbox": { - "background": { - "default": { - "$type": "color", - "$value": "{primary.50}" - }, - "selected": { - "$type": "color", - "$value": "{primary.500}" - } - }, - "border": { - "default": { - "$type": "color", - "$value": "{primary.100}" - }, - "selected": { - "$type": "color", - "$value": "{primary.700}" - } - }, - "width": { - "$type": "sizing", - "$value": "{number-24}" - }, - "height": { - "$type": "sizing", - "$value": "{number-24}" - } - }, - "avatar": { - "backround": { - "$type": "color", - "$value": "{action.beige.strong}" - } - } -} \ No newline at end of file diff --git a/Projects/Shared/DesignSystem/Resources/primitive.json b/Projects/Shared/DesignSystem/Resources/primitive.json deleted file mode 100644 index 4435d92..0000000 --- a/Projects/Shared/DesignSystem/Resources/primitive.json +++ /dev/null @@ -1,510 +0,0 @@ -{ - "130": { - "$type": "lineHeights", - "$value": "130%" - }, - "140": { - "$type": "lineHeights", - "$value": "140%" - }, - "150": { - "$type": "lineHeights", - "$value": "150%" - }, - "primary": { - "50": { - "$type": "color", - "$value": "#F3EBE9" - }, - "100": { - "$type": "color", - "$value": "#E7D7D3" - }, - "200": { - "$type": "color", - "$value": "#D0AFA8" - }, - "300": { - "$type": "color", - "$value": "#B8887C" - }, - "400": { - "$type": "color", - "$value": "#A16051" - }, - "500": { - "$type": "color", - "$value": "#893825" - }, - "600": { - "$type": "color", - "$value": "#7A3626" - }, - "700": { - "$type": "color", - "$value": "#71372A" - }, - "800": { - "$type": "color", - "$value": "#653226" - }, - "900": { - "$type": "color", - "$value": "#4E2A21" - } - }, - "secondary": { - "50": { - "$type": "color", - "$value": "#FCF8F1" - }, - "100": { - "$type": "color", - "$value": "#F9F1E3" - }, - "200": { - "$type": "color", - "$value": "#F3E3C7" - }, - "300": { - "$type": "color", - "$value": "#EDD5AC" - }, - "400": { - "$type": "color", - "$value": "#E7C790" - }, - "500": { - "$type": "color", - "$value": "#E1B974" - }, - "600": { - "$type": "color", - "$value": "#CEA969" - }, - "700": { - "$type": "color", - "$value": "#B7965C" - }, - "800": { - "$type": "color", - "$value": "#A38653" - }, - "900": { - "$type": "color", - "$value": "#92784A" - } - }, - "beige": { - "50": { - "$type": "color", - "$value": "#FEFEFD" - }, - "100": { - "$type": "color", - "$value": "#FDFCFB" - }, - "200": { - "$type": "color", - "$value": "#FBF9F7" - }, - "300": { - "$type": "color", - "$value": "#F9F7F2" - }, - "400": { - "$type": "color", - "$value": "#F7F4EE" - }, - "500": { - "$type": "color", - "$value": "#F5F1EA" - }, - "600": { - "$type": "color", - "$value": "#EFEAE0" - }, - "700": { - "$type": "color", - "$value": "#DAD1BF" - }, - "800": { - "$type": "color", - "$value": "#CEC1A8" - }, - "900": { - "$type": "color", - "$value": "#B7A88B" - } - }, - "gray": { - "50": { - "$type": "color", - "$value": "#EBEBEB" - }, - "100": { - "$type": "color", - "$value": "#D7D7D7" - }, - "200": { - "$type": "color", - "$value": "#B0AFAE" - }, - "300": { - "$type": "color", - "$value": "#888786" - }, - "400": { - "$type": "color", - "$value": "#615F5D" - }, - "500": { - "$type": "color", - "$value": "#393735" - }, - "600": { - "$type": "color", - "$value": "#2B2A28" - }, - "700": { - "$type": "color", - "$value": "#222120" - }, - "800": { - "$type": "color", - "$value": "#1A1918" - }, - "900": { - "$type": "color", - "$value": "#131212" - } - }, - "number-2": { - "$type": "number", - "$value": "2" - }, - "number-4": { - "$type": "number", - "$value": "4" - }, - "number-6": { - "$type": "number", - "$value": "6" - }, - "number-8": { - "$type": "number", - "$value": "8" - }, - "number-12": { - "$type": "number", - "$value": "12" - }, - "number-16": { - "$type": "number", - "$value": "16" - }, - "number-20": { - "$type": "number", - "$value": "20" - }, - "number-24": { - "$type": "number", - "$value": "24" - }, - "number-28": { - "$type": "number", - "$value": "28" - }, - "number-32": { - "$type": "number", - "$value": "32" - }, - "number-36": { - "$type": "number", - "$value": "36" - }, - "number-40": { - "$type": "number", - "$value": "40" - }, - "number-44": { - "$type": "number", - "$value": "44" - }, - "number-48": { - "$type": "number", - "$value": "48" - }, - "number-52": { - "$type": "number", - "$value": "52" - }, - "number-56": { - "$type": "number", - "$value": "56" - }, - "number-60": { - "$type": "number", - "$value": "60" - }, - "number-68": { - "$type": "number", - "$value": "68" - }, - "number-64": { - "$type": "number", - "$value": "64" - }, - "number-72": { - "$type": "number", - "$value": "72" - }, - "number-76": { - "$type": "number", - "$value": "76" - }, - "number-80": { - "$type": "number", - "$value": "80" - }, - "number-84": { - "$type": "number", - "$value": "84" - }, - "number-88": { - "$type": "number", - "$value": "88" - }, - "number-92": { - "$type": "number", - "$value": "92" - }, - "number-96": { - "$type": "number", - "$value": "96" - }, - "number-100": { - "$type": "number", - "$value": "100" - }, - "icon": { - "xs": { - "$type": "sizing", - "$value": "{number-12}" - }, - "sm": { - "$type": "sizing", - "$value": "{number-16}" - }, - "md": { - "$type": "sizing", - "$value": "{number-20}" - }, - "lg": { - "$type": "sizing", - "$value": "{number-24}" - } - }, - "avatar": { - "sm": { - "$type": "sizing", - "$value": "{number-36}" - }, - "md": { - "$type": "sizing", - "$value": "{number-40}" - }, - "lg": { - "$type": "sizing", - "$value": "{number-68}" - } - }, - "control": { - "sm": { - "$type": "sizing", - "$value": "{number-28}", - "$description": "카테고리 칩, 정렬 칩 높이" - }, - "md": { - "$type": "sizing", - "$value": "{number-52}", - "$description": "버튼" - } - }, - "spacing-0": { - "$type": "spacing", - "$value": "{number-0}" - }, - "number-0": { - "$type": "number", - "$value": "0" - }, - "spacing-4": { - "$type": "spacing", - "$value": "{number-4}" - }, - "spacing-2": { - "$type": "spacing", - "$value": "{number-2}" - }, - "spacing-8": { - "$type": "spacing", - "$value": "{number-8}" - }, - "spacing-16": { - "$type": "spacing", - "$value": "{number-16}" - }, - "spacing-24": { - "$type": "spacing", - "$value": "{number-24}" - }, - "spacing-32": { - "$type": "spacing", - "$value": "{number-32}" - }, - "spacing-40": { - "$type": "spacing", - "$value": "{number-40}" - }, - "spacing-48": { - "$type": "spacing", - "$value": "{number-48}" - }, - "spacing-64": { - "$type": "spacing", - "$value": "{number-64}" - }, - "spacing-80": { - "$type": "spacing", - "$value": "{number-80}" - }, - "spacing-96": { - "$type": "spacing", - "$value": "{number-96}" - }, - "type": { - "$type": "fontFamilies", - "$value": "Pretendard" - }, - "font-weight-regular": { - "$type": "fontWeights", - "$value": "regular" - }, - "font-weight-medium": { - "$type": "fontWeights", - "$value": "medium" - }, - "font-weight-bold": { - "$type": "fontWeights", - "$value": "bold" - }, - "font-weight-semibold": { - "$type": "fontWeights", - "$value": "semibold" - }, - "error": { - "default": { - "$type": "color", - "$value": "#C92D33" - }, - "alpha": { - "$type": "color", - "$value": "rgba(201,45,51,0.4)" - } - }, - "warning": { - "default": { - "$type": "color", - "$value": "#FFB400" - }, - "slpha": { - "$type": "color", - "$value": "rgba(255,180,0,0.4)" - } - }, - "spacing-6": { - "$type": "spacing", - "$value": "{number-6}" - }, - "spscing-12": { - "$type": "spacing", - "$value": "{number-12}" - }, - "spacing-20": { - "$type": "spacing", - "$value": "{number-20}" - }, - "headings": { - "xs": { - "$type": "fontSizes", - "$value": "14" - }, - "sm": { - "$type": "fontSizes", - "$value": "{number-16}" - }, - "md": { - "$type": "fontSizes", - "$value": "18" - }, - "lg": { - "$type": "fontSizes", - "$value": "{number-20}" - }, - "xl": { - "$type": "fontSizes", - "$value": "{number-24}" - } - }, - "displays": { - "md": { - "$type": "fontSizes", - "$value": "30" - }, - "lg": { - "$type": "fontSizes", - "$value": "{number-40}" - } - }, - "bodys": { - "lg": { - "$type": "fontSizes", - "$value": "{number-16}" - }, - "md": { - "$type": "fontSizes", - "$value": "15" - }, - "sm": { - "$type": "fontSizes", - "$value": "14" - }, - "xs": { - "$type": "fontSizes", - "$value": "13" - }, - "xxs": { - "$type": "fontSizes", - "$value": "12" - } - }, - "captions": { - "lg": { - "$type": "fontSizes", - "$value": "12" - }, - "md": { - "$type": "fontSizes", - "$value": "11" - }, - "sm": { - "$type": "fontSizes", - "$value": "10" - } - }, - "font-weight-extrabold": { - "$type": "fontWeights", - "$value": "extrabold" - } -} \ No newline at end of file diff --git a/Projects/Shared/DesignSystem/Resources/semantic.json b/Projects/Shared/DesignSystem/Resources/semantic.json deleted file mode 100644 index 55c1442..0000000 --- a/Projects/Shared/DesignSystem/Resources/semantic.json +++ /dev/null @@ -1,725 +0,0 @@ -{ - "background": { - "default": { - "$type": "color", - "$value": "#FAFAF9", - "$description": "전체 화면 배경" - }, - "subtle": { - "$type": "color", - "$value": "#F5F5F4", - "$description": "연한 회색 전체 화면 배경" - }, - "subtler": { - "$type": "color", - "$value": "{gray.50}", - "$description": "진한 회색 전체 화면 배경" - }, - "beige": { - "$type": "color", - "$value": "{beige.200}", - "$description": "마이페이지" - } - }, - "text": { - "default": { - "$type": "color", - "$value": "{gray.800}" - }, - "subtle": { - "$type": "color", - "$value": "{gray.700}" - }, - "subtler": { - "$type": "color", - "$value": "{gray.500}" - }, - "muted": { - "$type": "color", - "$value": "{gray.300}" - }, - "inverse": { - "$type": "color", - "$value": "{beige.50}" - }, - "primary": { - "$type": "color", - "$value": "{primary.500}" - }, - "body": { - "$type": "color", - "$value": "{gray.400}" - }, - "secondary": { - "$type": "color", - "$value": "{secondary.500}" - }, - "error": { - "$type": "color", - "$value": "{error.default}" - } - }, - "border": { - "beige": { - "selected": { - "$type": "color", - "$value": "{beige.700}" - }, - "disabled": { - "$type": "color", - "$value": "{beige.500}" - }, - "focus": { - "$type": "color", - "$value": "{beige.700}" - }, - "default": { - "$type": "color", - "$value": "{beige.600}" - } - }, - "secondary": { - "selected": { - "$type": "color", - "$value": "{secondary.500}" - } - }, - "primary": { - "default": { - "$type": "color", - "$value": "{primary.500}" - } - }, - "error": { - "default": { - "$type": "color", - "$value": "{error.alpha}" - } - }, - "warning": { - "default": { - "$type": "color", - "$value": "{warning.slpha}" - } - }, - "gray": { - "default": { - "$type": "color", - "$value": "{gray.100}" - }, - "subtle": { - "$type": "color", - "$value": "{gray.50}" - } - } - }, - "surface": { - "beige": { - "default": { - "$type": "color", - "$value": "{beige.50}", - "$description": "카드, 리스트 아이템, 입력창 같은 기본 면\n예: 흰색 카드" - }, - "subtle": { - "$type": "color", - "$value": "{beige.300}", - "$description": "베이지 버튼\n예: 홈화면 투표 버튼, 배튼 리스트 버튼" - }, - "strong": { - "$type": "color", - "$value": "{beige.400}" - } - }, - "primary": { - "default": { - "$type": "color", - "$value": "{primary.500}" - }, - "subtle": { - "$type": "color", - "$value": "{primary.50}" - } - } - }, - "action": { - "primary": { - "default": { - "$type": "color", - "$value": "{primary.500}" - }, - "pressed": { - "$type": "color", - "$value": "{primary.800}" - }, - "disabled": { - "$type": "color", - "$value": "{primary.200}" - }, - "subtle": { - "$type": "color", - "$value": "{primary.100}" - } - }, - "beige": { - "default": { - "$type": "color", - "$value": "{beige.300}" - }, - "subtle": { - "$type": "color", - "$value": "{beige.50}" - }, - "pressed": { - "$type": "color", - "$value": "{beige.400}" - }, - "strong": { - "$type": "color", - "$value": "{beige.600}" - } - }, - "secondary": { - "default": { - "$type": "color", - "$value": "{secondary.50}" - } - }, - "gray": { - "default": { - "$type": "color", - "$value": "{gray.700}" - }, - "pressed": { - "$type": "color", - "$value": "{gray.900}" - } - } - }, - "gap": { - "0": { - "$type": "spacing", - "$value": "{spacing-0}" - }, - "2": { - "$type": "spacing", - "$value": "{spacing-2}" - }, - "4": { - "$type": "spacing", - "$value": "{spacing-4}" - }, - "6": { - "$type": "spacing", - "$value": "{spacing-6}" - }, - "8": { - "$type": "spacing", - "$value": "{spacing-8}" - }, - "12": { - "$type": "spacing", - "$value": "{spscing-12}" - }, - "16": { - "$type": "spacing", - "$value": "{spacing-16}" - }, - "20": { - "$type": "spacing", - "$value": "{spacing-20}" - }, - "24": { - "$type": "spacing", - "$value": "{spacing-24}" - }, - "32": { - "$type": "spacing", - "$value": "{spacing-32}" - }, - "40": { - "$type": "spacing", - "$value": "{spacing-40}" - } - }, - "padding": { - "screen": { - "horizontal": { - "$type": "spacing", - "$value": "{spacing-16}" - } - }, - "container": { - "8": { - "$type": "spacing", - "$value": "{spacing-8}" - }, - "12": { - "$type": "spacing", - "$value": "{spscing-12}" - }, - "16": { - "$type": "spacing", - "$value": "{spacing-16}" - }, - "20": { - "$type": "spacing", - "$value": "{spacing-20}" - }, - "24": { - "$type": "spacing", - "$value": "{spacing-24}" - }, - "32": { - "$type": "spacing", - "$value": "{spacing-32}" - }, - "40": { - "$type": "spacing", - "$value": "{spacing-40}" - } - }, - "component": { - "2": { - "$type": "spacing", - "$value": "{spacing-2}" - }, - "4": { - "$type": "spacing", - "$value": "{spacing-4}" - }, - "6": { - "$type": "spacing", - "$value": "{spacing-6}" - }, - "8": { - "$type": "spacing", - "$value": "{spacing-8}" - }, - "12": { - "$type": "spacing", - "$value": "{spscing-12}" - }, - "16": { - "$type": "spacing", - "$value": "{spacing-16}" - } - } - }, - "radius-default": { - "$type": "borderRadius", - "$value": "{number-2}" - }, - "radius-max": { - "$type": "borderRadius", - "$value": "999" - }, - "border-width-regular": { - "$type": "borderWidth", - "$value": "1" - }, - "border-width-medium": { - "$type": "borderWidth", - "$value": "1.5" - }, - "border-width-large": { - "$type": "borderWidth", - "$value": "4" - }, - "heading": { - "xl": { - "$type": "typography", - "$value": { - "fontFamily": "{type}", - "fontWeight": "{font-weight-semibold}", - "fontSize": "{headings.xl}", - "lineHeight": "{130}", - "letterSpacing": "{number-0}" - } - }, - "lg": { - "$type": "typography", - "$value": { - "fontFamily": "{type}", - "fontWeight": "{font-weight-semibold}", - "fontSize": "{headings.lg}", - "lineHeight": "{130}", - "letterSpacing": "{number-0}" - } - }, - "md": { - "$type": "typography", - "$value": { - "fontFamily": "{type}", - "fontWeight": "{font-weight-bold}", - "fontSize": "{headings.md}", - "lineHeight": "{130}", - "letterSpacing": "{number-0}" - } - }, - "sm": { - "$type": "typography", - "$value": { - "fontFamily": "{type}", - "fontWeight": "{font-weight-semibold}", - "fontSize": "{headings.sm}", - "lineHeight": "{130}", - "letterSpacing": "{number-0}" - } - }, - "xs": { - "$type": "typography", - "$value": { - "fontFamily": "{type}", - "fontSize": "{headings.xs}", - "fontWeight": "{font-weight-semibold}", - "lineHeight": "{130}", - "letterSpacing": "{number-0}" - } - } - }, - "body": { - "lg": { - "regular": { - "$type": "typography", - "$value": { - "fontFamily": "{type}", - "fontWeight": "{font-weight-regular}", - "fontSize": "{bodys.lg}", - "lineHeight": "{150}", - "letterSpacing": "{number-0}" - } - }, - "medium": { - "$type": "typography", - "$value": { - "fontFamily": "{type}", - "fontWeight": "{font-weight-medium}", - "fontSize": "{bodys.lg}", - "lineHeight": "{150}", - "letterSpacing": "{number-0}" - } - }, - "semibold": { - "$type": "typography", - "$value": { - "fontFamily": "{type}", - "fontWeight": "{font-weight-semibold}", - "fontSize": "{bodys.lg}", - "lineHeight": "{150}", - "letterSpacing": "{number-0}" - } - }, - "bold": { - "$type": "typography", - "$value": { - "fontFamily": "{type}", - "fontWeight": "{font-weight-bold}", - "fontSize": "{bodys.lg}", - "lineHeight": "{150}", - "letterSpacing": "{number-0}" - } - } - }, - "md": { - "semebold": { - "$type": "typography", - "$value": { - "fontFamily": "{type}", - "fontWeight": "{font-weight-semibold}", - "fontSize": "{bodys.md}", - "lineHeight": "{150}", - "letterSpacing": "{number-0}" - } - }, - "medium": { - "$type": "typography", - "$value": { - "fontFamily": "{type}", - "fontWeight": "{font-weight-medium}", - "fontSize": "{bodys.md}", - "lineHeight": "{150}", - "letterSpacing": "{number-0}" - } - }, - "bold": { - "$type": "typography", - "$value": { - "fontFamily": "{type}", - "fontWeight": "{font-weight-bold}", - "fontSize": "{bodys.md}", - "lineHeight": "{150}", - "letterSpacing": "{number-0}" - } - } - }, - "sm": { - "regular": { - "$type": "typography", - "$value": { - "fontFamily": "{type}", - "fontWeight": "{font-weight-regular}", - "fontSize": "{bodys.sm}", - "lineHeight": "{140}", - "letterSpacing": "{number-0}" - } - }, - "medium": { - "$type": "typography", - "$value": { - "fontFamily": "{type}", - "fontWeight": "{font-weight-medium}", - "fontSize": "{bodys.sm}", - "lineHeight": "{140}", - "letterSpacing": "{number-0}" - } - }, - "semibold": { - "$type": "typography", - "$value": { - "fontFamily": "{type}", - "fontWeight": "{font-weight-semibold}", - "fontSize": "{bodys.sm}", - "lineHeight": "{140}", - "letterSpacing": "{number-0}" - } - } - }, - "xs": { - "medium": { - "$type": "typography", - "$value": { - "fontFamily": "{type}", - "fontWeight": "{font-weight-medium}", - "fontSize": "{bodys.xs}", - "lineHeight": "{140}", - "letterSpacing": "{number-0}" - } - }, - "semibold": { - "$type": "typography", - "$value": { - "fontFamily": "{type}", - "fontWeight": "{font-weight-semibold}", - "fontSize": "{bodys.xs}", - "lineHeight": "{140}", - "letterSpacing": "{number-0}" - } - }, - "regular": { - "$type": "typography", - "$value": { - "fontFamily": "{type}", - "fontWeight": "{font-weight-regular}", - "fontSize": "{bodys.xs}", - "lineHeight": "{140}", - "letterSpacing": "{number-0}" - } - } - }, - "xxs": { - "semibold": { - "$type": "typography", - "$value": { - "fontFamily": "{type}", - "fontWeight": "{font-weight-semibold}", - "fontSize": "{bodys.xxs}", - "lineHeight": "{140}", - "letterSpacing": "{number-0}" - } - }, - "regular": { - "$type": "typography", - "$value": { - "fontFamily": "{type}", - "fontWeight": "{font-weight-regular}", - "fontSize": "{bodys.xxs}", - "lineHeight": "{140}", - "letterSpacing": "{number-0}" - } - }, - "medium": { - "$type": "typography", - "$value": { - "fontFamily": "{type}", - "fontWeight": "{font-weight-medium}", - "fontSize": "{bodys.xxs}", - "lineHeight": "{140}", - "letterSpacing": "{number-0}" - } - } - } - }, - "display": { - "lg": { - "$type": "typography", - "$value": { - "fontFamily": "{type}", - "fontWeight": "{font-weight-extrabold}", - "fontSize": "{displays.lg}", - "lineHeight": "{130}", - "letterSpacing": "{number-0}" - } - }, - "md": { - "$type": "typography", - "$value": { - "fontFamily": "{type}", - "fontWeight": "{font-weight-semibold}", - "fontSize": "{displays.md}", - "lineHeight": "{130}", - "letterSpacing": "{number-0}" - } - } - }, - "caption": { - "lg": { - "regular": { - "$type": "typography", - "$value": { - "fontFamily": "{type}", - "fontWeight": "{font-weight-regular}", - "fontSize": "{captions.lg}", - "lineHeight": "{140}", - "letterSpacing": "{number-0}" - } - }, - "semibold": { - "$type": "typography", - "$value": { - "fontFamily": "{type}", - "fontWeight": "{font-weight-semibold}", - "fontSize": "{captions.lg}", - "lineHeight": "{140}", - "letterSpacing": "{number-0}" - } - }, - "bold": { - "$type": "typography", - "$value": { - "fontFamily": "{type}", - "fontWeight": "{font-weight-bold}", - "fontSize": "{captions.lg}", - "lineHeight": "{140}", - "letterSpacing": "{number-0}" - } - }, - "medium": { - "$type": "typography", - "$value": { - "fontFamily": "{type}", - "fontWeight": "{font-weight-medium}", - "fontSize": "{captions.md}", - "lineHeight": "{140}", - "letterSpacing": "{number-0}" - } - } - }, - "md": { - "semibold": { - "$type": "typography", - "$value": { - "fontFamily": "{type}", - "fontWeight": "{font-weight-semibold}", - "fontSize": "{captions.md}", - "lineHeight": "{140}", - "letterSpacing": "{number-0}" - } - }, - "medium": { - "$type": "typography", - "$value": { - "fontFamily": "{type}", - "fontWeight": "{font-weight-medium}", - "fontSize": "{captions.md}", - "lineHeight": "{140}", - "letterSpacing": "{number-0}" - } - }, - "regular": { - "$type": "typography", - "$value": { - "fontFamily": "{type}", - "fontWeight": "{font-weight-regular}", - "fontSize": "{captions.md}", - "lineHeight": "{140}", - "letterSpacing": "{number-0}" - } - }, - "bold": { - "$type": "typography", - "$value": { - "fontFamily": "{type}", - "fontWeight": "{font-weight-bold}", - "fontSize": "{captions.md}", - "lineHeight": "{140}", - "letterSpacing": "{number-0}" - } - } - }, - "sm": { - "medium": { - "$type": "typography", - "$value": { - "fontFamily": "{type}", - "fontWeight": "{font-weight-medium}", - "fontSize": "{captions.sm}", - "lineHeight": "{140}", - "letterSpacing": "{number-0}" - } - }, - "semibold": { - "$type": "typography", - "$value": { - "fontFamily": "{type}", - "fontWeight": "{font-weight-semibold}", - "fontSize": "{captions.sm}", - "lineHeight": "{140}", - "letterSpacing": "{number-0}" - } - }, - "bold": { - "$type": "typography", - "$value": { - "fontFamily": "{type}", - "fontWeight": "{font-weight-bold}", - "fontSize": "{captions.sm}", - "lineHeight": "{140}", - "letterSpacing": "{number-0}" - } - } - } - }, - "default": { - "$type": "boxShadow", - "$value": { - "x": "0", - "y": "-4", - "blur": "12", - "spread": "0", - "color": "rgba(0,0,0,0.08)", - "type": "dropShadow" - } - }, - "icon": { - "gray": { - "default": { - "$type": "color", - "$value": "{gray.900}" - }, - "subtle": { - "$type": "color", - "$value": "{gray.300}" - }, - "inverse": { - "$type": "color", - "$value": "{text.inverse}" - } - }, - "primary": { - "default": { - "$type": "color", - "$value": "{primary.500}" - } - } - } -} \ No newline at end of file diff --git a/Projects/Shared/DesignSystem/Sources/Color/ShapeStyle+.swift b/Projects/Shared/DesignSystem/Sources/Color/ShapeStyle+.swift index f705a69..500e283 100644 --- a/Projects/Shared/DesignSystem/Sources/Color/ShapeStyle+.swift +++ b/Projects/Shared/DesignSystem/Sources/Color/ShapeStyle+.swift @@ -1,5 +1,5 @@ // AUTO-GENERATED by Tools/TokenGenerator.swift — DO NOT EDIT -// Source: Projects/Shared/DesignSystem/Resources/{primitive,semantic,component}.json +// Source: SWYP-Find/design-tokens repo (primitive.json / semantic.json / component.json) import SwiftUI @@ -53,6 +53,18 @@ public extension ShapeStyle where Self == Color { static var gray800: Color { .init(hex: "1A1918") } static var gray900: Color { .init(hex: "131212") } + // MARK: - Compat / Neutral (alias of Gray) + static var neutral50: Color { .gray50 } + static var neutral100: Color { .gray100 } + static var neutral200: Color { .gray200 } + static var neutral300: Color { .gray300 } + static var neutral400: Color { .gray400 } + static var neutral500: Color { .gray500 } + static var neutral600: Color { .gray600 } + static var neutral700: Color { .gray700 } + static var neutral800: Color { .gray800 } + static var neutral900: Color { .gray900 } + // MARK: - Primitive / Status static var errorAlpha: Color { .init(hex: "C92D33", alpha: 0.4) } static var errorDefault: Color { .init(hex: "C92D33") } diff --git a/Projects/Shared/DesignSystem/Sources/Extension/CGFloat/CGFloat+Component+.swift b/Projects/Shared/DesignSystem/Sources/Extension/CGFloat/CGFloat+Component+.swift index 1b27577..2b35bf6 100644 --- a/Projects/Shared/DesignSystem/Sources/Extension/CGFloat/CGFloat+Component+.swift +++ b/Projects/Shared/DesignSystem/Sources/Extension/CGFloat/CGFloat+Component+.swift @@ -1,5 +1,5 @@ // AUTO-GENERATED by Tools/TokenGenerator.swift — DO NOT EDIT -// Source: Projects/Shared/DesignSystem/Resources/{primitive,semantic,component}.json +// Source: SWYP-Find/design-tokens repo (primitive.json / semantic.json / component.json) import CoreGraphics diff --git a/Projects/Shared/DesignSystem/Sources/Extension/CGFloat/CGFloat+Radius+.swift b/Projects/Shared/DesignSystem/Sources/Extension/CGFloat/CGFloat+Radius+.swift index ebdfce9..94b4608 100644 --- a/Projects/Shared/DesignSystem/Sources/Extension/CGFloat/CGFloat+Radius+.swift +++ b/Projects/Shared/DesignSystem/Sources/Extension/CGFloat/CGFloat+Radius+.swift @@ -1,5 +1,5 @@ // AUTO-GENERATED by Tools/TokenGenerator.swift — DO NOT EDIT -// Source: Projects/Shared/DesignSystem/Resources/{primitive,semantic,component}.json +// Source: SWYP-Find/design-tokens repo (primitive.json / semantic.json / component.json) import CoreGraphics diff --git a/Projects/Shared/DesignSystem/Sources/Extension/CGFloat/CGFloat+Spacing+.swift b/Projects/Shared/DesignSystem/Sources/Extension/CGFloat/CGFloat+Spacing+.swift index 7113d5d..ae90fe1 100644 --- a/Projects/Shared/DesignSystem/Sources/Extension/CGFloat/CGFloat+Spacing+.swift +++ b/Projects/Shared/DesignSystem/Sources/Extension/CGFloat/CGFloat+Spacing+.swift @@ -1,5 +1,5 @@ // AUTO-GENERATED by Tools/TokenGenerator.swift — DO NOT EDIT -// Source: Projects/Shared/DesignSystem/Resources/{primitive,semantic,component}.json +// Source: SWYP-Find/design-tokens repo (primitive.json / semantic.json / component.json) import CoreGraphics diff --git a/Projects/Shared/DesignSystem/Sources/UI/Button/CustomButtonConfig.swift b/Projects/Shared/DesignSystem/Sources/UI/Button/CustomButtonConfig.swift index 04d2861..eea39b4 100644 --- a/Projects/Shared/DesignSystem/Sources/UI/Button/CustomButtonConfig.swift +++ b/Projects/Shared/DesignSystem/Sources/UI/Button/CustomButtonConfig.swift @@ -23,7 +23,7 @@ public class CustomButtonConfig: PickeCustomButtonConfig { ) -> PickeCustomButtonConfig { let variant: CTAButtonVariant = .primary return PickeCustomButtonConfig( - cornerRadius: .default, + cornerRadius: .radiusDefault, enableFontColor: variant.foregroundColor(isEnabled: true), enableBackgroundColor: variant.backgroundColor(isEnabled: true), frameHeight: height ?? size.height, diff --git a/Projects/Shared/DesignSystem/Sources/UI/Toast/ToastType.swift b/Projects/Shared/DesignSystem/Sources/UI/Toast/ToastType.swift index 490c8b3..f61f57c 100644 --- a/Projects/Shared/DesignSystem/Sources/UI/Toast/ToastType.swift +++ b/Projects/Shared/DesignSystem/Sources/UI/Toast/ToastType.swift @@ -28,15 +28,15 @@ public enum ToastType: Equatable { public var backgroundColor: Color { switch self { case .success: - return .neutral50 + return .gray50 case .error: - return .neutral50 + return .gray50 case .warning: - return .neutral50 + return .gray50 case .info: - return .neutral50 + return .gray50 case .loading: - return .neutral50 + return .gray50 } } diff --git a/Projects/Shared/DesignSystem/Sources/UI/Token/ComponentToken.swift b/Projects/Shared/DesignSystem/Sources/UI/Token/ComponentToken.swift index eb175eb..35007f1 100644 --- a/Projects/Shared/DesignSystem/Sources/UI/Token/ComponentToken.swift +++ b/Projects/Shared/DesignSystem/Sources/UI/Token/ComponentToken.swift @@ -1,5 +1,5 @@ // AUTO-GENERATED by Tools/TokenGenerator.swift — DO NOT EDIT -// Source: Projects/Shared/DesignSystem/Resources/{primitive,semantic,component}.json +// Source: SWYP-Find/design-tokens repo (primitive.json / semantic.json / component.json) import SwiftUI diff --git a/Tools/TokenGenerator.swift b/Tools/TokenGenerator.swift index bdb04fe..f3a4dce 100644 --- a/Tools/TokenGenerator.swift +++ b/Tools/TokenGenerator.swift @@ -1,19 +1,48 @@ #!/usr/bin/env swift // // TokenGenerator.swift -// Reads {primitive,semantic,component}.json (Tokens Studio format) and emits Swift token files. -// Run from repo root: swift Tools/TokenGenerator.swift +// Reads Tokens Studio JSON (primitive/semantic/component) from the SWYP-Find/design-tokens +// repo and emits Swift token files. Source resolution order: +// 1. $TOKENS_SOURCE_DIR (CI / explicit override) +// 2. ../design-tokens (local layout: sibling checkout) +// Run from Picke-iOS repo root: swift Tools/TokenGenerator.swift // import Foundation -// MARK: - Paths +// MARK: - Source resolution +let env = ProcessInfo.processInfo.environment let cwd = FileManager.default.currentDirectoryPath -let resourcesDir = "\(cwd)/Projects/Shared/DesignSystem/Resources" -let primitiveURL = URL(fileURLWithPath: "\(resourcesDir)/primitive.json") -let semanticURL = URL(fileURLWithPath: "\(resourcesDir)/semantic.json") -let componentURL = URL(fileURLWithPath: "\(resourcesDir)/component.json") + +func resolveSourceDir() -> String { + if let override = env["TOKENS_SOURCE_DIR"], !override.isEmpty { + return override + } + let sibling = URL(fileURLWithPath: cwd).deletingLastPathComponent() + .appendingPathComponent("design-tokens").path + return sibling +} + +let sourceDir = resolveSourceDir() +let primitiveURL = URL(fileURLWithPath: "\(sourceDir)/primitive.json") +let semanticURL = URL(fileURLWithPath: "\(sourceDir)/semantic.json") +let componentURL = URL(fileURLWithPath: "\(sourceDir)/component.json") + +for url in [primitiveURL, semanticURL, componentURL] { + guard FileManager.default.fileExists(atPath: url.path) else { + fputs(""" + [token-gen] cannot find \(url.lastPathComponent) at: \(url.path) + Set TOKENS_SOURCE_DIR to your design-tokens checkout, e.g. + TOKENS_SOURCE_DIR=../design-tokens swift Tools/TokenGenerator.swift + Or clone SWYP-Find/design-tokens as a sibling of this repo. + + """, stderr) + exit(1) + } +} + +// MARK: - Output paths let sourcesDir = "\(cwd)/Projects/Shared/DesignSystem/Sources" let colorOut = "\(sourcesDir)/Color/ShapeStyle+.swift" @@ -133,11 +162,6 @@ func lowerFirst(_ s: String) -> String { s.prefix(1).lowercased() + s.dropFirst() } -/// `"radius-default"` / `"font-weight-bold"` → `["radius", "default"]` / `["font", "weight", "bold"]`. -/// Also handles known typos like `"spscing-12"` → `["spscing", "12"]` (preserved verbatim). -func splitDashed(_ s: String) -> [String] { s.split(separator: "-").map(String.init) } - -/// `["gap", "0"]` → `"gap0"`, `["padding", "container", "8"]` → `"paddingContainer8"`. func camelCase(_ segs: [String]) -> String { guard let first = segs.first else { return "" } let head = lowerFirst(first) @@ -162,12 +186,11 @@ func writeFile(_ path: String, _ contents: String) throws { let header = """ // AUTO-GENERATED by Tools/TokenGenerator.swift — DO NOT EDIT -// Source: Projects/Shared/DesignSystem/Resources/{primitive,semantic,component}.json +// Source: SWYP-Find/design-tokens repo (primitive.json / semantic.json / component.json) """ // MARK: - Color emission -/// Tracks emitted color symbols so downstream emissions can prefer aliases over inline hex. var hexToColorName: [String: String] = [:] func emitColor(name: String, value: Any, into lines: inout [String]) { @@ -182,7 +205,7 @@ func emitColor(name: String, value: Any, into lines: inout [String]) { var colorLines: [String] = [header, "", "import SwiftUI", "", "public extension ShapeStyle where Self == Color {", ""] -// Primitive color scales: primary / secondary / beige / gray (50..900) +// Primitive color scales: primary / secondary / beige / gray (50..900). let primitiveColorGroups = ["primary", "secondary", "beige", "gray"] let scales = ["50", "100", "200", "300", "400", "500", "600", "700", "800", "900"] for group in primitiveColorGroups { @@ -195,6 +218,17 @@ for group in primitiveColorGroups { colorLines.append("") } +// Backward-compat aliases: `neutral{N}` → `gray{N}`. Earlier call sites used `neutral*` +// before the upstream rename to `gray*`. Keep emitting both so existing usages compile; +// new code should prefer `gray*`. +if primitive["gray"] != nil { + colorLines.append(" // MARK: - Compat / Neutral (alias of Gray)") + for s in scales { + colorLines.append(" static var neutral\(s): Color { .gray\(s) }") + } + colorLines.append("") +} + // Primitive status colors (error / warning, including alpha variants — typo "slpha" normalized). colorLines.append(" // MARK: - Primitive / Status") for bucket in ["error", "warning"] { @@ -278,7 +312,6 @@ try writeFile(colorOut, colorLines.joined(separator: "\n")) var spacingLines: [String] = [header, "", "import CoreGraphics", "", "public extension CGFloat {", ""] -// Primitive spacing-N (note: source has typo "spscing-12" — emit canonical s12 from its resolved value). spacingLines.append(" // MARK: - Primitive Spacing") var spacingPairs: [(value: Int, line: String)] = [] for (key, value) in primitive { @@ -297,7 +330,6 @@ for (_, line) in spacingPairs.sorted(by: { $0.value < $1.value }) { spacingLines.append("") -// Primitive sizing (icon / avatar / control). spacingLines.append(" // MARK: - Primitive Sizing") for group in ["icon", "avatar", "control"] { guard let g = primitive[group] as? [String: Any] else { continue } @@ -309,7 +341,6 @@ for group in ["icon", "avatar", "control"] { spacingLines.append("") -// Semantic gap. if let gap = semantic["gap"] as? [String: Any] { spacingLines.append(" // MARK: - Semantic Gap") var gapPairs: [(Int, String)] = [] @@ -324,11 +355,9 @@ if let gap = semantic["gap"] as? [String: Any] { spacingLines.append("") } -// Semantic padding. if let padding = semantic["padding"] as? [String: Any] { spacingLines.append(" // MARK: - Semantic Padding") var paddingLines: [String] = [] - walkColorLeaves(padding, path: ["padding"]) { _, _ in } // unused, just to keep walker available func walkPadding(_ node: [String: Any], path: [String]) { for (key, value) in node.sorted(by: { $0.key < $1.key }) { guard let dict = value as? [String: Any] else { continue } @@ -346,7 +375,6 @@ if let padding = semantic["padding"] as? [String: Any] { spacingLines.append("") } -// Semantic border-width-{regular,medium,large}. spacingLines.append(" // MARK: - Semantic Border Width") for variant in ["regular", "medium", "large"] { let key = "border-width-\(variant)" @@ -429,4 +457,4 @@ ctLines.append("}") ctLines.append("") try writeFile(componentTokenOut, ctLines.joined(separator: "\n")) -print("[token-gen] done.") +print("[token-gen] done. source=\(sourceDir)") From d1cc3ece6ebb5a6c708b18648611d316d0051be7 Mon Sep 17 00:00:00 2001 From: Roy Date: Tue, 19 May 2026 22:58:16 +0900 Subject: [PATCH 18/21] =?UTF-8?q?fix:=20#19=20Kingfisher=20Bearer=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=EC=9D=84=20picke=20=EB=B3=B4=ED=98=B8=20?= =?UTF-8?q?=EB=A6=AC=EC=86=8C=EC=8A=A4(/api/)=20=EC=97=90=EB=A7=8C=20?= =?UTF-8?q?=EC=A0=9C=ED=95=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - protectedHostSuffixes 화이트리스트 (picke.store / dev.picke.store) 도입 - host suffix 정확 일치 + path.hasPrefix("/api/") + 토큰 존재 4-조건 만족 시에만 Authorization 헤더 첨부 - picsum.photos / 카카오 CDN 같은 외부 호스트로 keychain accessToken 이 새지 않도록 차단 --- .../Application/KingfisherConfigurator.swift | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/Projects/App/Sources/Application/KingfisherConfigurator.swift b/Projects/App/Sources/Application/KingfisherConfigurator.swift index 6500fe4..c94bff9 100644 --- a/Projects/App/Sources/Application/KingfisherConfigurator.swift +++ b/Projects/App/Sources/Application/KingfisherConfigurator.swift @@ -15,14 +15,30 @@ import DomainInterface import Foundations enum KingfisherConfigurator { + /// 보호 이미지 (picke 백엔드 `/api/v1/resources/...`) 에만 Bearer 토큰을 첨부한다. + /// 그 외 외부 호스트 (picsum.photos / 카카오 CDN 등) 로는 토큰을 절대 보내지 않는다. + private static let protectedHostSuffixes: Set = [ + "picke.store", + "dev.picke.store", + ] + static func configureAuthorizedDownloader( keychainManager: KeychainManaging ) { let modifier = AnyModifier { request in var req = request - if let token = keychainManager.accessToken(), !token.isEmpty { - req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + + guard + let url = req.url, + let host = url.host?.lowercased(), + protectedHostSuffixes.contains(where: { host == $0 || host.hasSuffix(".\($0)") }), + url.path.hasPrefix("/api/"), + let token = keychainManager.accessToken(), !token.isEmpty + else { + return req } + + req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") return req } From f53ed2acd4479b234d5dddf81ddf07d789e69266 Mon Sep 17 00:00:00 2001 From: Roy Date: Tue, 19 May 2026 23:04:28 +0900 Subject: [PATCH 19/21] =?UTF-8?q?refactor:=20PR=20#34=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=ED=94=BC=EB=93=9C=EB=B0=B1=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=20=E2=80=94=20=EB=8B=A8=EC=9D=BC=20=EC=A7=84=EC=8B=A4?= =?UTF-8?q?=20=EA=B3=B5=EA=B8=89=EC=9B=90=20/=20=EC=A4=91=EB=B3=B5=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 - PhilosopherAvatar.imageAsset 매핑을 Home 모듈 내부 internal extension (PhilosopherAvatar+ImageAsset.swift) 으로 추출 - Entity 가 DesignSystem 에 의존하지 않도록 매핑은 Presentation 계층에 위치 - NewBattleCardView 의 private extension PhilosopherAvatar 제거 → 공용 extension 사용 - PreVoteView.avatarView 의 switch 문 제거 → Image(asset: philosopher.imageAsset) 한 줄 - HeroCardView 의 thumbnailHeight 중복 상수 제거 → init 매개변수로 받고 HeroCarouselView 가 Self.thumbnailHeight 주입 (단일 진실 공급원) --- .../Common/PhilosopherAvatar+ImageAsset.swift | 20 +++++++++++++++++++ .../View/Components/HeroCarouselView.swift | 6 +++--- .../View/Components/NewBattleCardView.swift | 10 ---------- .../Home/Sources/Vote/View/PreVoteView.swift | 9 +-------- 4 files changed, 24 insertions(+), 21 deletions(-) create mode 100644 Projects/Presentation/Home/Sources/Common/PhilosopherAvatar+ImageAsset.swift diff --git a/Projects/Presentation/Home/Sources/Common/PhilosopherAvatar+ImageAsset.swift b/Projects/Presentation/Home/Sources/Common/PhilosopherAvatar+ImageAsset.swift new file mode 100644 index 0000000..0d5ee09 --- /dev/null +++ b/Projects/Presentation/Home/Sources/Common/PhilosopherAvatar+ImageAsset.swift @@ -0,0 +1,20 @@ +// +// PhilosopherAvatar+ImageAsset.swift +// Home +// +// Home 모듈 내부에서만 사용하는 매핑. Entity 가 DesignSystem 에 의존하지 않도록 +// ImageAsset 매핑은 Home 모듈 내부 internal extension 으로 둔다. +// + +import DesignSystem +import Entity + +extension PhilosopherAvatar { + var imageAsset: ImageAsset { + switch self { + case .plato: .avatarPlato + case .sartre: .avatarSartre + case .sunja: .avatarSunja + } + } +} diff --git a/Projects/Presentation/Home/Sources/Main/View/Components/HeroCarouselView.swift b/Projects/Presentation/Home/Sources/Main/View/Components/HeroCarouselView.swift index 4ad31fd..7ebf9b8 100644 --- a/Projects/Presentation/Home/Sources/Main/View/Components/HeroCarouselView.swift +++ b/Projects/Presentation/Home/Sources/Main/View/Components/HeroCarouselView.swift @@ -30,7 +30,8 @@ struct HeroCarouselView: View { HeroCardView( hero: hero, position: index + 1, - total: heroes.count + total: heroes.count, + thumbnailHeight: Self.thumbnailHeight ) .contentShape(Rectangle()) .onTapGesture { onTap(hero) } @@ -57,8 +58,7 @@ struct HeroCardView: View { let hero: HeroBattle let position: Int let total: Int - - private let thumbnailHeight: CGFloat = 220 + let thumbnailHeight: CGFloat var body: some View { VStack(spacing: 0) { diff --git a/Projects/Presentation/Home/Sources/Main/View/Components/NewBattleCardView.swift b/Projects/Presentation/Home/Sources/Main/View/Components/NewBattleCardView.swift index 9c48c2d..d7e10fc 100644 --- a/Projects/Presentation/Home/Sources/Main/View/Components/NewBattleCardView.swift +++ b/Projects/Presentation/Home/Sources/Main/View/Components/NewBattleCardView.swift @@ -161,13 +161,3 @@ extension NewBattleCardView { .overlay(Circle().stroke(.beige50, lineWidth: 1.5)) } } - -private extension PhilosopherAvatar { - var imageAsset: ImageAsset { - switch self { - case .plato: .avatarPlato - case .sartre: .avatarSartre - case .sunja: .avatarSunja - } - } -} diff --git a/Projects/Presentation/Home/Sources/Vote/View/PreVoteView.swift b/Projects/Presentation/Home/Sources/Vote/View/PreVoteView.swift index 148bd98..9ed57d9 100644 --- a/Projects/Presentation/Home/Sources/Vote/View/PreVoteView.swift +++ b/Projects/Presentation/Home/Sources/Vote/View/PreVoteView.swift @@ -227,15 +227,8 @@ extension PreVoteView { .buttonStyle(.plain) } - @ViewBuilder private func avatarView(_ philosopher: PhilosopherAvatar) -> some View { - let asset: ImageAsset = switch philosopher { - case .plato: .avatarPlato - case .sartre: .avatarSartre - case .sunja: .avatarSunja - } - - return Image(asset: asset) + Image(asset: philosopher.imageAsset) .resizable() .scaledToFit() .frame(width: 40, height: 40) From a80ac9d84920984c5ef286cea27e8ef05c867bea Mon Sep 17 00:00:00 2001 From: Roy Date: Tue, 19 May 2026 23:08:16 +0900 Subject: [PATCH 20/21] =?UTF-8?q?fix:=20#4=20fetchPoll=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=EC=9D=84=20state.battle=20=EB=A1=9C=20=EB=A7=A4?= =?UTF-8?q?=ED=95=91=20=E2=80=94=20mock=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EB=85=B8=EC=B6=9C=20=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기존: pollResponse success 시 state.poll 만 갱신, View 는 mock 으로 초기화된 state.battle 만 사용 → 실제 응답이 화면에 반영 안 됨 + share/submit 은 새 pollId 로 동작해 콘텐츠 불일치 - makeBattle(from:fallback:) 헬퍼로 PollDetail → PreVoteBattle 변환: battleId/titlePrefix/titleSuffix/options 매핑, summary/tags/backgroundImageURL 은 API 에 없으므로 fallback (mock) 유지 - 옵션은 displayOrder 정렬된 상태에서 앞 2개를 좌/우 카드로, 부족하면 fallback option 사용. philosopher 매핑은 인덱스 기반 (.plato/.sartre/.sunja) --- .../Sources/Vote/Reducer/PreVoteFeature.swift | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift b/Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift index 265b0a3..9a395e6 100644 --- a/Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift +++ b/Projects/Presentation/Home/Sources/Vote/Reducer/PreVoteFeature.swift @@ -167,6 +167,7 @@ extension PreVoteFeature { switch result { case let .success(poll): state.poll = poll + state.battle = makeBattle(from: poll, fallback: state.battle) case let .failure(error): Log.error("[PreVoteFeature] fetchPoll failed: \(error.localizedDescription)") } @@ -174,6 +175,35 @@ extension PreVoteFeature { } } + /// API 로 받은 PollDetail 을 화면 모델 PreVoteBattle 로 매핑. + /// background/summary/tags 는 응답에 없으므로 fallback (이전 state.battle) 값을 유지한다. + /// 옵션은 displayOrder 순으로 앞에서부터 2개만 좌/우 카드에 매핑. + private func makeBattle( + from poll: PollDetail, + fallback: PreVoteBattle + ) -> PreVoteBattle { + let philosophers: [PhilosopherAvatar] = [.plato, .sartre, .sunja] + let mapped = poll.options.enumerated().map { idx, option in + PreVoteOption( + philosopher: philosophers[safe: idx] ?? .plato, + stance: option.title + ) + } + let leftOption = mapped[safe: 0] ?? fallback.leftOption + let rightOption = mapped[safe: 1] ?? fallback.rightOption + + return PreVoteBattle( + battleId: poll.pollId, + backgroundImageURL: fallback.backgroundImageURL, + tags: fallback.tags, + titleLine1: poll.titlePrefix, + titleLine2: poll.titleSuffix, + summary: fallback.summary, + leftOption: leftOption, + rightOption: rightOption + ) + } + private func handleDelegateAction( state _: inout State, action: DelegateAction @@ -184,3 +214,9 @@ extension PreVoteFeature { } } } + +private extension Array { + subscript(safe index: Int) -> Element? { + indices.contains(index) ? self[index] : nil + } +} From 9bf1ade835fc332a88c0086ffeb8eb529490795a Mon Sep 17 00:00:00 2001 From: Roy Date: Tue, 19 May 2026 23:11:52 +0900 Subject: [PATCH 21/21] =?UTF-8?q?chore:=20KingfisherConfigurator=20?= =?UTF-8?q?=EB=AF=B8=EC=82=AC=EC=9A=A9=20Foundations=20import=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/App/Sources/Application/KingfisherConfigurator.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Projects/App/Sources/Application/KingfisherConfigurator.swift b/Projects/App/Sources/Application/KingfisherConfigurator.swift index c94bff9..7800a6e 100644 --- a/Projects/App/Sources/Application/KingfisherConfigurator.swift +++ b/Projects/App/Sources/Application/KingfisherConfigurator.swift @@ -12,7 +12,6 @@ import Foundation import Kingfisher import DomainInterface -import Foundations enum KingfisherConfigurator { /// 보호 이미지 (picke 백엔드 `/api/v1/resources/...`) 에만 Bearer 토큰을 첨부한다.