diff --git a/Projects/Core/Network/Interface/Sources/Endpoint.swift b/Projects/Core/Network/Interface/Sources/Endpoint.swift index 91725f8d..bdbf47a6 100644 --- a/Projects/Core/Network/Interface/Sources/Endpoint.swift +++ b/Projects/Core/Network/Interface/Sources/Endpoint.swift @@ -16,6 +16,7 @@ public enum FeatureTag: String { case proopPhoto = "ProofPhoto" case notification = "Notification" case poke = "Poke" + case stats = "Stats" case unknown = "Unknown" } diff --git a/Projects/Domain/Stats/Interface/Sources/DTO/StatsResponseDTO.swift b/Projects/Domain/Stats/Interface/Sources/DTO/StatsResponseDTO.swift new file mode 100644 index 00000000..68969b65 --- /dev/null +++ b/Projects/Domain/Stats/Interface/Sources/DTO/StatsResponseDTO.swift @@ -0,0 +1,70 @@ +// +// StatsResponseDTO.swift +// DomainStats +// +// Created by 정지훈 on 2/25/26. +// + +import Foundation + +/// 통계 목록 조회 응답을 디코딩하는 DTO입니다. +public struct StatsResponseDTO: Decodable { + let selectedDate: String + let statsGoals: [StatsGoal] + + struct StatsGoal: Codable { + let goalId: Int64 + let goalName: String + let goalIconType: String + let monthlyTargetCount: Int + let stamp: String + let myStats: Stats + let partnerStats: Stats + + struct Stats: Codable { + let nickname: String + let endCount: Int + let stampColors: [String] + } + } +} + +extension StatsResponseDTO { + /// 통계 목록 응답 DTO를 도메인 `Stats` 엔티티로 변환합니다. + /// + /// ## 사용 예시 + /// ```swift + /// let dto: StatsResponseDTO = ... + /// let stats = dto.toEntity(isInProgress: true) + /// ``` + public func toEntity(isInProgress: Bool) -> Stats? { + guard let firstStats = statsGoals.first else { return nil } + + return Stats( + myNickname: firstStats.myStats.nickname, + partnerNickname: firstStats.partnerStats.nickname, + stats: statsGoals.map { + Stats.StatsItem( + goalId: $0.goalId, + icon: $0.goalIconType, + goalName: $0.goalName, + monthlyCount: $0.monthlyTargetCount, + totalCount: isInProgress ? nil : $0.monthlyTargetCount, + stamp: $0.stamp, + myStamp: .init( + completedCount: $0.myStats.endCount, + stampColors: isInProgress + ? $0.myStats.stampColors.compactMap { Stats.StatsItem.StampColor.init(rawValue: $0) } + : [] + ), + partnerStamp: .init( + completedCount: $0.partnerStats.endCount, + stampColors: isInProgress + ? $0.partnerStats.stampColors.compactMap { Stats.StatsItem.StampColor.init(rawValue: $0) } + : [] + ) + ) + } + ) + } +} diff --git a/Projects/Domain/Stats/Interface/Sources/Endpoint/StatsEndpoint.swift b/Projects/Domain/Stats/Interface/Sources/Endpoint/StatsEndpoint.swift new file mode 100644 index 00000000..5ca8150e --- /dev/null +++ b/Projects/Domain/Stats/Interface/Sources/Endpoint/StatsEndpoint.swift @@ -0,0 +1,55 @@ +// +// StatsEndpoint.swift +// DomainStats +// +// Created by 정지훈 on 2/25/26. +// + +import Foundation + +import CoreNetworkInterface + +/// 통계 목록 조회 API 엔드포인트를 정의합니다. +public enum StatsEndpoint: Endpoint { + case fetchStats(selectedDate: String, status: String) +} + +extension StatsEndpoint { + public var path: String { + switch self { + case .fetchStats: + return "/api/v1/stats" + } + } + + public var method: HTTPMethod { + switch self { + case .fetchStats: + return .get + } + } + + public var headers: [String : String]? { + ["Content-Type": "application/json"] + } + + public var query: [URLQueryItem]? { + switch self { + case let .fetchStats(date, status): + return [ + .init(name: "selectedDate", value: date), + .init(name: "status", value: status), + ] + } + } + + public var body: (any Encodable)? { + switch self { + case .fetchStats: + return nil + } + } + + public var requiresAuth: Bool { true } + public var featureTag: FeatureTag { .stats } +} diff --git a/Projects/Domain/Stats/Interface/Sources/Entity/Stats.swift b/Projects/Domain/Stats/Interface/Sources/Entity/Stats.swift index 3189b8e7..073dd67b 100644 --- a/Projects/Domain/Stats/Interface/Sources/Entity/Stats.swift +++ b/Projects/Domain/Stats/Interface/Sources/Entity/Stats.swift @@ -81,39 +81,53 @@ public struct Stats: Equatable { public let goalName: String public let monthlyCount: Int? public let totalCount: Int? - public let myCompletedCount: Int - public let partnerCompletedCount: Int + public let stamp: String? + public let myStamp: Stamp + public let partnerStamp: Stamp - /// 단일 목표 통계 항목을 생성합니다. - /// - /// ## 사용 예시 - /// ```swift - /// let item = Stats.StatsItem( - /// goalId: 1, - /// icon: "ICON_BOOK", - /// goalName: "독서하기", - /// monthlyCount: 12, - /// totalCount: nil, - /// myCompletedCount: 6, - /// partnerCompletedCount: 2 - /// ) - /// ``` public init( goalId: Int64, icon: String, goalName: String, monthlyCount: Int?, totalCount: Int?, - myCompletedCount: Int, - partnerCompletedCount: Int + stamp: String?, + myStamp: Stamp, + partnerStamp: Stamp ) { self.goalId = goalId self.icon = icon self.goalName = goalName self.monthlyCount = monthlyCount self.totalCount = totalCount - self.myCompletedCount = myCompletedCount - self.partnerCompletedCount = partnerCompletedCount + self.stamp = stamp + self.myStamp = myStamp + self.partnerStamp = partnerStamp + } + + /// 통계 스탬프에서 사용하는 색상 타입입니다. + public enum StampColor: String, Equatable, CaseIterable { + case green400 = "GREEN400" + case blue400 = "BLUE400" + case yellow400 = "YELLOW400" + case pink400 = "PINK400" + case pink300 = "PINK300" + case pink200 = "PINK200" + case orange400 = "ORANGE400" + case purple400 = "PURPLE400" + } + + public struct Stamp: Equatable { + public let completedCount: Int + public let stampColors: [StampColor] + + public init( + completedCount: Int, + stampColors: [StampColor] + ) { + self.completedCount = completedCount + self.stampColors = stampColors + } } } } diff --git a/Projects/Domain/Stats/Interface/Sources/StatsClient.swift b/Projects/Domain/Stats/Interface/Sources/StatsClient.swift index 31582543..32165381 100644 --- a/Projects/Domain/Stats/Interface/Sources/StatsClient.swift +++ b/Projects/Domain/Stats/Interface/Sources/StatsClient.swift @@ -17,14 +17,12 @@ import CoreNetworkInterface /// ## 사용 예시 /// ```swift /// @Dependency(\.statsClient) var statsClient -/// let ongoing = try await statsClient.fetchOngoingStats("2026-02") +/// let ongoing = try await statsClient.fetchStats("2026-02") /// let completed = try await statsClient.fetchCompletedStats("2026-02") /// ``` public struct StatsClient { - /// 진행 중 목표 통계를 조회합니다. - public var fetchOngoingStats: (String) async throws -> Stats - /// 완료된 목표 통계를 조회합니다. - public var fetchCompletedStats: (String) async throws -> Stats + /// 목표 통계를 조회합니다. + public var fetchStats: (String, Bool) async throws -> Stats /// 단일 목표의 상세 통계를 조회합니다. public var fetchStatsDetail: (String) async throws -> StatsDetail @@ -33,8 +31,7 @@ public struct StatsClient { /// ## 사용 예시 /// ```swift /// let client = StatsClient( - /// fetchOngoingStats: { _ in Stats(myNickname: "", partnerNickname: "", stats: []) }, - /// fetchCompletedStats: { _ in Stats(myNickname: "", partnerNickname: "", stats: []) }, + /// fetchStats: { _ in Stats(myNickname: "", partnerNickname: "", stats: []) }, /// fetchStatsDetail: { _ in /// StatsDetail( /// goalId: 1, @@ -56,22 +53,46 @@ public struct StatsClient { /// ) /// ``` public init( - fetchOngoingStats: @escaping (String) async throws -> Stats, - fetchCompletedStats: @escaping (String) async throws -> Stats, + fetchStats: @escaping (String, Bool) async throws -> Stats, fetchStatsDetail: @escaping (String) async throws -> StatsDetail, ) { - self.fetchOngoingStats = fetchOngoingStats - self.fetchCompletedStats = fetchCompletedStats + self.fetchStats = fetchStats self.fetchStatsDetail = fetchStatsDetail } } -// TODO: - API 연동 extension StatsClient: TestDependencyKey { public static var testValue: StatsClient = Self( - fetchOngoingStats: { date in - // assertionFailure("StatsClient.fetchOngoingStats이 구현되지 않았습니다. withDependencies로 mock을 주입하세요.") - // return [] + fetchStats: { date, _ in + assertionFailure("StatsClient.fetchStats이 구현되지 않았습니다. withDependencies로 mock을 주입하세요.") + return Stats( + myNickname: "현수", + partnerNickname: "민정", + stats: [] + ) + }, + fetchStatsDetail: { _ in + assertionFailure("StatsClient.fetchStatsDetail이 구현되지 않았습니다. withDependencies로 mock을 주입하세요.") + return .init( + goalId: 1, + goalName: "", + isCompleted: false, + completedDate: [ ], + summary: .init( + myNickname: "", + partnerNickname: "", + totalCount: 322, + myCompletedCount: 82, + partnerCompltedCount: 211, + repeatCycle: .daily, + startDate: "2026-01-07", + endDate: "2027-01-07" + ) + ) + } + ) + public static var previewValue: StatsClient = Self( + fetchStats: { date, _ in return Stats( myNickname: "현수", partnerNickname: "민정", @@ -82,17 +103,39 @@ extension StatsClient: TestDependencyKey { goalName: "독서하기", monthlyCount: 12, totalCount: nil, - myCompletedCount: 6, - partnerCompletedCount: 2 + stamp: "CLOVER", + myStamp: .init( + completedCount: 5, + stampColors: [ + .pink200, .orange400, .purple400 + ] + ), + partnerStamp: .init( + completedCount: 2, + stampColors: [ + .green400, .orange400, .yellow400 + ] + ) ), .init( goalId: 2, icon: "ICON_DEFUALT", goalName: "요리 해먹기", - monthlyCount: 17, + monthlyCount: 31, totalCount: nil, - myCompletedCount: 12, - partnerCompletedCount: 8 + stamp: "FLOWER", + myStamp: .init( + completedCount: 2, + stampColors: [ + .pink400, .orange400, .blue400 + ] + ), + partnerStamp: .init( + completedCount: 11, + stampColors: [ + .green400, .blue400, .yellow400 + ] + ) ), .init( goalId: 3, @@ -100,8 +143,19 @@ extension StatsClient: TestDependencyKey { goalName: "운동하기", monthlyCount: 31, totalCount: nil, - myCompletedCount: 2, - partnerCompletedCount: 11 + stamp: "MOON", + myStamp: .init( + completedCount: 25, + stampColors: [ + .pink200, .orange400, .purple400 + ] + ), + partnerStamp: .init( + completedCount: 12, + stampColors: [ + .green400, .orange400, .yellow400 + ] + ) ), .init( goalId: 4, @@ -109,61 +163,24 @@ extension StatsClient: TestDependencyKey { goalName: "난나난나", monthlyCount: 15, totalCount: nil, - myCompletedCount: 13, - partnerCompletedCount: 15 - ), - ] - ) - }, - fetchCompletedStats: { _ in - // assertionFailure("StatsClient.fetchCompletedStats이 구현되지 않았습니다. withDependencies로 mock을 주입하세요.") - // return [] - return Stats( - myNickname: "현수", - partnerNickname: "민정", - stats: [ - .init( - goalId: 6, - icon: "ICON_BOOK", - goalName: "독서하기", - monthlyCount: nil, - totalCount: 232, - myCompletedCount: 221, - partnerCompletedCount: 187 - ), - .init( - goalId: 7, - icon: "ICON_DEFUALT", - goalName: "요리 해먹기", - monthlyCount: nil, - totalCount: 68, - myCompletedCount: 23, - partnerCompletedCount: 62 - ), - .init( - goalId: 8, - icon: "ICON_HEALTH", - goalName: "운동하기", - monthlyCount: nil, - totalCount: 5, - myCompletedCount: 5, - partnerCompletedCount: 5 - ), - .init( - goalId: 9, - icon: "ICON_DEFAULT", - goalName: "난나난나", - monthlyCount: nil, - totalCount: 300, - myCompletedCount: 102, - partnerCompletedCount: 203 + stamp: "CLOVER", + myStamp: .init( + completedCount: 13, + stampColors: [ + .pink300, .orange400, .purple400 + ] + ), + partnerStamp: .init( + completedCount: 15, + stampColors: [ + .green400, .orange400, .blue400 + ] + ) ), ] ) }, fetchStatsDetail: { _ in - // assertionFailure("StatsClient.fetchStatsDetail이 구현되지 않았습니다. withDependencies로 mock을 주입하세요.") - // return .init( goalId: 1, goalName: "밥 잘 챙겨먹기", @@ -197,7 +214,7 @@ extension StatsClient: TestDependencyKey { ) ) } - ) + ) } extension DependencyValues { diff --git a/Projects/Domain/Stats/Sources/StatsClient+Live.swift b/Projects/Domain/Stats/Sources/StatsClient+Live.swift new file mode 100644 index 00000000..825f88a1 --- /dev/null +++ b/Projects/Domain/Stats/Sources/StatsClient+Live.swift @@ -0,0 +1,40 @@ +// +// StatsClient+Live.swift +// DomainStats +// +// Created by 정지훈 on 2/25/26. +// + +import ComposableArchitecture +import CoreNetworkInterface +import DomainStatsInterface + +extension StatsClient: @retroactive DependencyKey { + public static let liveValue: StatsClient = live() + + private static func live() -> StatsClient { + @Dependency(\.networkClient) var networkClient + + return StatsClient( + fetchStats: { date, isInProgress in + do { + let status = isInProgress ? "IN_PROGRESS" : "COMPLETED" + let response: StatsResponseDTO = try await networkClient.request( + endpoint: StatsEndpoint.fetchStats(selectedDate: date, status: status) + ) + + guard let stats = response.toEntity(isInProgress: isInProgress) + else { throw NetworkError.invalidResponseError } + + return stats + } catch { + throw error + } + }, + fetchStatsDetail: { _ in + // FIXME: - API 연동 + throw NetworkError.notFoundError + } + ) + } +} diff --git a/Projects/Feature/Stats/Example/Sources/StatsApp.swift b/Projects/Feature/Stats/Example/Sources/StatsApp.swift index 56cee407..efe4574b 100644 --- a/Projects/Feature/Stats/Example/Sources/StatsApp.swift +++ b/Projects/Feature/Stats/Example/Sources/StatsApp.swift @@ -8,6 +8,7 @@ import SwiftUI import ComposableArchitecture +import DomainStats import DomainStatsInterface import FeatureGoalDetail import FeatureGoalDetailInterface @@ -36,7 +37,7 @@ struct StatsApp: App { ) }, withDependencies: { - $0.statsClient = .testValue + $0.statsClient = .previewValue $0.goalDetailFactory = .liveValue $0.makeGoalFactory = .liveValue $0.goalClient = .previewValue diff --git a/Projects/Feature/Stats/Interface/Sources/Stats/StatsReducer.swift b/Projects/Feature/Stats/Interface/Sources/Stats/StatsReducer.swift index 71f9a219..cf554ae3 100644 --- a/Projects/Feature/Stats/Interface/Sources/Stats/StatsReducer.swift +++ b/Projects/Feature/Stats/Interface/Sources/Stats/StatsReducer.swift @@ -41,10 +41,6 @@ public struct StatsReducer { return isOngoing ? ongoingItems : completedItems } - public var hasItems: Bool { - !isLoading && !items.isEmpty - } - public var ongoingItems: [StatsCardItem] = [] public var completedItems: [StatsCardItem] = [] public var ongoingItemsCache: [String: [StatsCardItem]] = [:] diff --git a/Projects/Feature/Stats/Project.swift b/Projects/Feature/Stats/Project.swift index d5fdbf7b..7da3095e 100644 --- a/Projects/Feature/Stats/Project.swift +++ b/Projects/Feature/Stats/Project.swift @@ -23,6 +23,7 @@ let project = Project.makeModule( .feature(interface: .stats), .feature(interface: .goalDetail), .feature(interface: .makeGoal), + .domain(implements: .stats), .domain(interface: .stats), .shared(implements: .designSystem), .external(dependency: .ComposableArchitecture) diff --git a/Projects/Feature/Stats/Sources/Stats/StatsReducer+Impl.swift b/Projects/Feature/Stats/Sources/Stats/StatsReducer+Impl.swift index e8677034..86284d49 100644 --- a/Projects/Feature/Stats/Sources/Stats/StatsReducer+Impl.swift +++ b/Projects/Feature/Stats/Sources/Stats/StatsReducer+Impl.swift @@ -54,7 +54,7 @@ extension StatsReducer { // MARK: - Network case .fetchStats: let isOngoing = state.isOngoing - let month = state.currentMonth.formattedAPIDateString() + let month = state.currentMonth.formattedYearDashMonth if isOngoing, let cachedItems = state.ongoingItemsCache[month] { @@ -66,13 +66,7 @@ extension StatsReducer { return .run { send in do { - let stats: Stats - if isOngoing { - stats = try await statsClient.fetchOngoingStats(month) - } else { - stats = try await statsClient.fetchCompletedStats(month) - } - + let stats = try await statsClient.fetchStats(month, isOngoing) await send(.fetchedStats(stats: stats, month: month)) } catch { await send(.fetchStatsFailed) @@ -88,10 +82,19 @@ extension StatsReducer { goalId: $0.goalId, goalName: $0.goalName, iconImage: GoalIcon(from: $0.icon).image, + stampIcon: .init(statsStamp: $0.stamp), goalCount: goalCount, completionInfos: [ - .init(name: stats.myNickname, count: $0.myCompletedCount), - .init(name: stats.partnerNickname, count: $0.partnerCompletedCount) + .init( + name: stats.myNickname, + count: $0.myStamp.completedCount, + stampColors: $0.myStamp.stampColors.map(\.statsCardStampColor) + ), + .init( + name: stats.partnerNickname, + count: $0.partnerStamp.completedCount, + stampColors: $0.partnerStamp.stampColors.map(\.statsCardStampColor) + ) ] ) } @@ -101,7 +104,7 @@ extension StatsReducer { } // 요청 시점의 탭/월과 현재 상태가 같을 때만 화면을 업데이트합니다. - guard month == state.currentMonth.formattedAPIDateString() else { + guard month == state.currentMonth.formattedYearDashMonth else { return .none } @@ -127,3 +130,27 @@ extension StatsReducer { self.init(reducer: reducer) } } + +private extension Stats.StatsItem.StampColor { + var statsCardStampColor: StatsCardItem.StampColor { + switch self { + case .green400: .green400 + case .blue400: .blue400 + case .yellow400: .yellow400 + case .pink400: .pink400 + case .pink300: .pink300 + case .pink200: .pink200 + case .orange400: .orange400 + case .purple400: .purple400 + } + } +} + +private extension TXVector.Icon { + init(statsStamp: String?) { + self = statsStamp + .map { $0.lowercased() } + .flatMap(Self.init(rawValue:)) + ?? .clover + } +} diff --git a/Projects/Feature/Stats/Sources/Stats/StatsView.swift b/Projects/Feature/Stats/Sources/Stats/StatsView.swift index 414b12ae..8abcc83a 100644 --- a/Projects/Feature/Stats/Sources/Stats/StatsView.swift +++ b/Projects/Feature/Stats/Sources/Stats/StatsView.swift @@ -22,20 +22,21 @@ struct StatsView: View { .padding(.top, store.isOngoing ? 16 : 20) .background(Color.Gray.gray50) - if store.hasItems { + if !store.items.isEmpty { cardList } Spacer() } + .overlay { + if !store.isLoading && store.items.isEmpty { + statsEmptyView + } + } .overlay { if store.isLoading { ProgressView() } - - if !store.hasItems { - statsEmptyView - } } .onAppear { store.send(.onAppear) } .txToast(item: $store.toast) @@ -64,8 +65,8 @@ private extension StatsView { title: store.monthTitle, onTitleTap: { }, isNextDisabled: store.isNextMonthDisabled, - onPrevious: { store.send(.previousMonthTapped)}, - onNext: { store.send(.nextMonthTapped)} + onPrevious: { store.send(.previousMonthTapped) }, + onNext: { store.send(.nextMonthTapped) } ) } else { EmptyView() } } diff --git a/Projects/Shared/DesignSystem/Sources/Components/Card/Stats/Model/StatsCardItem.swift b/Projects/Shared/DesignSystem/Sources/Components/Card/Stats/Model/StatsCardItem.swift index 862320b5..34f5c7ad 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/Card/Stats/Model/StatsCardItem.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/Card/Stats/Model/StatsCardItem.swift @@ -7,36 +7,79 @@ import SwiftUI +/// 통계 카드 UI를 구성하기 위한 목표별 표시 모델입니다. public struct StatsCardItem: Equatable { + /// 통계 카드에서 사용하는 스탬프 색상 타입입니다. + public enum StampColor: Equatable { + case green400 + case blue400 + case yellow400 + case pink400 + case pink300 + case pink200 + case orange400 + case purple400 + } + public let goalId: Int64 public let goalName: String public let iconImage: Image + public let stampIcon: TXVector.Icon public let goalCount: Int public let completionInfos: [CompletionInfo] + /// 사용자별 완료 횟수와 스탬프 색상 정보를 표현하는 모델입니다. public struct CompletionInfo: Equatable { public let name: String public let count: Int + public let stampColors: [StampColor] + /// 사용자 완료 정보를 생성합니다. + /// + /// ## 사용 예시 + /// ```swift + /// let info = StatsCardItem.CompletionInfo( + /// name: "민정", + /// count: 10, + /// stampColors: [.green400, .blue400] + /// ) + /// ``` public init( name: String, - count: Int + count: Int, + stampColors: [StampColor] ) { self.name = name self.count = count + self.stampColors = stampColors } } + /// 통계 카드 표시 모델을 생성합니다. + /// + /// ## 사용 예시 + /// ```swift + /// let item = StatsCardItem( + /// goalId: 1, + /// goalName: "독서하기", + /// iconImage: .Icon.Illustration.book, + /// stampIcon: .clover, + /// goalCount: 30, + /// completionInfos: [] + /// ) + /// ``` public init( goalId: Int64, goalName: String, iconImage: Image, + stampIcon: TXVector.Icon, goalCount: Int, completionInfos: [CompletionInfo] ) { self.goalId = goalId self.goalName = goalName self.iconImage = iconImage + self.stampIcon = stampIcon self.goalCount = goalCount self.completionInfos = completionInfos } diff --git a/Projects/Shared/DesignSystem/Sources/Components/Card/Stats/View/StatsCardView.swift b/Projects/Shared/DesignSystem/Sources/Components/Card/Stats/View/StatsCardView.swift index 67c955fd..9e190273 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/Card/Stats/View/StatsCardView.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/Card/Stats/View/StatsCardView.swift @@ -27,7 +27,6 @@ public struct StatsCardView: View { repeating: GridItem(.flexible()), count: Constants.gridColumnCount ) - private let icon = TXVector.Icon.allCases.randomElement() ?? .clover private var onTap: (Int64) -> Void @@ -116,12 +115,14 @@ private extension StatsCardView { LazyVGrid(columns: columns, spacing: Constants.gridSpacing) { ForEach(0.. Color? { + guard indices.contains(index) else { return nil } + return self[index].color + } +} + +private extension StatsCardItem.StampColor { + var color: Color { + switch self { + case .green400: + return Color.Chromatic.green400 + case .blue400: + return Color.Chromatic.blue400 + case .yellow400: + return Color.Chromatic.yellow400 + case .pink400: + return Color.Chromatic.pink400 + case .pink300: + return Color.Chromatic.pink300 + case .pink200: + return Color.Chromatic.pink200 + case .orange400: + return Color.Chromatic.orange400 + case .purple400: + return Color.Chromatic.purple400 + } + } +} + // MARK: - Constants private extension StatsCardView { enum Constants { @@ -151,14 +182,6 @@ private extension StatsCardView { static let gridSpacing: CGFloat = 4 static let iconSize: CGFloat = 18 static let cellPadding: CGFloat = 16 - static let iconColors: [Color] = [ - Color.Chromatic.blue400, - Color.Chromatic.green400, - Color.Chromatic.pink400, - Color.Chromatic.yellow400, - Color.Chromatic.orange400, - Color.Chromatic.purple400 - ] } } @@ -169,10 +192,11 @@ private extension StatsCardView { goalId: 1, goalName: "목표이름", iconImage: .Icon.Illustration.book, + stampIcon: .clover, goalCount: 30, completionInfos: [ - .init(name: "민정", count: 10), - .init(name: "현수", count: 20) + .init(name: "민정", count: 10, stampColors: [.green400, .blue400]), + .init(name: "현수", count: 20, stampColors: [.pink400, .orange400]) ] ), isOngoing: true, @@ -184,10 +208,11 @@ private extension StatsCardView { goalId: 2, goalName: "목표이름", iconImage: .Icon.Illustration.book, + stampIcon: .flower, goalCount: 20, completionInfos: [ - .init(name: "민정", count: 3), - .init(name: "현수", count: 10) + .init(name: "민정", count: 3, stampColors: []), + .init(name: "현수", count: 10, stampColors: []) ] ), isOngoing: false,