From 594600946ba5724eff63e028e7a31b689eea4d6f Mon Sep 17 00:00:00 2001 From: Jihun <75370733+jihun32@users.noreply.github.com> Date: Sun, 10 May 2026 21:47:30 +0900 Subject: [PATCH 01/44] =?UTF-8?q?refactor:=20Feature,=20Domain=20=EA=B3=B5?= =?UTF-8?q?=ED=86=B5=20=ED=83=80=EC=9E=85=20=EB=B6=84=EB=A6=AC=20-=20[TWI-?= =?UTF-8?q?73]=20(#279)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: DomainCommon, FeatureCommon 모듈 생성 - #278 * refactor: 공통 타입들 DomainCommon, FeatureCommon으로 책임 분리 - #278 * refactor: common 분리에 영향받는 파일들 수정 - #278 * refactor: SharedDesignSystem에 있던 임시 TXItem 구현체 Feature에 맞게 분리 - #278 * refactor: TXItem 구현체 Feature분리에 따른 영향 범위 수정 - #278 * refactor: DomainStats common 의존성 제거 - #278 * refactor: RepeatCycle+Text재활용 리뷰 반영 - #278 --------- Co-authored-by: jihun --- .../Common/Interface/Sources/GoalStatus.swift | 18 ++++++ .../Interface/Sources/RepeatCycle.swift | 18 ++++++ .../Common/Interface/Sources/StampColor.swift | 23 +++++++ Projects/Domain/Common/Project.swift | 20 ++++++ Projects/Domain/Common/Sources/Source.swift | 8 +++ .../DTO/DetailGoalListResponseDTO.swift | 3 +- .../Sources/DTO/GoalCreateResponseDTO.swift | 3 +- .../Sources/DTO/GoalEditListResponseDTO.swift | 3 +- .../Sources/DTO/GoalListResponseDTO.swift | 5 +- .../Goal/Interface/Sources/Entity/Goal.swift | 17 +---- .../Interface/Sources/Entity/GoalDetail.swift | 5 +- Projects/Domain/Goal/Project.swift | 2 + .../DTO/StatsDetailSummaryResponseDTO.swift | 1 + .../Sources/DTO/StatsResponseDTO.swift | 5 +- .../Interface/Sources/Entity/Stats.swift | 20 +----- .../Sources/Entity/StatsDetail.swift | 3 +- .../Interface/Sources/GoalDropList.swift | 23 +++++++ .../Interface/Sources/RepeatCycle+Text.swift | 22 +++++++ Projects/Feature/Common/Project.swift | 25 ++++++++ Projects/Feature/Common/Sources/Source.swift | 8 +++ .../Extensions/GoalRepeatCycle+Text.swift | 18 ------ .../Sources/Goal/EditGoalListReducer.swift | 1 + Projects/Feature/Home/Project.swift | 3 + .../Goal/EditGoalListReducer+Impl.swift | 1 + .../Home/Sources/Goal/EditGoalListView.swift | 1 + .../Interface/Sources/GoalCategory.swift | 3 +- .../Interface/Sources/MakeGoalReducer.swift | 20 ++---- .../Interface/Sources/PeriodItem.swift | 37 +++++++++++ Projects/Feature/MakeGoal/Project.swift | 3 + .../Sources/MakeGoalReducer+Impl.swift | 9 +-- .../MakeGoal/Sources/MakeGoalView.swift | 6 +- .../Sources/Detail/StatsDetailReducer.swift | 1 + .../Sources/Stats/StatsReducer.swift | 1 + .../Sources/Stats/StatsTopTabItem.swift | 21 +++++++ Projects/Feature/Stats/Project.swift | 4 ++ .../Detail/StatsDetailReducer+Impl.swift | 1 + .../Sources/Detail/StatsDetailView.swift | 1 + .../Detail/StatsRepeatCycle+Text.swift | 18 ------ .../Sources/Stats/StatsReducer+Impl.swift | 4 +- .../Components/Dropdown/TXDropdown.swift | 22 +++++-- .../Sources/Components/Dropdown/TXItem.swift | 63 ------------------- .../Sources/Components/Tab/TXTab.swift | 9 ++- .../Components/Tab/TopBar/TXTopTabBar.swift | 20 +++++- Tuist/ProjectDescriptionHelpers/Module.swift | 2 + 44 files changed, 321 insertions(+), 180 deletions(-) create mode 100644 Projects/Domain/Common/Interface/Sources/GoalStatus.swift create mode 100644 Projects/Domain/Common/Interface/Sources/RepeatCycle.swift create mode 100644 Projects/Domain/Common/Interface/Sources/StampColor.swift create mode 100644 Projects/Domain/Common/Project.swift create mode 100644 Projects/Domain/Common/Sources/Source.swift create mode 100644 Projects/Feature/Common/Interface/Sources/GoalDropList.swift create mode 100644 Projects/Feature/Common/Interface/Sources/RepeatCycle+Text.swift create mode 100644 Projects/Feature/Common/Project.swift create mode 100644 Projects/Feature/Common/Sources/Source.swift delete mode 100644 Projects/Feature/Home/Interface/Sources/Extensions/GoalRepeatCycle+Text.swift create mode 100644 Projects/Feature/MakeGoal/Interface/Sources/PeriodItem.swift create mode 100644 Projects/Feature/Stats/Interface/Sources/Stats/StatsTopTabItem.swift delete mode 100644 Projects/Feature/Stats/Sources/Detail/StatsRepeatCycle+Text.swift delete mode 100644 Projects/Shared/DesignSystem/Sources/Components/Dropdown/TXItem.swift diff --git a/Projects/Domain/Common/Interface/Sources/GoalStatus.swift b/Projects/Domain/Common/Interface/Sources/GoalStatus.swift new file mode 100644 index 00000000..6fd13c41 --- /dev/null +++ b/Projects/Domain/Common/Interface/Sources/GoalStatus.swift @@ -0,0 +1,18 @@ +// +// GoalStatus.swift +// DomainCommonInterface +// +// Created by 정지훈 on 4/20/26. +// + +import Foundation + +/// 목표 진행 상태를 나타내는 공통 비즈니스 enum입니다. +/// +/// 특정 Feature의 표시 문구와 분리하고, DTO 매핑과 도메인 엔티티에서 +/// 동일한 상태 값을 재사용하기 위해 `DomainCommonInterface`에서 관리합니다. +public enum GoalStatus: String, Equatable { + case notStarted = "NOT_STARTED" + case inProgressed = "IN_PROGRESSED" + case completed = "COMPLETED" +} diff --git a/Projects/Domain/Common/Interface/Sources/RepeatCycle.swift b/Projects/Domain/Common/Interface/Sources/RepeatCycle.swift new file mode 100644 index 00000000..1bb8c5ed --- /dev/null +++ b/Projects/Domain/Common/Interface/Sources/RepeatCycle.swift @@ -0,0 +1,18 @@ +// +// RepeatCycle.swift +// DomainCommonInterface +// +// Created by 정지훈 on 4/20/26. +// + +import Foundation + +/// 목표 반복 주기를 나타내는 공통 비즈니스 enum입니다. +/// +/// `Goal`, `Stats`처럼 동일한 반복 주기 의미를 공유하는 도메인 모델이 +/// 하나의 타입만 참조하도록 `DomainCommonInterface`에서 소유합니다. +public enum RepeatCycle: String, Equatable { + case daily = "DAILY" + case weekly = "WEEKLY" + case monthly = "MONTHLY" +} diff --git a/Projects/Domain/Common/Interface/Sources/StampColor.swift b/Projects/Domain/Common/Interface/Sources/StampColor.swift new file mode 100644 index 00000000..9c0416dd --- /dev/null +++ b/Projects/Domain/Common/Interface/Sources/StampColor.swift @@ -0,0 +1,23 @@ +// +// StampColor.swift +// DomainCommonInterface +// +// Created by 정지훈 on 4/20/26. +// + +import Foundation + +/// 통계 스탬프 색상을 나타내는 공통 비즈니스 enum입니다. +/// +/// API 응답과 도메인 모델에서는 이 타입을 사용하고, 실제 렌더링 색상으로의 변환은 +/// Feature 또는 DesignSystem 경계에서 처리합니다. +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" +} diff --git a/Projects/Domain/Common/Project.swift b/Projects/Domain/Common/Project.swift new file mode 100644 index 00000000..3f10a1bd --- /dev/null +++ b/Projects/Domain/Common/Project.swift @@ -0,0 +1,20 @@ +import ProjectDescription +import ProjectDescriptionHelpers + +let project = Project.makeModule( + name: Module.Domain.name + Module.Domain.common.rawValue, + targets: [ + .domain( + interface: .common, + config: .init() + ), + .domain( + implements: .common, + config: .init( + dependencies: [ + .domain(interface: .common) + ] + ) + ) + ] +) diff --git a/Projects/Domain/Common/Sources/Source.swift b/Projects/Domain/Common/Sources/Source.swift new file mode 100644 index 00000000..a1095a1a --- /dev/null +++ b/Projects/Domain/Common/Sources/Source.swift @@ -0,0 +1,8 @@ +// +// Source.swift +// +// +// Created by Jihun on 12/20/25. +// + +/// Remove Or EditME: Remove Or Edit diff --git a/Projects/Domain/Goal/Interface/Sources/DTO/DetailGoalListResponseDTO.swift b/Projects/Domain/Goal/Interface/Sources/DTO/DetailGoalListResponseDTO.swift index f1bc6aa0..e516ba90 100644 --- a/Projects/Domain/Goal/Interface/Sources/DTO/DetailGoalListResponseDTO.swift +++ b/Projects/Domain/Goal/Interface/Sources/DTO/DetailGoalListResponseDTO.swift @@ -6,6 +6,7 @@ // import Foundation +import DomainCommonInterface /// 날짜별 목표 인증 목록 응답 DTO입니다. public struct DetailGoalListResponseDTO: Decodable { @@ -67,7 +68,7 @@ public extension DetailGoalListResponseDTO { createdAt: $0.uploadedAt ) }, - status: Goal.Status(rawValue: photolog.goalStatus) + status: GoalStatus(rawValue: photolog.goalStatus) ) } ) diff --git a/Projects/Domain/Goal/Interface/Sources/DTO/GoalCreateResponseDTO.swift b/Projects/Domain/Goal/Interface/Sources/DTO/GoalCreateResponseDTO.swift index 28914c21..151e9bbd 100644 --- a/Projects/Domain/Goal/Interface/Sources/DTO/GoalCreateResponseDTO.swift +++ b/Projects/Domain/Goal/Interface/Sources/DTO/GoalCreateResponseDTO.swift @@ -6,6 +6,7 @@ // import Foundation +import DomainCommonInterface /// 목표 생성 응답 DTO입니다. public struct GoalCreateResponseDTO: Decodable { @@ -42,7 +43,7 @@ public extension GoalCreateResponseDTO { imageURL: nil, emoji: nil ), - repeatCycle: Goal.RepeatCycle(rawValue: response.repeatCycle), + repeatCycle: RepeatCycle(rawValue: response.repeatCycle), repeatCount: response.repeatCount, startDate: response.startDate, endDate: response.endDate diff --git a/Projects/Domain/Goal/Interface/Sources/DTO/GoalEditListResponseDTO.swift b/Projects/Domain/Goal/Interface/Sources/DTO/GoalEditListResponseDTO.swift index a473eb65..c3cd7518 100644 --- a/Projects/Domain/Goal/Interface/Sources/DTO/GoalEditListResponseDTO.swift +++ b/Projects/Domain/Goal/Interface/Sources/DTO/GoalEditListResponseDTO.swift @@ -6,6 +6,7 @@ // import Foundation +import DomainCommonInterface public struct GoalEditListResponseDTO: Decodable { public let goals: [GoalEditResponseDTO] @@ -29,7 +30,7 @@ extension GoalEditListResponseDTO { title: $0.goalName, myVerification: nil, yourVerification: nil, - repeatCycle: Goal.RepeatCycle(rawValue: $0.repeatCycle), + repeatCycle: RepeatCycle(rawValue: $0.repeatCycle), startDate: $0.startDate, endDate: $0.endDate ) diff --git a/Projects/Domain/Goal/Interface/Sources/DTO/GoalListResponseDTO.swift b/Projects/Domain/Goal/Interface/Sources/DTO/GoalListResponseDTO.swift index 7f62beb6..d763a8cd 100644 --- a/Projects/Domain/Goal/Interface/Sources/DTO/GoalListResponseDTO.swift +++ b/Projects/Domain/Goal/Interface/Sources/DTO/GoalListResponseDTO.swift @@ -6,6 +6,7 @@ // import Foundation +import DomainCommonInterface /// 목표 목록 API 응답 DTO입니다. /// @@ -67,11 +68,11 @@ public extension GoalListResponseDTO { imageURL: $0.partnerVerification?.imageUrl, emoji: $0.partnerVerification?.reaction ), - repeatCycle: $0.repeatCycle.flatMap { Goal.RepeatCycle(rawValue: $0) }, + repeatCycle: $0.repeatCycle.flatMap(RepeatCycle.init(rawValue:)), repeatCount: $0.repeatCount, startDate: $0.startDate, endDate: $0.endDate, - status: .init(rawValue: $0.goalStatus) + status: GoalStatus(rawValue: $0.goalStatus) ) } diff --git a/Projects/Domain/Goal/Interface/Sources/Entity/Goal.swift b/Projects/Domain/Goal/Interface/Sources/Entity/Goal.swift index 387a0a02..16cbb436 100644 --- a/Projects/Domain/Goal/Interface/Sources/Entity/Goal.swift +++ b/Projects/Domain/Goal/Interface/Sources/Entity/Goal.swift @@ -6,6 +6,7 @@ // import Foundation +import DomainCommonInterface public struct GoalList: Equatable { public let hasEverRegisteredGoal: Bool @@ -33,18 +34,6 @@ public struct GoalList: Equatable { /// ) /// ``` public struct Goal: Equatable { - public enum RepeatCycle: String, Equatable { - case daily = "DAILY" - case weekly = "WEEKLY" - case monthly = "MONTHLY" - } - - public enum Status: String, Equatable { - case notStarted = "NOT_STARTED" - case inProgressed = "IN_PROGRESSED" - case completed = "COMPLETED" - } - public let id: Int64 public let goalIcon: String public let title: String @@ -54,7 +43,7 @@ public struct Goal: Equatable { public let repeatCount: Int? public let startDate: String? public let endDate: String? - public let status: Status + public let status: GoalStatus /// 목표 인증 상태를 나타내는 모델입니다. /// @@ -119,7 +108,7 @@ public struct Goal: Equatable { repeatCount: Int? = nil, startDate: String? = nil, endDate: String? = nil, - status: Status? = nil + status: GoalStatus? = nil ) { self.id = id self.goalIcon = goalIcon diff --git a/Projects/Domain/Goal/Interface/Sources/Entity/GoalDetail.swift b/Projects/Domain/Goal/Interface/Sources/Entity/GoalDetail.swift index 043e0100..a297391e 100644 --- a/Projects/Domain/Goal/Interface/Sources/Entity/GoalDetail.swift +++ b/Projects/Domain/Goal/Interface/Sources/Entity/GoalDetail.swift @@ -6,6 +6,7 @@ // import SwiftUI +import DomainCommonInterface /// 목표 상세 정보를 나타내는 모델입니다. /// @@ -57,7 +58,7 @@ public struct GoalDetail: Equatable { /// ``` public struct CompletedGoal: Equatable { public let goalName: String - public let status: Goal.Status + public let status: GoalStatus public let myPhotoLog: PhotoLog? public let yourPhotoLog: PhotoLog? @@ -65,7 +66,7 @@ public struct GoalDetail: Equatable { goalName: String, myPhotoLog: PhotoLog?, yourPhotoLog: PhotoLog?, - status: Goal.Status? = nil + status: GoalStatus? = nil ) { self.goalName = goalName self.status = status ?? .notStarted diff --git a/Projects/Domain/Goal/Project.swift b/Projects/Domain/Goal/Project.swift index 457b5ad2..98749904 100644 --- a/Projects/Domain/Goal/Project.swift +++ b/Projects/Domain/Goal/Project.swift @@ -8,6 +8,7 @@ let project = Project.makeModule( interface: .goal, config: .init( dependencies: [ + .domain(interface: .common), .core(interface: .network), .external(dependency: .ComposableArchitecture) ] @@ -17,6 +18,7 @@ let project = Project.makeModule( implements: .goal, config: .init( dependencies: [ + .domain(interface: .common), .core(interface: .network), .domain(interface: .goal), .external(dependency: .ComposableArchitecture) diff --git a/Projects/Domain/Stats/Interface/Sources/DTO/StatsDetailSummaryResponseDTO.swift b/Projects/Domain/Stats/Interface/Sources/DTO/StatsDetailSummaryResponseDTO.swift index bca20d33..d1da20c8 100644 --- a/Projects/Domain/Stats/Interface/Sources/DTO/StatsDetailSummaryResponseDTO.swift +++ b/Projects/Domain/Stats/Interface/Sources/DTO/StatsDetailSummaryResponseDTO.swift @@ -6,6 +6,7 @@ // import Foundation +import DomainCommonInterface public struct StatsDetailSummaryResponseDTO: Decodable { let myNickname: String diff --git a/Projects/Domain/Stats/Interface/Sources/DTO/StatsResponseDTO.swift b/Projects/Domain/Stats/Interface/Sources/DTO/StatsResponseDTO.swift index 974a4194..b72182e8 100644 --- a/Projects/Domain/Stats/Interface/Sources/DTO/StatsResponseDTO.swift +++ b/Projects/Domain/Stats/Interface/Sources/DTO/StatsResponseDTO.swift @@ -6,6 +6,7 @@ // import CoreNetworkInterface +import DomainCommonInterface /// 통계 목록 조회 응답을 디코딩하는 DTO입니다. public struct StatsResponseDTO: Decodable { @@ -61,13 +62,13 @@ extension StatsResponseDTO { myStamp: .init( completedCount: $0.myStats.endCount, stampColors: isInProgress - ? $0.myStats.stampColors.compactMap { Stats.StatsItem.StampColor.init(rawValue: $0) } + ? $0.myStats.stampColors.compactMap { StampColor(rawValue: $0) } : [] ), partnerStamp: .init( completedCount: $0.partnerStats.endCount, stampColors: isInProgress - ? $0.partnerStats.stampColors.compactMap { Stats.StatsItem.StampColor.init(rawValue: $0) } + ? $0.partnerStats.stampColors.compactMap { StampColor(rawValue: $0) } : [] ) ) diff --git a/Projects/Domain/Stats/Interface/Sources/Entity/Stats.swift b/Projects/Domain/Stats/Interface/Sources/Entity/Stats.swift index 073dd67b..84195988 100644 --- a/Projects/Domain/Stats/Interface/Sources/Entity/Stats.swift +++ b/Projects/Domain/Stats/Interface/Sources/Entity/Stats.swift @@ -6,6 +6,7 @@ // import Foundation +import DomainCommonInterface /// 통계 화면에서 사용하는 사용자별 목표 달성 통계 모델입니다. /// @@ -20,13 +21,6 @@ import Foundation /// ) /// ``` public struct Stats: Equatable { - /// 목표 반복 주기 타입입니다. - public enum RepeatCycle: String, Equatable { - case daily = "DAILY" - case weekly = "WEEKLY" - case monthly = "MONTHLY" - } - public let myNickname: String public let partnerNickname: String public let stats: [StatsItem] @@ -105,18 +99,6 @@ public struct Stats: Equatable { 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] diff --git a/Projects/Domain/Stats/Interface/Sources/Entity/StatsDetail.swift b/Projects/Domain/Stats/Interface/Sources/Entity/StatsDetail.swift index 37c32cf4..584f32c1 100644 --- a/Projects/Domain/Stats/Interface/Sources/Entity/StatsDetail.swift +++ b/Projects/Domain/Stats/Interface/Sources/Entity/StatsDetail.swift @@ -6,6 +6,7 @@ // import Foundation +import DomainCommonInterface /// 통계 상세 화면에서 사용하는 목표 상세 정보 모델입니다. /// @@ -31,7 +32,7 @@ public struct StatsDetail: Equatable { public let totalCount: Int public let myCompletedCount: Int public let partnerCompltedCount: Int - public let repeatCycle: Stats.RepeatCycle + public let repeatCycle: RepeatCycle public let startDate: String public let endDate: String? } diff --git a/Projects/Feature/Common/Interface/Sources/GoalDropList.swift b/Projects/Feature/Common/Interface/Sources/GoalDropList.swift new file mode 100644 index 00000000..77687c03 --- /dev/null +++ b/Projects/Feature/Common/Interface/Sources/GoalDropList.swift @@ -0,0 +1,23 @@ +// +// GoalDropList.swift +// FeatureCommonInterface +// +// Created by 정지훈 on 4/20/26. +// + +import SharedDesignSystem + +/// 목표 편집/상세 화면에서 사용하는 드롭다운 액션 타입입니다. +public enum GoalDropList: TXItem { + case edit + case finish + case delete + + public var title: String { + switch self { + case .edit: return "수정하기" + case .finish: return "끝내기" + case .delete: return "삭제하기" + } + } +} diff --git a/Projects/Feature/Common/Interface/Sources/RepeatCycle+Text.swift b/Projects/Feature/Common/Interface/Sources/RepeatCycle+Text.swift new file mode 100644 index 00000000..41801f14 --- /dev/null +++ b/Projects/Feature/Common/Interface/Sources/RepeatCycle+Text.swift @@ -0,0 +1,22 @@ +// +// RepeatCycle+Text.swift +// FeatureCommonInterface +// +// Created by 정지훈 on 4/20/26. +// + +import DomainCommonInterface + +public extension RepeatCycle { + /// 반복 주기의 현재 표시 문구를 반환합니다. + /// + /// 공통 도메인 값을 여러 Feature에서 같은 방식으로 보여주기 위한 + /// 단기 표시 정책이며, 실제 문자열 소유 책임은 Domain이 아니라 Feature에 둡니다. + var text: String { + switch self { + case .daily: "매일" + case .weekly: "매주" + case .monthly: "매월" + } + } +} diff --git a/Projects/Feature/Common/Project.swift b/Projects/Feature/Common/Project.swift new file mode 100644 index 00000000..5e3727a8 --- /dev/null +++ b/Projects/Feature/Common/Project.swift @@ -0,0 +1,25 @@ +import ProjectDescription +import ProjectDescriptionHelpers + +let project = Project.makeModule( + name: Module.Feature.name + Module.Feature.common.rawValue, + targets: [ + .feature( + interface: .common, + config: .init( + dependencies: [ + .domain(interface: .common), + .shared(implements: .designSystem) + ] + ) + ), + .feature( + implements: .common, + config: .init( + dependencies: [ + .feature(interface: .common) + ] + ) + ) + ] +) diff --git a/Projects/Feature/Common/Sources/Source.swift b/Projects/Feature/Common/Sources/Source.swift new file mode 100644 index 00000000..a1095a1a --- /dev/null +++ b/Projects/Feature/Common/Sources/Source.swift @@ -0,0 +1,8 @@ +// +// Source.swift +// +// +// Created by Jihun on 12/20/25. +// + +/// Remove Or EditME: Remove Or Edit diff --git a/Projects/Feature/Home/Interface/Sources/Extensions/GoalRepeatCycle+Text.swift b/Projects/Feature/Home/Interface/Sources/Extensions/GoalRepeatCycle+Text.swift deleted file mode 100644 index 2ef10389..00000000 --- a/Projects/Feature/Home/Interface/Sources/Extensions/GoalRepeatCycle+Text.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// GoalRepeatCycle+Text.swift -// FeatureHome -// -// Created by Jihun on 2/6/26. -// - -import DomainGoalInterface - -extension Goal.RepeatCycle { - public var text: String { - switch self { - case .daily: return "매일" - case .weekly: return "매주" - case .monthly: return "매월" - } - } -} diff --git a/Projects/Feature/Home/Interface/Sources/Goal/EditGoalListReducer.swift b/Projects/Feature/Home/Interface/Sources/Goal/EditGoalListReducer.swift index 72e95716..abbe7eb5 100644 --- a/Projects/Feature/Home/Interface/Sources/Goal/EditGoalListReducer.swift +++ b/Projects/Feature/Home/Interface/Sources/Goal/EditGoalListReducer.swift @@ -8,6 +8,7 @@ import Foundation import ComposableArchitecture +import FeatureCommonInterface import SharedDesignSystem import SharedUtil import SwiftUI diff --git a/Projects/Feature/Home/Project.swift b/Projects/Feature/Home/Project.swift index a936a5b3..ee379c1e 100644 --- a/Projects/Feature/Home/Project.swift +++ b/Projects/Feature/Home/Project.swift @@ -10,6 +10,7 @@ let project = Project.makeModule( dependencies: [ .domain(interface: .photoLog), .domain(interface: .goal), + .feature(interface: .common), .feature(interface: .proofPhoto), .feature(interface: .goalDetail), .feature(interface: .notification), @@ -29,6 +30,7 @@ let project = Project.makeModule( .domain(interface: .notification), .domain(interface: .photoLog), .domain(interface: .goal), + .feature(interface: .common), .feature(interface: .proofPhoto), .feature(interface: .goalDetail), .feature(interface: .notification), @@ -62,6 +64,7 @@ let project = Project.makeModule( example: .home, config: .init( dependencies: [ + .feature(interface: .common), .feature(implements: .home), .feature(interface: .home), .domain(interface: .goal), diff --git a/Projects/Feature/Home/Sources/Goal/EditGoalListReducer+Impl.swift b/Projects/Feature/Home/Sources/Goal/EditGoalListReducer+Impl.swift index 889f01ea..acb49c4d 100644 --- a/Projects/Feature/Home/Sources/Goal/EditGoalListReducer+Impl.swift +++ b/Projects/Feature/Home/Sources/Goal/EditGoalListReducer+Impl.swift @@ -10,6 +10,7 @@ import SwiftUI import ComposableArchitecture import DomainGoalInterface +import FeatureCommonInterface import FeatureHomeInterface import SharedDesignSystem import SharedUtil diff --git a/Projects/Feature/Home/Sources/Goal/EditGoalListView.swift b/Projects/Feature/Home/Sources/Goal/EditGoalListView.swift index 89bc8703..465f4759 100644 --- a/Projects/Feature/Home/Sources/Goal/EditGoalListView.swift +++ b/Projects/Feature/Home/Sources/Goal/EditGoalListView.swift @@ -8,6 +8,7 @@ import SwiftUI import ComposableArchitecture +import FeatureCommonInterface import FeatureHomeInterface import SharedDesignSystem diff --git a/Projects/Feature/MakeGoal/Interface/Sources/GoalCategory.swift b/Projects/Feature/MakeGoal/Interface/Sources/GoalCategory.swift index 3cf6faf4..e21966fe 100644 --- a/Projects/Feature/MakeGoal/Interface/Sources/GoalCategory.swift +++ b/Projects/Feature/MakeGoal/Interface/Sources/GoalCategory.swift @@ -7,6 +7,7 @@ import SwiftUI +import DomainCommonInterface import DomainGoalInterface import SharedDesignSystem @@ -52,7 +53,7 @@ extension GoalCategory { } } - public var repeatCycle: Goal.RepeatCycle { + public var repeatCycle: RepeatCycle { switch self { case .custom: .daily case .health: .weekly diff --git a/Projects/Feature/MakeGoal/Interface/Sources/MakeGoalReducer.swift b/Projects/Feature/MakeGoal/Interface/Sources/MakeGoalReducer.swift index 36c7ff55..ba00f39b 100644 --- a/Projects/Feature/MakeGoal/Interface/Sources/MakeGoalReducer.swift +++ b/Projects/Feature/MakeGoal/Interface/Sources/MakeGoalReducer.swift @@ -8,7 +8,9 @@ import SwiftUI import ComposableArchitecture +import DomainCommonInterface import DomainGoalInterface +import FeatureCommonInterface import SharedDesignSystem import SharedUtil @@ -39,15 +41,15 @@ public struct MakeGoalReducer { public let weeklyMaximumPeriodCount = 6 public let monthlyMaximumPeriodCount = 25 public let icons: [GoalIcon] = GoalIcon.allCases - public let dailyPeriodText: String = Goal.RepeatCycle.daily.text - public let weeklyPeriodText: String = Goal.RepeatCycle.weekly.text - public let monthlyPeriodText: String = Goal.RepeatCycle.monthly.text + public let dailyPeriodText: String = RepeatCycle.daily.text + public let weeklyPeriodText: String = RepeatCycle.weekly.text + public let monthlyPeriodText: String = RepeatCycle.monthly.text public var mode: Mode public var editingGoalId: Int64? public var category: GoalCategory public var goalTitle: String - public var selectedPeriod: Goal.RepeatCycle + public var selectedPeriod: RepeatCycle public var weeklyPeriodCount: Int = 1 public var monthlyPeriodCount: Int = 1 public var startDate: TXCalendarDate @@ -258,13 +260,3 @@ public extension MakeGoalReducer.State.Mode { } } } - -extension Goal.RepeatCycle { - public var text: String { - switch self { - case .daily: return "매일" - case .weekly: return "매주" - case .monthly: return "매월" - } - } -} diff --git a/Projects/Feature/MakeGoal/Interface/Sources/PeriodItem.swift b/Projects/Feature/MakeGoal/Interface/Sources/PeriodItem.swift new file mode 100644 index 00000000..9cdf85eb --- /dev/null +++ b/Projects/Feature/MakeGoal/Interface/Sources/PeriodItem.swift @@ -0,0 +1,37 @@ +// +// PeriodItem.swift +// FeatureMakeGoalInterface +// +// Created by 정지훈 on 4/20/26. +// + +import DomainCommonInterface +import FeatureCommonInterface +import SharedDesignSystem + +/// 목표 생성/수정 화면에서 사용하는 반복 주기 탭 아이템입니다. +public enum PeriodItem: TXItem { + case daily + case weekly + case monthly + + public var title: String { + repeatCycle.text + } + + public var repeatCycle: RepeatCycle { + switch self { + case .daily: return .daily + case .weekly: return .weekly + case .monthly: return .monthly + } + } + + public init(repeatCycle: RepeatCycle) { + switch repeatCycle { + case .daily: self = .daily + case .weekly: self = .weekly + case .monthly: self = .monthly + } + } +} diff --git a/Projects/Feature/MakeGoal/Project.swift b/Projects/Feature/MakeGoal/Project.swift index 453160e0..cdba5d83 100644 --- a/Projects/Feature/MakeGoal/Project.swift +++ b/Projects/Feature/MakeGoal/Project.swift @@ -8,7 +8,9 @@ let project = Project.makeModule( interface: .makeGoal, config: .init( dependencies: [ + .domain(interface: .common), .domain(interface: .goal), + .feature(interface: .common), .shared(implements: .designSystem), .shared(implements: .util), .external(dependency: .ComposableArchitecture) @@ -20,6 +22,7 @@ let project = Project.makeModule( config: .init( dependencies: [ .feature(interface: .makeGoal), + .feature(interface: .common), .domain(interface: .goal), .shared(implements: .designSystem), .external(dependency: .ComposableArchitecture) diff --git a/Projects/Feature/MakeGoal/Sources/MakeGoalReducer+Impl.swift b/Projects/Feature/MakeGoal/Sources/MakeGoalReducer+Impl.swift index 5c4ebcda..ec08838f 100644 --- a/Projects/Feature/MakeGoal/Sources/MakeGoalReducer+Impl.swift +++ b/Projects/Feature/MakeGoal/Sources/MakeGoalReducer+Impl.swift @@ -104,14 +104,7 @@ extension MakeGoalReducer { return .none case let .periodTabSelected(item): - switch item { - case .daily: - state.selectedPeriod = .daily - case .weekly: - state.selectedPeriod = .weekly - case .monthly: - state.selectedPeriod = .monthly - } + state.selectedPeriod = item.repeatCycle return .none case .periodSelected: diff --git a/Projects/Feature/MakeGoal/Sources/MakeGoalView.swift b/Projects/Feature/MakeGoal/Sources/MakeGoalView.swift index edb90bd7..d571e467 100644 --- a/Projects/Feature/MakeGoal/Sources/MakeGoalView.swift +++ b/Projects/Feature/MakeGoal/Sources/MakeGoalView.swift @@ -360,10 +360,6 @@ private extension MakeGoalView { } var selectedPeriodItem: PeriodItem { - switch store.selectedPeriod { - case .daily: return .daily - case .weekly: return .weekly - case .monthly: return .monthly - } + PeriodItem(repeatCycle: store.selectedPeriod) } } diff --git a/Projects/Feature/Stats/Interface/Sources/Detail/StatsDetailReducer.swift b/Projects/Feature/Stats/Interface/Sources/Detail/StatsDetailReducer.swift index ead47343..965a94bb 100644 --- a/Projects/Feature/Stats/Interface/Sources/Detail/StatsDetailReducer.swift +++ b/Projects/Feature/Stats/Interface/Sources/Detail/StatsDetailReducer.swift @@ -9,6 +9,7 @@ import Foundation import ComposableArchitecture import DomainStatsInterface +import FeatureCommonInterface import SharedDesignSystem /// 통계 상세 화면의 상태와 액션을 관리하는 Reducer입니다. diff --git a/Projects/Feature/Stats/Interface/Sources/Stats/StatsReducer.swift b/Projects/Feature/Stats/Interface/Sources/Stats/StatsReducer.swift index b797b8bc..cddf4468 100644 --- a/Projects/Feature/Stats/Interface/Sources/Stats/StatsReducer.swift +++ b/Projects/Feature/Stats/Interface/Sources/Stats/StatsReducer.swift @@ -9,6 +9,7 @@ import Foundation import ComposableArchitecture import DomainStatsInterface +import FeatureCommonInterface import SharedDesignSystem /// 통계 메인 화면의 상태와 액션을 관리하는 Reducer입니다. diff --git a/Projects/Feature/Stats/Interface/Sources/Stats/StatsTopTabItem.swift b/Projects/Feature/Stats/Interface/Sources/Stats/StatsTopTabItem.swift new file mode 100644 index 00000000..1666321c --- /dev/null +++ b/Projects/Feature/Stats/Interface/Sources/Stats/StatsTopTabItem.swift @@ -0,0 +1,21 @@ +// +// StatsTopTabItem.swift +// FeatureStatsInterface +// +// Created by 정지훈 on 4/20/26. +// + +import SharedDesignSystem + +/// 통계 메인 화면 상단 탭에서 사용하는 선택 아이템입니다. +public enum StatsTopTabItem: TXItem { + case ongoing + case completed + + public var title: String { + switch self { + case .ongoing: return "진행중" + case .completed: return "종료" + } + } +} diff --git a/Projects/Feature/Stats/Project.swift b/Projects/Feature/Stats/Project.swift index 7da3095e..d78d04c0 100644 --- a/Projects/Feature/Stats/Project.swift +++ b/Projects/Feature/Stats/Project.swift @@ -9,6 +9,7 @@ let project = Project.makeModule( config: .init( dependencies: [ .domain(interface: .stats), + .feature(interface: .common), .shared(implements: .designSystem), .feature(interface: .goalDetail), .feature(interface: .makeGoal), @@ -20,6 +21,8 @@ let project = Project.makeModule( implements: .stats, config: .init( dependencies: [ + .domain(interface: .common), + .feature(interface: .common), .feature(interface: .stats), .feature(interface: .goalDetail), .feature(interface: .makeGoal), @@ -53,6 +56,7 @@ let project = Project.makeModule( "UIUserInterfaceStyle": "Light" ]), dependencies: [ + .feature(interface: .common), .feature(interface: .stats), .feature(implements: .stats), .feature(interface: .goalDetail), diff --git a/Projects/Feature/Stats/Sources/Detail/StatsDetailReducer+Impl.swift b/Projects/Feature/Stats/Sources/Detail/StatsDetailReducer+Impl.swift index 23e04226..68f481ee 100644 --- a/Projects/Feature/Stats/Sources/Detail/StatsDetailReducer+Impl.swift +++ b/Projects/Feature/Stats/Sources/Detail/StatsDetailReducer+Impl.swift @@ -9,6 +9,7 @@ import Foundation import ComposableArchitecture import DomainStatsInterface +import FeatureCommonInterface import FeatureStatsInterface import SharedDesignSystem diff --git a/Projects/Feature/Stats/Sources/Detail/StatsDetailView.swift b/Projects/Feature/Stats/Sources/Detail/StatsDetailView.swift index c87e4bf4..44ff999b 100644 --- a/Projects/Feature/Stats/Sources/Detail/StatsDetailView.swift +++ b/Projects/Feature/Stats/Sources/Detail/StatsDetailView.swift @@ -8,6 +8,7 @@ import SwiftUI import ComposableArchitecture +import FeatureCommonInterface import FeatureStatsInterface import SharedDesignSystem import Kingfisher diff --git a/Projects/Feature/Stats/Sources/Detail/StatsRepeatCycle+Text.swift b/Projects/Feature/Stats/Sources/Detail/StatsRepeatCycle+Text.swift deleted file mode 100644 index 63b859ff..00000000 --- a/Projects/Feature/Stats/Sources/Detail/StatsRepeatCycle+Text.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// StatsRepeatCycle+Text.swift -// FeatureStats -// -// Created by 정지훈 on 2/20/26. -// - -import DomainStatsInterface - -extension Stats.RepeatCycle { - var text: String { - switch self { - case .daily: "매일" - case .weekly: "매주" - case .monthly: "매월" - } - } -} diff --git a/Projects/Feature/Stats/Sources/Stats/StatsReducer+Impl.swift b/Projects/Feature/Stats/Sources/Stats/StatsReducer+Impl.swift index 86284d49..8d2dad07 100644 --- a/Projects/Feature/Stats/Sources/Stats/StatsReducer+Impl.swift +++ b/Projects/Feature/Stats/Sources/Stats/StatsReducer+Impl.swift @@ -8,7 +8,9 @@ import Foundation import ComposableArchitecture +import DomainCommonInterface import DomainStatsInterface +import FeatureCommonInterface import FeatureStatsInterface import SharedDesignSystem @@ -131,7 +133,7 @@ extension StatsReducer { } } -private extension Stats.StatsItem.StampColor { +private extension StampColor { var statsCardStampColor: StatsCardItem.StampColor { switch self { case .green400: .green400 diff --git a/Projects/Shared/DesignSystem/Sources/Components/Dropdown/TXDropdown.swift b/Projects/Shared/DesignSystem/Sources/Components/Dropdown/TXDropdown.swift index 1aeed7ce..eb8026ce 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/Dropdown/TXDropdown.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/Dropdown/TXDropdown.swift @@ -15,10 +15,15 @@ public protocol TXItem: CaseIterable, Equatable, Hashable { /// /// ## 사용 예시 /// ```swift -/// TXDropdown(items: GoalDropDownItem.allCases) { item in -/// if item == .edit { -/// print("수정하기 선택") -/// } +/// enum MenuItem: String, TXItem { +/// case first +/// case second +/// +/// var title: String { rawValue } +/// } +/// +/// TXDropdown(items: MenuItem.allCases) { item in +/// print(item) /// } /// ``` public struct TXDropdown: View { @@ -113,6 +118,13 @@ private enum Constants { #Preview { VStack { - TXDropdown(items: GoalDropList.allCases, onSelect: { print($0) }) + TXDropdown(items: PreviewDropdownItem.allCases, onSelect: { print($0) }) } } + +private enum PreviewDropdownItem: String, TXItem { + case first = "first" + case second = "second" + + var title: String { rawValue } +} diff --git a/Projects/Shared/DesignSystem/Sources/Components/Dropdown/TXItem.swift b/Projects/Shared/DesignSystem/Sources/Components/Dropdown/TXItem.swift deleted file mode 100644 index b63f3bfb..00000000 --- a/Projects/Shared/DesignSystem/Sources/Components/Dropdown/TXItem.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// TXDropdownAction.swift -// SharedDesignSystem -// -// Created by 정지훈 on 2/5/26. -// - -import Foundation - -/// 목표 드롭다운에서 선택 가능한 기본 항목 타입입니다. -/// -/// ## 사용 예시 -/// ```swift -/// let action: GoalDropList = .edit -/// print(action.title) -/// ``` - -// TODO: - Feature 계층으로 분리 예정 -public enum GoalDropList: TXItem { - case edit - case finish - case delete -} - -public enum PeriodItem: TXItem { - case daily - case weekly - case monthly -} - -public enum StatsTopTabItem: TXItem { - case ongoing - case completed -} - -public extension GoalDropList { - var title: String { - switch self { - case .edit: return "수정하기" - case .finish: return "끝내기" - case .delete: return "삭제하기" - } - } -} - -public extension PeriodItem { - var title: String { - switch self { - case .daily: return "매일" - case .weekly: return "매주" - case .monthly: return "매월" - } - } -} - -public extension StatsTopTabItem { - var title: String { - switch self { - case .ongoing: return "진행중" - case .completed: return "종료" - } - } -} diff --git a/Projects/Shared/DesignSystem/Sources/Components/Tab/TXTab.swift b/Projects/Shared/DesignSystem/Sources/Components/Tab/TXTab.swift index ec1be001..5d055dcc 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/Tab/TXTab.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/Tab/TXTab.swift @@ -43,5 +43,12 @@ public struct TXTab: View { } #Preview { - TXTab(style: .button(GoalDropList.allCases), onSelect: { _ in }) + TXTab(style: .button(PreviewTabItem.allCases), onSelect: { _ in }) +} + +private enum PreviewTabItem: String, TXItem { + case first = "first" + case second = "second" + + var title: String { rawValue } } diff --git a/Projects/Shared/DesignSystem/Sources/Components/Tab/TopBar/TXTopTabBar.swift b/Projects/Shared/DesignSystem/Sources/Components/Tab/TopBar/TXTopTabBar.swift index 8487f6bb..7eddf617 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/Tab/TopBar/TXTopTabBar.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/Tab/TopBar/TXTopTabBar.swift @@ -11,7 +11,14 @@ import SwiftUI /// /// ## 사용 예시 /// ```swift -/// TXTab(style: .line(StatsTopTabItem.allCases), selectedItem: .ongoing) { item in print(item) } +/// enum TopTabItem: String, TXItem { +/// case first +/// case second +/// +/// var title: String { rawValue } +/// } +/// +/// TXTab(style: .line(TopTabItem.allCases), selectedItem: .first) { item in print(item) } /// ``` struct TXTopTabBar: View { private let selectedItem: Item? @@ -74,10 +81,17 @@ private enum Constants { #Preview { VStack { TXTopTabBar( - items: StatsTopTabItem.allCases, - selectedItem: .ongoing + items: PreviewTopTabItem.allCases, + selectedItem: .first ) Spacer() } } + +private enum PreviewTopTabItem: String, TXItem { + case first = "first" + case second = "second" + + var title: String { rawValue } +} diff --git a/Tuist/ProjectDescriptionHelpers/Module.swift b/Tuist/ProjectDescriptionHelpers/Module.swift index d42e6f8b..8199e77a 100644 --- a/Tuist/ProjectDescriptionHelpers/Module.swift +++ b/Tuist/ProjectDescriptionHelpers/Module.swift @@ -49,6 +49,7 @@ public extension Module { /// 화면 단위 또는 사용자 플로우 단위로 구성되며, /// UI와 사용자 상호작용 로직을 중심으로 설계됩니다. enum Feature: String, CaseIterable { + case common = "Common" case makeGoal = "MakeGoal" case notification = "Notification" case stats = "Stats" @@ -71,6 +72,7 @@ public extension Module { /// 앱의 핵심 규칙과 정책을 담으며, /// Feature에 의존하지 않고 독립적으로 설계되는 것이 원칙입니다. enum Domain: String, CaseIterable { + case common = "Common" case notification = "Notification" case stats = "Stats" case photoLog = "PhotoLog" From 91d086e8ab7e17e6751dde2df4ae9c9efc7164a9 Mon Sep 17 00:00:00 2001 From: Jiyong Jung Date: Tue, 5 May 2026 20:00:11 +0900 Subject: [PATCH 02/44] =?UTF-8?q?fix:=20D020=20=EC=BB=AC=EB=9F=AC=EC=9D=98?= =?UTF-8?q?=20=EC=95=8C=ED=8C=8C=EA=B0=92=EC=9D=B4=20=EC=9E=98=EB=AA=BB?= =?UTF-8?q?=EB=90=98=EC=97=88=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20-=20#282?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Dimmed/dimmed-20.colorset/Contents.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Projects/Shared/DesignSystem/Resources/Color/ColorAssets.xcassets/Dimmed/dimmed-20.colorset/Contents.json b/Projects/Shared/DesignSystem/Resources/Color/ColorAssets.xcassets/Dimmed/dimmed-20.colorset/Contents.json index 8da89c84..6f2c4037 100644 --- a/Projects/Shared/DesignSystem/Resources/Color/ColorAssets.xcassets/Dimmed/dimmed-20.colorset/Contents.json +++ b/Projects/Shared/DesignSystem/Resources/Color/ColorAssets.xcassets/Dimmed/dimmed-20.colorset/Contents.json @@ -4,7 +4,7 @@ "color" : { "color-space" : "srgb", "components" : { - "alpha" : "0.700", + "alpha" : "0.200", "blue" : "0.000", "green" : "0.000", "red" : "0.000" From 1a7fd969d2a842cfbcdcc4532782b3f99c94c2a4 Mon Sep 17 00:00:00 2001 From: Jiyong Jung Date: Tue, 5 May 2026 20:00:34 +0900 Subject: [PATCH 03/44] =?UTF-8?q?feat:=20=EC=8B=A0=EA=B7=9C=20=EB=A1=9C?= =?UTF-8?q?=EB=94=A9=20=EB=B7=B0/=EC=9D=B8=EB=94=94=EC=BC=80=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EC=B6=94=EA=B0=80=20-=20#282?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Loading/TXLoadingIndicator.swift | 35 +++++ .../Loading/TXLoadingPresenter.swift | 127 ++++++++++++++++++ .../Loading/TXLoadingStatusView.swift | 35 +++++ 3 files changed, 197 insertions(+) create mode 100644 Projects/Shared/DesignSystem/Sources/Components/Indicator/Loading/TXLoadingIndicator.swift create mode 100644 Projects/Shared/DesignSystem/Sources/Components/Indicator/Loading/TXLoadingPresenter.swift create mode 100644 Projects/Shared/DesignSystem/Sources/Components/Indicator/Loading/TXLoadingStatusView.swift diff --git a/Projects/Shared/DesignSystem/Sources/Components/Indicator/Loading/TXLoadingIndicator.swift b/Projects/Shared/DesignSystem/Sources/Components/Indicator/Loading/TXLoadingIndicator.swift new file mode 100644 index 00000000..5ff34a12 --- /dev/null +++ b/Projects/Shared/DesignSystem/Sources/Components/Indicator/Loading/TXLoadingIndicator.swift @@ -0,0 +1,35 @@ +// +// TXLoadingIndicator.swift +// SharedDesignSystem +// +// Created by 정지용 on 5/5/26. +// + +import SwiftUI + +struct TXLoadingIndicator: View { + @State private var rotation: Double = 0 + + var body: some View { + Circle() + .trim(from: 0.175, to: 0.825) + .stroke( + Color.primary, + style: StrokeStyle( + lineWidth: 1, + lineCap: .square + ) + ) + .frame(width: 16, height: 16) + .rotationEffect(.degrees(rotation)) + .onAppear { + withAnimation(.linear(duration: 0.8).repeatForever(autoreverses: false)) { + rotation = 360 + } + } + } +} + +#Preview { + TXLoadingIndicator() +} diff --git a/Projects/Shared/DesignSystem/Sources/Components/Indicator/Loading/TXLoadingPresenter.swift b/Projects/Shared/DesignSystem/Sources/Components/Indicator/Loading/TXLoadingPresenter.swift new file mode 100644 index 00000000..413e970c --- /dev/null +++ b/Projects/Shared/DesignSystem/Sources/Components/Indicator/Loading/TXLoadingPresenter.swift @@ -0,0 +1,127 @@ +// +// TXLoadingPresenter.swift +// SharedDesignSystem +// +// Created by 정지용 on 5/5/26. +// + +import SwiftUI + +// MARK: - TXLoadingModifier + +/// 로딩 상태를 표시하는 ViewModifier입니다. +/// - message가 nil이면 스피너 단독 + dimmed20 +/// - message가 있으면 중앙 캡슐 + dimmed70 +struct TXLoadingModifier: ViewModifier { + let isPresented: Bool + let message: String? + + private var dimColor: Color { + message == nil ? Color.Dimmed.dimmed20 : Color.Dimmed.dimmed70 + } + + func body(content: Content) -> some View { + ZStack { + content + .disabled(isPresented) + if isPresented { + dimColor + .ignoresSafeArea() + if let message { + TXLoadingStatusView(message: message) + } else { + TXLoadingIndicator() + } + } + } + .animation(.easeInOut(duration: 0.2), value: isPresented) + } +} + +// MARK: - View Extension +public extension View { + /// 스피너를 dimmed20 배경과 함께 표시합니다. + /// + /// ## 사용 예시 + /// ```swift + /// VStack { ... } + /// .txLoading(isPresented: $isLoading) + /// ``` + func txLoading(isPresented: Binding) -> some View { + self.modifier(TXLoadingModifier(isPresented: isPresented.wrappedValue, message: nil)) + } + + /// 중앙 캡슐 로딩 뷰를 dimmed70 배경과 함께 표시합니다. + /// + /// ## 사용 예시 + /// ```swift + /// VStack { ... } + /// .txLoading(isPresented: $isLoading, message: "저장 중...") + /// ``` + func txLoading(isPresented: Binding, message: String) -> some View { + self.modifier(TXLoadingModifier(isPresented: isPresented.wrappedValue, message: message)) + } + + /// 중앙 캡슐 로딩 뷰를 `String?` item 기반으로 표시합니다. + /// + /// ## 사용 예시 + /// ```swift + /// @State private var loadingMessage: String? + /// + /// VStack { ... } + /// .txLoading(item: $loadingMessage) + /// + /// // 표시 + /// loadingMessage = "업로드 중..." + /// + /// // 숨김 + /// loadingMessage = nil + /// ``` + func txLoading(item: Binding) -> some View { + self.modifier(TXLoadingModifier(isPresented: item.wrappedValue != nil, message: item.wrappedValue)) + } +} + +// MARK: - Preview +#Preview("스피너 only") { + struct PreviewWrapper: View { + @State private var isLoading = false + + var body: some View { + Button("토글") { isLoading.toggle() } + .txLoading(isPresented: $isLoading) + } + } + + return PreviewWrapper() +} + +#Preview("캡슐 - isPresented") { + struct PreviewWrapper: View { + @State private var isLoading = false + + var body: some View { + Button("토글") { isLoading.toggle() } + .txLoading(isPresented: $isLoading, message: "저장 중...") + } + } + + return PreviewWrapper() +} + +#Preview("캡슐 - item") { + struct PreviewWrapper: View { + @State private var loadingMessage: String? + + var body: some View { + VStack(spacing: 16) { + Button("업로드") { loadingMessage = "업로드 중..." } + Button("저장") { loadingMessage = "저장 중..." } + Button("숨기기") { loadingMessage = nil } + } + .txLoading(item: $loadingMessage) + } + } + + return PreviewWrapper() +} diff --git a/Projects/Shared/DesignSystem/Sources/Components/Indicator/Loading/TXLoadingStatusView.swift b/Projects/Shared/DesignSystem/Sources/Components/Indicator/Loading/TXLoadingStatusView.swift new file mode 100644 index 00000000..3bca2a28 --- /dev/null +++ b/Projects/Shared/DesignSystem/Sources/Components/Indicator/Loading/TXLoadingStatusView.swift @@ -0,0 +1,35 @@ +// +// TXLoadingStatusView.swift +// SharedDesignSystem +// +// Created by 정지용 on 5/5/26. +// + +import SwiftUI + +struct TXLoadingStatusView: View { + let message: String + + init(message: String = "로딩 중...") { + self.message = message + } + + var body: some View { + HStack(spacing: 12) { + TXLoadingIndicator() + + Text(message) + .typography(.b1_14b) + .foregroundStyle(Color.Gray.gray500) + } + .frame(width: 159, height: 60) + .background( + Capsule() + .fill(Color.Common.white) + ) + } +} + +#Preview { + TXLoadingStatusView() +} From 48ada445f32a91e59606f593b51d3be80a9d7644 Mon Sep 17 00:00:00 2001 From: Jiyong Jung Date: Tue, 5 May 2026 20:06:37 +0900 Subject: [PATCH 04/44] =?UTF-8?q?feat:=20=EC=9D=B8=EC=A6=9D=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=EC=97=90=EC=84=9C=20=EA=B8=B0=EC=A1=B4=20=EB=A1=9C?= =?UTF-8?q?=EB=94=A9=20=EB=94=94=EC=9E=90=EC=9D=B8=20=EC=A0=9C=EA=B1=B0=20?= =?UTF-8?q?=EB=B0=8F=20=EC=8B=A0=EA=B7=9C=20=EB=A1=9C=EB=94=A9=20=EB=94=94?= =?UTF-8?q?=EC=9E=90=EC=9D=B8=20=EC=A0=81=EC=9A=A9=20-=20#282?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ProofPhoto/ProofPhotoLoadingView.swift | 64 ------------------- .../Sources/ProofPhoto/ProofPhotoView.swift | 4 +- 2 files changed, 1 insertion(+), 67 deletions(-) delete mode 100644 Projects/Feature/ProofPhoto/Sources/ProofPhoto/ProofPhotoLoadingView.swift diff --git a/Projects/Feature/ProofPhoto/Sources/ProofPhoto/ProofPhotoLoadingView.swift b/Projects/Feature/ProofPhoto/Sources/ProofPhoto/ProofPhotoLoadingView.swift deleted file mode 100644 index 680cb7dd..00000000 --- a/Projects/Feature/ProofPhoto/Sources/ProofPhoto/ProofPhotoLoadingView.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// ProofPhotoLoadingView.swift -// FeatureProofPhoto -// -// Created by 정지훈 on 4/13/26. -// - -import SwiftUI - -import SharedDesignSystem - -struct ProofPhotoLoadingView: View { - @State private var isAnimating = false - - var body: some View { - VStack(spacing: Constants.stackSpacing) { - Image.Illustration.plane - .resizable() - .frame( - width: Constants.imageWidth, - height: Constants.imageHeight - ) - .rotationEffect( - .degrees(isAnimating ? Constants.rotationDegrees : .zero) - ) - .animation( - .easeInOut(duration: Constants.animationDuration) - .repeatForever(autoreverses: true), - value: isAnimating - ) - - Text(Constants.title) - .typography(.h1_28b) - .foregroundStyle(Color.Gray.gray500) - - Text(Constants.subTitle) - .typography(.t2_16b) - .foregroundStyle(Color.Gray.gray300) - .padding(.top, Constants.descriptionTopPadding) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color.Common.white) - .onAppear { - isAnimating = true - } - } -} - -#Preview { - ProofPhotoLoadingView() -} - -private extension ProofPhotoLoadingView { - enum Constants { - static let title: String = "인증샷 업로드 중..." - static let subTitle: String = "잠시만 기다려 주세요." - static let stackSpacing: CGFloat = 6 - static let imageWidth: CGFloat = 164 - static let imageHeight: CGFloat = 134 - static let rotationDegrees: Double = 10 - static let animationDuration: Double = 0.8 - static let descriptionTopPadding: CGFloat = 10 - } -} diff --git a/Projects/Feature/ProofPhoto/Sources/ProofPhoto/ProofPhotoView.swift b/Projects/Feature/ProofPhoto/Sources/ProofPhoto/ProofPhotoView.swift index a134676c..46889072 100644 --- a/Projects/Feature/ProofPhoto/Sources/ProofPhoto/ProofPhotoView.swift +++ b/Projects/Feature/ProofPhoto/Sources/ProofPhoto/ProofPhotoView.swift @@ -65,9 +65,6 @@ public struct ProofPhotoView: View { floatingCommentOverlay } - if store.isUploading { - ProofPhotoLoadingView() - } } .ignoresSafeArea(.keyboard) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) @@ -77,6 +74,7 @@ public struct ProofPhotoView: View { store.send(.onAppear) } .txToast(item: $store.toast, customPadding: 75) + .txLoading(isPresented: $store.isUploading, message: "업로드 중...") } } From e04fa86095b2303ff9df3c8eb8fecab0698a78e2 Mon Sep 17 00:00:00 2001 From: Jiyong Jung Date: Tue, 5 May 2026 20:11:37 +0900 Subject: [PATCH 05/44] =?UTF-8?q?feat:=20=EC=A0=84=EC=B2=B4=20=EB=A1=9C?= =?UTF-8?q?=EB=94=A9=20=EC=9D=B8=EB=94=94=EC=BC=80=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=EB=A5=BC=20=EC=8B=A0=EA=B7=9C=20=EB=94=94=EC=9E=90=EC=9D=B8?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EA=B5=90=EC=B2=B4=20-=20#282?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/App/Sources/View/AppRootView.swift | 5 ++--- .../Feature/Auth/Sources/View/AuthView.swift | 13 ++----------- .../Sources/Detail/GoalDetailView.swift | 6 +----- .../Home/Sources/Goal/EditGoalListView.swift | 6 +----- .../Feature/Home/Sources/Home/HomeView.swift | 6 ------ .../MainTab/Sources/View/MainTabView.swift | 8 ++++++++ .../Interface/Sources/MakeGoalReducer.swift | 1 + .../Sources/MakeGoalReducer+Impl.swift | 7 ++++++- .../MakeGoal/Sources/MakeGoalView.swift | 5 +++++ .../Sources/NotificationView.swift | 18 ++---------------- .../CodeInput/OnboardingCodeInputView.swift | 1 + .../Sources/Dday/OnboardingDdayView.swift | 1 + .../Profile/OnboardingProfileView.swift | 1 + .../Settings/Sources/Account/AccountView.swift | 8 +------- .../NotificationSettingsView.swift | 18 ++---------------- .../Stats/Sources/Detail/StatsDetailView.swift | 6 +----- .../Stats/Sources/Stats/StatsView.swift | 5 ----- 17 files changed, 35 insertions(+), 80 deletions(-) diff --git a/Projects/App/Sources/View/AppRootView.swift b/Projects/App/Sources/View/AppRootView.swift index 83cf6ce5..f6ff9a0b 100644 --- a/Projects/App/Sources/View/AppRootView.swift +++ b/Projects/App/Sources/View/AppRootView.swift @@ -26,9 +26,7 @@ struct AppRootView: View { let routeStore = store.scope(state: \.route, action: \.route) ZStack { - if store.isCheckingAuth { - ProgressView() - } else { + if !store.isCheckingAuth { switch routeStore.state { case .auth: if let authStore = routeStore.scope(state: \.auth, action: \.auth) { @@ -51,6 +49,7 @@ struct AppRootView: View { } } .animation(.easeInOut(duration: Constants.transitionDuration), value: store.route) + .txLoading(isPresented: .constant(store.isCheckingAuth)) .onAppear { store.send(.onAppear) } diff --git a/Projects/Feature/Auth/Sources/View/AuthView.swift b/Projects/Feature/Auth/Sources/View/AuthView.swift index 34542abb..4f6e5df9 100644 --- a/Projects/Feature/Auth/Sources/View/AuthView.swift +++ b/Projects/Feature/Auth/Sources/View/AuthView.swift @@ -24,10 +24,8 @@ public struct AuthView: View { backgroundIllustration foregroundContent } - .overlay { - loadingView - } - .background(Color.Common.white) + .background(Color.Common.white) + .txLoading(isPresented: .constant(store.isLoading)) .alert( "로그인 실패", isPresented: Binding( @@ -191,13 +189,6 @@ private extension AuthView { .disabled(store.isLoading) } - @ViewBuilder - var loadingView: some View { - if store.isLoading { - ProgressView() - .frame(maxWidth: .infinity, maxHeight: .infinity) - } - } } // swiftlint:enable no_magic_numbers diff --git a/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift b/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift index 92be8fcb..47d2749c 100644 --- a/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift +++ b/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift @@ -111,12 +111,8 @@ public struct GoalDetailView: View { .overlay(alignment: .bottom) { myEmojiFlyingReactionOverlay } - .overlay { - if store.isSavingPhotoLog { - ProgressView() - } - } .txToast(item: $store.toast, customPadding: 54) + .txLoading(isPresented: $store.isSavingPhotoLog) } } diff --git a/Projects/Feature/Home/Sources/Goal/EditGoalListView.swift b/Projects/Feature/Home/Sources/Goal/EditGoalListView.swift index 465f4759..1c0324b8 100644 --- a/Projects/Feature/Home/Sources/Goal/EditGoalListView.swift +++ b/Projects/Feature/Home/Sources/Goal/EditGoalListView.swift @@ -31,11 +31,6 @@ struct EditGoalListView: View { .ignoresSafeArea(.container, edges: .bottom) .frame(maxWidth: .infinity, maxHeight: .infinity) .overlay { - if store.isLoading { - ProgressView() - .frame(maxWidth: .infinity, maxHeight: .infinity) - } - if let cards = store.cards, cards.isEmpty { emptyContent } @@ -62,6 +57,7 @@ struct EditGoalListView: View { .txToast(item: $store.toast, onButtonTap: { store.send(.toastButtonTapped) }) + .txLoading(isPresented: $store.isLoading) } } diff --git a/Projects/Feature/Home/Sources/Home/HomeView.swift b/Projects/Feature/Home/Sources/Home/HomeView.swift index 0e7a41ac..6c87a999 100644 --- a/Projects/Feature/Home/Sources/Home/HomeView.swift +++ b/Projects/Feature/Home/Sources/Home/HomeView.swift @@ -51,12 +51,6 @@ public struct HomeView: View { } } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) - .overlay { - if store.isLoading { - ProgressView() - .frame(maxWidth: .infinity, maxHeight: .infinity) - } - } .onAppear { store.send(.onAppear) } diff --git a/Projects/Feature/MainTab/Sources/View/MainTabView.swift b/Projects/Feature/MainTab/Sources/View/MainTabView.swift index f52f1df0..bc3f24bb 100644 --- a/Projects/Feature/MainTab/Sources/View/MainTabView.swift +++ b/Projects/Feature/MainTab/Sources/View/MainTabView.swift @@ -66,6 +66,14 @@ public struct MainTabView: View { item: $store.home.home.toast, customPadding: Constants.tabBarHeight ) + .txLoading(isPresented: .constant(isTabLoading)) + } +} + +private extension MainTabView { + var isTabLoading: Bool { + (store.selectedTab == .home && store.home.routes.isEmpty && store.home.home.isLoading) || + (store.selectedTab == .statistics && store.stats.routes.isEmpty && store.stats.stats.isLoading) } } diff --git a/Projects/Feature/MakeGoal/Interface/Sources/MakeGoalReducer.swift b/Projects/Feature/MakeGoal/Interface/Sources/MakeGoalReducer.swift index ba00f39b..01a4fd2d 100644 --- a/Projects/Feature/MakeGoal/Interface/Sources/MakeGoalReducer.swift +++ b/Projects/Feature/MakeGoal/Interface/Sources/MakeGoalReducer.swift @@ -74,6 +74,7 @@ public struct MakeGoalReducer { public var modal: TXModalStyle? public var toast: TXToastType? public var isLoading: Bool = false + public var submitMessage: String? = nil /// 화면 모드를 구분합니다. public enum Mode: Equatable { diff --git a/Projects/Feature/MakeGoal/Sources/MakeGoalReducer+Impl.swift b/Projects/Feature/MakeGoal/Sources/MakeGoalReducer+Impl.swift index ec08838f..5f192c63 100644 --- a/Projects/Feature/MakeGoal/Sources/MakeGoalReducer+Impl.swift +++ b/Projects/Feature/MakeGoal/Sources/MakeGoalReducer+Impl.swift @@ -74,10 +74,12 @@ extension MakeGoalReducer { case .createGoalFailed: state.isLoading = false + state.submitMessage = nil return .send(.showToast(.warning(message: "목표 생성에 실패했어요"))) case .updateGoalFailed: state.isLoading = false + state.submitMessage = nil return .send(.showToast(.warning(message: "이미 완료한 목표입니다!"))) // MARK: - User Action @@ -198,7 +200,7 @@ extension MakeGoalReducer { guard !state.completeButtonDisabled else { return .send(.showToast(.warning(message: "목표 이름은 14글자 이내로 입력해 주세요!"))) } - + state.isLoading = true let endDateString: String? = state.isEndDateOn ? TXCalendarUtil.apiDateString(for: state.endDate) @@ -206,6 +208,7 @@ extension MakeGoalReducer { switch state.mode { case .add: + state.submitMessage = "등록 중..." let request = GoalCreateRequestDTO( name: state.goalTitle, icon: state.selectedEmoji.rawValue, @@ -224,8 +227,10 @@ extension MakeGoalReducer { } case .edit: + state.submitMessage = "수정 중..." guard let goalId = state.editingGoalId else { state.isLoading = false + state.submitMessage = nil return .send(.showToast(.warning(message: "목표 수정에 실패했어요"))) } let request = GoalUpdateRequestDTO( diff --git a/Projects/Feature/MakeGoal/Sources/MakeGoalView.swift b/Projects/Feature/MakeGoal/Sources/MakeGoalView.swift index d571e467..556ae2e0 100644 --- a/Projects/Feature/MakeGoal/Sources/MakeGoalView.swift +++ b/Projects/Feature/MakeGoal/Sources/MakeGoalView.swift @@ -79,6 +79,11 @@ public struct MakeGoalView: View { } ) .txToast(item: $store.toast, customPadding: 70) + .txLoading(isPresented: Binding( + get: { store.isLoading && store.submitMessage == nil }, + set: { _ in } + )) + .txLoading(item: $store.submitMessage) } } diff --git a/Projects/Feature/Notification/Sources/NotificationView.swift b/Projects/Feature/Notification/Sources/NotificationView.swift index 0a11cd6a..0e44c712 100644 --- a/Projects/Feature/Notification/Sources/NotificationView.swift +++ b/Projects/Feature/Notification/Sources/NotificationView.swift @@ -23,9 +23,7 @@ public struct NotificationView: View { navigationBar ZStack { - if store.isLoading && store.notifications.isEmpty { - loadingView - } else if filteredNotifications.isEmpty { + if filteredNotifications.isEmpty { emptyView } else { contentView @@ -39,6 +37,7 @@ public struct NotificationView: View { store.send(.onAppear) } .toolbar(.hidden, for: .navigationBar) + .txLoading(isPresented: .constant(store.isLoading && store.notifications.isEmpty)) } } @@ -75,19 +74,6 @@ private extension NotificationView { } } -// MARK: - Loading View - -private extension NotificationView { - var loadingView: some View { - VStack { - Spacer() - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: Color.Gray.gray500)) - Spacer() - } - } -} - // MARK: - Empty View private extension NotificationView { diff --git a/Projects/Feature/Onboarding/Sources/CodeInput/OnboardingCodeInputView.swift b/Projects/Feature/Onboarding/Sources/CodeInput/OnboardingCodeInputView.swift index 26a004af..1399033f 100644 --- a/Projects/Feature/Onboarding/Sources/CodeInput/OnboardingCodeInputView.swift +++ b/Projects/Feature/Onboarding/Sources/CodeInput/OnboardingCodeInputView.swift @@ -50,6 +50,7 @@ public struct OnboardingCodeInputView: View { isTextFieldFocused = false } .txToast(item: $store.toast, customPadding: 76) + .txLoading(isPresented: $store.isLoading) } } diff --git a/Projects/Feature/Onboarding/Sources/Dday/OnboardingDdayView.swift b/Projects/Feature/Onboarding/Sources/Dday/OnboardingDdayView.swift index 74f6eee9..1919eddd 100644 --- a/Projects/Feature/Onboarding/Sources/Dday/OnboardingDdayView.swift +++ b/Projects/Feature/Onboarding/Sources/Dday/OnboardingDdayView.swift @@ -59,6 +59,7 @@ public struct OnboardingDdayView: View { } ) } + .txLoading(isPresented: $store.isLoading) } } diff --git a/Projects/Feature/Onboarding/Sources/Profile/OnboardingProfileView.swift b/Projects/Feature/Onboarding/Sources/Profile/OnboardingProfileView.swift index 722a9b94..d66d27c4 100644 --- a/Projects/Feature/Onboarding/Sources/Profile/OnboardingProfileView.swift +++ b/Projects/Feature/Onboarding/Sources/Profile/OnboardingProfileView.swift @@ -45,6 +45,7 @@ public struct OnboardingProfileView: View { isTextFieldFocused = false } .txToast(item: $store.toast, customPadding: 76) + .txLoading(isPresented: $store.isLoading) } } diff --git a/Projects/Feature/Settings/Sources/Account/AccountView.swift b/Projects/Feature/Settings/Sources/Account/AccountView.swift index 0fd2f1c8..77aa7a8b 100644 --- a/Projects/Feature/Settings/Sources/Account/AccountView.swift +++ b/Projects/Feature/Settings/Sources/Account/AccountView.swift @@ -27,18 +27,12 @@ struct AccountView: View { .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.Common.white) .navigationBarBackButtonHidden(true) - .overlay { - if store.isLoading { - ProgressView() - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color.Common.white.opacity(0.4)) - } - } .txModal(item: $store.modal) { action in if action == .confirm { store.send(.modalConfirmTapped) } } + .txLoading(isPresented: $store.isLoading) } } diff --git a/Projects/Feature/Settings/Sources/NotificationSettings/NotificationSettingsView.swift b/Projects/Feature/Settings/Sources/NotificationSettings/NotificationSettingsView.swift index c25d2398..877c0196 100644 --- a/Projects/Feature/Settings/Sources/NotificationSettings/NotificationSettingsView.swift +++ b/Projects/Feature/Settings/Sources/NotificationSettings/NotificationSettingsView.swift @@ -20,9 +20,7 @@ struct NotificationSettingsView: View { navigationBar ZStack { - if store.isNotificationSettingsLoading { - loadingView - } else if !store.isSystemNotificationEnabled { + if !store.isSystemNotificationEnabled { disabledView } else { ScrollView { @@ -44,6 +42,7 @@ struct NotificationSettingsView: View { store.send(.notificationSettingsOnAppear) } } + .txLoading(isPresented: $store.isNotificationSettingsLoading) } } @@ -59,19 +58,6 @@ private extension NotificationSettingsView { } } -// MARK: - Loading View - -private extension NotificationSettingsView { - var loadingView: some View { - VStack { - Spacer() - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: Color.Gray.gray500)) - Spacer() - } - } -} - // MARK: - Disabled View (System Notification Off) private extension NotificationSettingsView { diff --git a/Projects/Feature/Stats/Sources/Detail/StatsDetailView.swift b/Projects/Feature/Stats/Sources/Detail/StatsDetailView.swift index 44ff999b..0120c054 100644 --- a/Projects/Feature/Stats/Sources/Detail/StatsDetailView.swift +++ b/Projects/Feature/Stats/Sources/Detail/StatsDetailView.swift @@ -36,11 +36,6 @@ struct StatsDetailView: View { } } .background(Color.Gray.gray50) - .overlay { - if store.isLoading { - ProgressView() - } - } .overlay(alignment: .topTrailing) { if store.isDropdownPresented { TXDropdown( @@ -69,6 +64,7 @@ struct StatsDetailView: View { } } .txToast(item: $store.toast) + .txLoading(isPresented: $store.isLoading) } } diff --git a/Projects/Feature/Stats/Sources/Stats/StatsView.swift b/Projects/Feature/Stats/Sources/Stats/StatsView.swift index b592fa76..8d010adc 100644 --- a/Projects/Feature/Stats/Sources/Stats/StatsView.swift +++ b/Projects/Feature/Stats/Sources/Stats/StatsView.swift @@ -36,11 +36,6 @@ struct StatsView: View { statsEmptyView } } - .overlay { - if store.isLoading { - ProgressView() - } - } .onAppear { store.send(.onAppear) } .txToast(item: $store.toast) .toolbar(.hidden, for: .tabBar) From 7985245782ace823fae4d5ab486235345565ac41 Mon Sep 17 00:00:00 2001 From: Jiyong Jung Date: Fri, 8 May 2026 18:09:45 +0900 Subject: [PATCH 06/44] =?UTF-8?q?refactor:=20txLoading=20modifier=20Bindin?= =?UTF-8?q?g=20=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20isPresented+message=20?= =?UTF-8?q?=EC=98=A4=EB=B2=84=EB=A1=9C=EB=93=9C=20=EC=A0=9C=EA=B1=B0=20-?= =?UTF-8?q?=20#282?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/App/Sources/View/AppRootView.swift | 2 +- .../Feature/Auth/Sources/View/AuthView.swift | 2 +- .../Sources/Detail/GoalDetailView.swift | 2 +- .../Home/Sources/Goal/EditGoalListView.swift | 2 +- .../MainTab/Sources/View/MainTabView.swift | 2 +- .../MakeGoal/Sources/MakeGoalView.swift | 7 +-- .../Sources/NotificationView.swift | 2 +- .../CodeInput/OnboardingCodeInputView.swift | 2 +- .../Sources/Dday/OnboardingDdayView.swift | 2 +- .../Profile/OnboardingProfileView.swift | 2 +- .../Sources/ProofPhoto/ProofPhotoView.swift | 2 +- .../Sources/Account/AccountView.swift | 2 +- .../NotificationSettingsView.swift | 2 +- .../Sources/Detail/StatsDetailView.swift | 2 +- .../Loading/TXLoadingPresenter.swift | 48 ++++--------------- 15 files changed, 23 insertions(+), 58 deletions(-) diff --git a/Projects/App/Sources/View/AppRootView.swift b/Projects/App/Sources/View/AppRootView.swift index f6ff9a0b..6f8c5410 100644 --- a/Projects/App/Sources/View/AppRootView.swift +++ b/Projects/App/Sources/View/AppRootView.swift @@ -49,7 +49,7 @@ struct AppRootView: View { } } .animation(.easeInOut(duration: Constants.transitionDuration), value: store.route) - .txLoading(isPresented: .constant(store.isCheckingAuth)) + .txLoading(isPresented: store.isCheckingAuth) .onAppear { store.send(.onAppear) } diff --git a/Projects/Feature/Auth/Sources/View/AuthView.swift b/Projects/Feature/Auth/Sources/View/AuthView.swift index 4f6e5df9..3f05ef7d 100644 --- a/Projects/Feature/Auth/Sources/View/AuthView.swift +++ b/Projects/Feature/Auth/Sources/View/AuthView.swift @@ -25,7 +25,7 @@ public struct AuthView: View { foregroundContent } .background(Color.Common.white) - .txLoading(isPresented: .constant(store.isLoading)) + .txLoading(isPresented: store.isLoading) .alert( "로그인 실패", isPresented: Binding( diff --git a/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift b/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift index 47d2749c..c1d56be6 100644 --- a/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift +++ b/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift @@ -112,7 +112,7 @@ public struct GoalDetailView: View { myEmojiFlyingReactionOverlay } .txToast(item: $store.toast, customPadding: 54) - .txLoading(isPresented: $store.isSavingPhotoLog) + .txLoading(isPresented: store.isSavingPhotoLog) } } diff --git a/Projects/Feature/Home/Sources/Goal/EditGoalListView.swift b/Projects/Feature/Home/Sources/Goal/EditGoalListView.swift index 1c0324b8..f610dd33 100644 --- a/Projects/Feature/Home/Sources/Goal/EditGoalListView.swift +++ b/Projects/Feature/Home/Sources/Goal/EditGoalListView.swift @@ -57,7 +57,7 @@ struct EditGoalListView: View { .txToast(item: $store.toast, onButtonTap: { store.send(.toastButtonTapped) }) - .txLoading(isPresented: $store.isLoading) + .txLoading(isPresented: store.isLoading) } } diff --git a/Projects/Feature/MainTab/Sources/View/MainTabView.swift b/Projects/Feature/MainTab/Sources/View/MainTabView.swift index bc3f24bb..f14d6edb 100644 --- a/Projects/Feature/MainTab/Sources/View/MainTabView.swift +++ b/Projects/Feature/MainTab/Sources/View/MainTabView.swift @@ -66,7 +66,7 @@ public struct MainTabView: View { item: $store.home.home.toast, customPadding: Constants.tabBarHeight ) - .txLoading(isPresented: .constant(isTabLoading)) + .txLoading(isPresented: isTabLoading) } } diff --git a/Projects/Feature/MakeGoal/Sources/MakeGoalView.swift b/Projects/Feature/MakeGoal/Sources/MakeGoalView.swift index 556ae2e0..2a8ea549 100644 --- a/Projects/Feature/MakeGoal/Sources/MakeGoalView.swift +++ b/Projects/Feature/MakeGoal/Sources/MakeGoalView.swift @@ -79,11 +79,8 @@ public struct MakeGoalView: View { } ) .txToast(item: $store.toast, customPadding: 70) - .txLoading(isPresented: Binding( - get: { store.isLoading && store.submitMessage == nil }, - set: { _ in } - )) - .txLoading(item: $store.submitMessage) + .txLoading(isPresented: store.isLoading && store.submitMessage == nil) + .txLoading(item: store.submitMessage) } } diff --git a/Projects/Feature/Notification/Sources/NotificationView.swift b/Projects/Feature/Notification/Sources/NotificationView.swift index 0e44c712..26d4f850 100644 --- a/Projects/Feature/Notification/Sources/NotificationView.swift +++ b/Projects/Feature/Notification/Sources/NotificationView.swift @@ -37,7 +37,7 @@ public struct NotificationView: View { store.send(.onAppear) } .toolbar(.hidden, for: .navigationBar) - .txLoading(isPresented: .constant(store.isLoading && store.notifications.isEmpty)) + .txLoading(isPresented: store.isLoading && store.notifications.isEmpty) } } diff --git a/Projects/Feature/Onboarding/Sources/CodeInput/OnboardingCodeInputView.swift b/Projects/Feature/Onboarding/Sources/CodeInput/OnboardingCodeInputView.swift index 1399033f..f515d498 100644 --- a/Projects/Feature/Onboarding/Sources/CodeInput/OnboardingCodeInputView.swift +++ b/Projects/Feature/Onboarding/Sources/CodeInput/OnboardingCodeInputView.swift @@ -50,7 +50,7 @@ public struct OnboardingCodeInputView: View { isTextFieldFocused = false } .txToast(item: $store.toast, customPadding: 76) - .txLoading(isPresented: $store.isLoading) + .txLoading(isPresented: store.isLoading) } } diff --git a/Projects/Feature/Onboarding/Sources/Dday/OnboardingDdayView.swift b/Projects/Feature/Onboarding/Sources/Dday/OnboardingDdayView.swift index 1919eddd..2ed671d7 100644 --- a/Projects/Feature/Onboarding/Sources/Dday/OnboardingDdayView.swift +++ b/Projects/Feature/Onboarding/Sources/Dday/OnboardingDdayView.swift @@ -59,7 +59,7 @@ public struct OnboardingDdayView: View { } ) } - .txLoading(isPresented: $store.isLoading) + .txLoading(isPresented: store.isLoading) } } diff --git a/Projects/Feature/Onboarding/Sources/Profile/OnboardingProfileView.swift b/Projects/Feature/Onboarding/Sources/Profile/OnboardingProfileView.swift index d66d27c4..e7a93492 100644 --- a/Projects/Feature/Onboarding/Sources/Profile/OnboardingProfileView.swift +++ b/Projects/Feature/Onboarding/Sources/Profile/OnboardingProfileView.swift @@ -45,7 +45,7 @@ public struct OnboardingProfileView: View { isTextFieldFocused = false } .txToast(item: $store.toast, customPadding: 76) - .txLoading(isPresented: $store.isLoading) + .txLoading(isPresented: store.isLoading) } } diff --git a/Projects/Feature/ProofPhoto/Sources/ProofPhoto/ProofPhotoView.swift b/Projects/Feature/ProofPhoto/Sources/ProofPhoto/ProofPhotoView.swift index 46889072..2b9f2d54 100644 --- a/Projects/Feature/ProofPhoto/Sources/ProofPhoto/ProofPhotoView.swift +++ b/Projects/Feature/ProofPhoto/Sources/ProofPhoto/ProofPhotoView.swift @@ -74,7 +74,7 @@ public struct ProofPhotoView: View { store.send(.onAppear) } .txToast(item: $store.toast, customPadding: 75) - .txLoading(isPresented: $store.isUploading, message: "업로드 중...") + .txLoading(item: store.isUploading ? "업로드 중..." : nil) } } diff --git a/Projects/Feature/Settings/Sources/Account/AccountView.swift b/Projects/Feature/Settings/Sources/Account/AccountView.swift index 77aa7a8b..c34d63f4 100644 --- a/Projects/Feature/Settings/Sources/Account/AccountView.swift +++ b/Projects/Feature/Settings/Sources/Account/AccountView.swift @@ -32,7 +32,7 @@ struct AccountView: View { store.send(.modalConfirmTapped) } } - .txLoading(isPresented: $store.isLoading) + .txLoading(isPresented: store.isLoading) } } diff --git a/Projects/Feature/Settings/Sources/NotificationSettings/NotificationSettingsView.swift b/Projects/Feature/Settings/Sources/NotificationSettings/NotificationSettingsView.swift index 877c0196..dbcb79cb 100644 --- a/Projects/Feature/Settings/Sources/NotificationSettings/NotificationSettingsView.swift +++ b/Projects/Feature/Settings/Sources/NotificationSettings/NotificationSettingsView.swift @@ -42,7 +42,7 @@ struct NotificationSettingsView: View { store.send(.notificationSettingsOnAppear) } } - .txLoading(isPresented: $store.isNotificationSettingsLoading) + .txLoading(isPresented: store.isNotificationSettingsLoading) } } diff --git a/Projects/Feature/Stats/Sources/Detail/StatsDetailView.swift b/Projects/Feature/Stats/Sources/Detail/StatsDetailView.swift index 0120c054..fa69a413 100644 --- a/Projects/Feature/Stats/Sources/Detail/StatsDetailView.swift +++ b/Projects/Feature/Stats/Sources/Detail/StatsDetailView.swift @@ -64,7 +64,7 @@ struct StatsDetailView: View { } } .txToast(item: $store.toast) - .txLoading(isPresented: $store.isLoading) + .txLoading(isPresented: store.isLoading) } } diff --git a/Projects/Shared/DesignSystem/Sources/Components/Indicator/Loading/TXLoadingPresenter.swift b/Projects/Shared/DesignSystem/Sources/Components/Indicator/Loading/TXLoadingPresenter.swift index 413e970c..546873fc 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/Indicator/Loading/TXLoadingPresenter.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/Indicator/Loading/TXLoadingPresenter.swift @@ -45,40 +45,21 @@ public extension View { /// ## 사용 예시 /// ```swift /// VStack { ... } - /// .txLoading(isPresented: $isLoading) + /// .txLoading(isPresented: store.isLoading) /// ``` - func txLoading(isPresented: Binding) -> some View { - self.modifier(TXLoadingModifier(isPresented: isPresented.wrappedValue, message: nil)) - } - - /// 중앙 캡슐 로딩 뷰를 dimmed70 배경과 함께 표시합니다. - /// - /// ## 사용 예시 - /// ```swift - /// VStack { ... } - /// .txLoading(isPresented: $isLoading, message: "저장 중...") - /// ``` - func txLoading(isPresented: Binding, message: String) -> some View { - self.modifier(TXLoadingModifier(isPresented: isPresented.wrappedValue, message: message)) + func txLoading(isPresented: Bool) -> some View { + self.modifier(TXLoadingModifier(isPresented: isPresented, message: nil)) } /// 중앙 캡슐 로딩 뷰를 `String?` item 기반으로 표시합니다. /// /// ## 사용 예시 /// ```swift - /// @State private var loadingMessage: String? - /// /// VStack { ... } - /// .txLoading(item: $loadingMessage) - /// - /// // 표시 - /// loadingMessage = "업로드 중..." - /// - /// // 숨김 - /// loadingMessage = nil + /// .txLoading(item: store.loadingMessage) /// ``` - func txLoading(item: Binding) -> some View { - self.modifier(TXLoadingModifier(isPresented: item.wrappedValue != nil, message: item.wrappedValue)) + func txLoading(item: String?) -> some View { + self.modifier(TXLoadingModifier(isPresented: item != nil, message: item)) } } @@ -89,20 +70,7 @@ public extension View { var body: some View { Button("토글") { isLoading.toggle() } - .txLoading(isPresented: $isLoading) - } - } - - return PreviewWrapper() -} - -#Preview("캡슐 - isPresented") { - struct PreviewWrapper: View { - @State private var isLoading = false - - var body: some View { - Button("토글") { isLoading.toggle() } - .txLoading(isPresented: $isLoading, message: "저장 중...") + .txLoading(isPresented: isLoading) } } @@ -119,7 +87,7 @@ public extension View { Button("저장") { loadingMessage = "저장 중..." } Button("숨기기") { loadingMessage = nil } } - .txLoading(item: $loadingMessage) + .txLoading(item: loadingMessage) } } From 801226de132bce0a10c35b40d0b8164a980f0862 Mon Sep 17 00:00:00 2001 From: Jihun <75370733+jihun32@users.noreply.github.com> Date: Tue, 12 May 2026 19:59:05 +0900 Subject: [PATCH 07/44] =?UTF-8?q?feat:=20GA=20=EC=85=8B=ED=8C=85=20-=20[TW?= =?UTF-8?q?I-76]=20(#284)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: DomainCommon, FeatureCommon 모듈 생성 - #278 * refactor: 공통 타입들 DomainCommon, FeatureCommon으로 책임 분리 - #278 * refactor: common 분리에 영향받는 파일들 수정 - #278 * refactor: SharedDesignSystem에 있던 임시 TXItem 구현체 Feature에 맞게 분리 - #278 * refactor: TXItem 구현체 Feature분리에 따른 영향 범위 수정 - #278 * feat: CoreAnalytics 모듈 추가 - #281 * feat: AnalyticsClient, Event 구현 - #281 - draft로 올릴 예정이라 Event는 추후 추가 예정 * feat: 유져 로그인/로그아웃 시 Analytics user setting 하도록 구현 - #281 * feat: release일 때 analytics 동작하도록 구현 - #281 * refactor: CoreAnalytics 구조 개선 - #281 - CoreAnalytics에서 Feature책임을 갖고있던 기존 구조에서 Evetn를 protocol로 추상화 - logEvent 클로저로 통합 * feat: Auth 이벤트 심기 - #281 * feat: Onboarding 이벤트 심기 - #281 * feat: Home 이벤트 심기 - #281 * feat: MainTab 이벤트 심기 - #281 * feat: MakeGoal 이벤트 심기 - #281 * feat: GoalDetail 이벤트 심기 - #281 * feat: ProofPhoto 이벤트 심기 - #281 * feat: Stats 이벤트 심기 - #281 * docs: 누락된 주석 추가 - #281 * fix: 리뷰 반영 - #284 * feat: 추가 요청사항 인증샷 이벤트 심기 - #281 * fix: home 목표 생성 선택 이벤트 네이밍 수정 - #281 --------- Co-authored-by: jihun --- Projects/App/Project.swift | 4 +- .../App/Sources/Reducer/AppCoordinator.swift | 82 ++++++++++++++----- .../Interface/Sources/AnalyticsClient.swift | 57 +++++++++++++ .../Interface/Sources/AnalyticsEvent.swift | 24 ++++++ .../Sources/AnalyticsUserProfile.swift | 18 ++++ .../Analytics/Interface/Sources/Source.swift | 8 ++ Projects/Core/Analytics/Project.swift | 42 ++++++++++ .../Sources/AnalyticsClient+Live.swift | 27 ++++++ Projects/Core/Analytics/Sources/Source.swift | 8 ++ .../Analytics/Testing/Sources/Source.swift | 8 ++ .../Core/Analytics/Tests/Sources/Source.swift | 8 ++ .../DTO/PhotoLogCreateRequestDTO.swift | 2 +- .../DTO/PhotoLogCreateResponseDTO.swift | 2 +- .../DTO/PhotoLogUploadURLResponseDTO.swift | 2 +- .../Sources/Endpoint/PhotoLogEndpoint.swift | 2 +- .../Interface/Sources/PhotoLogClient.swift | 2 +- .../Sources/PhotoLogClient+Live.swift | 2 +- .../Stats/Interface/Sources/StatsClient.swift | 1 + Projects/Domain/Stats/Project.swift | 2 + Projects/Feature/Auth/Project.swift | 1 + .../Analytics/AuthAnalyticsEvent.swift | 27 ++++++ .../Auth/Sources/Reducer/AuthReducer.swift | 8 ++ .../Feature/Auth/Sources/View/AuthView.swift | 2 +- Projects/Feature/GoalDetail/Project.swift | 1 + .../Analytics/GoalDetailAnalyticsEvent.swift | 34 ++++++++ .../Detail/FlyingReactionSupport.swift | 2 +- .../Detail/GoalDetailReducer+Impl.swift | 8 ++ .../Interface/Sources/Home/HomeGoalItem.swift | 2 +- Projects/Feature/Home/Project.swift | 2 + .../Analytics/HomeAnalyticsEvent.swift | 29 +++++++ .../Home/Sources/Home/HomeReducer+Impl.swift | 3 + Projects/Feature/MainTab/Project.swift | 1 + .../Analytics/MainTabAnalyticsEvent.swift | 39 +++++++++ .../Sources/Reducer/MainTabReducer.swift | 5 +- .../Interface/Sources/GoalCategory.swift | 2 +- Projects/Feature/MakeGoal/Project.swift | 1 + .../Analytics/MakeGoalAnalyticsEvent.swift | 30 +++++++ .../Sources/MakeGoalReducer+Impl.swift | 21 +++-- Projects/Feature/Onboarding/Project.swift | 1 + .../Analytics/OnboardingAnalyticsEvent.swift | 36 ++++++++ .../Sources/OnboardingCoordinator.swift | 50 +++++++++-- Projects/Feature/ProofPhoto/Project.swift | 1 + .../ProofPhotoAnalyticsEvent+Parameter.swift | 17 ++++ .../Analytics/ProofPhotoAnalyticsEvent.swift | 38 +++++++++ .../ProofPhoto/ProofPhotoReducer+Impl.swift | 17 +++- Projects/Feature/Stats/Project.swift | 1 + .../Analytics/StatsAnalyticsEvent.swift | 27 ++++++ .../Sources/Stats/StatsReducer+Impl.swift | 3 + .../Modal/Content/TXSelectListContent.swift | 2 +- .../Util/Sources/RelativeTimeFormatter.swift | 2 +- Tuist/ProjectDescriptionHelpers/Module.swift | 1 + .../TargetDependency+External.swift | 1 + 52 files changed, 666 insertions(+), 50 deletions(-) create mode 100644 Projects/Core/Analytics/Interface/Sources/AnalyticsClient.swift create mode 100644 Projects/Core/Analytics/Interface/Sources/AnalyticsEvent.swift create mode 100644 Projects/Core/Analytics/Interface/Sources/AnalyticsUserProfile.swift create mode 100644 Projects/Core/Analytics/Interface/Sources/Source.swift create mode 100644 Projects/Core/Analytics/Project.swift create mode 100644 Projects/Core/Analytics/Sources/AnalyticsClient+Live.swift create mode 100644 Projects/Core/Analytics/Sources/Source.swift create mode 100644 Projects/Core/Analytics/Testing/Sources/Source.swift create mode 100644 Projects/Core/Analytics/Tests/Sources/Source.swift create mode 100644 Projects/Feature/Auth/Sources/Analytics/AuthAnalyticsEvent.swift create mode 100644 Projects/Feature/GoalDetail/Sources/Analytics/GoalDetailAnalyticsEvent.swift create mode 100644 Projects/Feature/Home/Sources/Analytics/HomeAnalyticsEvent.swift create mode 100644 Projects/Feature/MainTab/Sources/Analytics/MainTabAnalyticsEvent.swift create mode 100644 Projects/Feature/MakeGoal/Sources/Analytics/MakeGoalAnalyticsEvent.swift create mode 100644 Projects/Feature/Onboarding/Sources/Analytics/OnboardingAnalyticsEvent.swift create mode 100644 Projects/Feature/ProofPhoto/Sources/Analytics/ProofPhotoAnalyticsEvent+Parameter.swift create mode 100644 Projects/Feature/ProofPhoto/Sources/Analytics/ProofPhotoAnalyticsEvent.swift create mode 100644 Projects/Feature/Stats/Sources/Analytics/StatsAnalyticsEvent.swift diff --git a/Projects/App/Project.swift b/Projects/App/Project.swift index 373af6fd..7a2cafce 100644 --- a/Projects/App/Project.swift +++ b/Projects/App/Project.swift @@ -76,7 +76,9 @@ let project = Project( infoPlist: .extendingDefault(with: commonInfoPlist), entitlements: .file(path: "Support/Twix.entitlements"), scripts: [.swiftLint], - dependencies: commonDependencies, + dependencies: commonDependencies + [ + .core(implements: .analytics) + ], settings: .settings( base: commonBuildSettings.merging([ "PROVISIONING_PROFILE_SPECIFIER": "match Development \(Project.Environment.BundleId.bundlePrefix)" diff --git a/Projects/App/Sources/Reducer/AppCoordinator.swift b/Projects/App/Sources/Reducer/AppCoordinator.swift index d287d8e7..6873142a 100644 --- a/Projects/App/Sources/Reducer/AppCoordinator.swift +++ b/Projects/App/Sources/Reducer/AppCoordinator.swift @@ -6,6 +6,9 @@ // import ComposableArchitecture +import CoreAnalytics +import CoreAnalyticsInterface +import CoreLogging import CoreNetworkInterface import CorePushInterface import DomainAuthInterface @@ -32,6 +35,8 @@ struct AppCoordinator { @Dependency(\.notificationClient) var notificationClient + @Dependency(\.analyticsClient) var analyticsClient + private let authReducer: AuthReducer private let onboardingCoordinator: OnboardingCoordinator private let mainTabReducer: MainTabReducer @@ -42,6 +47,7 @@ struct AppCoordinator { var isCheckingAuth: Bool = true var pendingInviteCode: String? var pendingNotificationDeepLink: NotificationDeepLink? + var userProfile: UserProfile? public init() { } } @@ -95,6 +101,9 @@ struct AppCoordinator { // MARK: - FCM Token case registerFCMTokenCompleted case fcmTokenRefreshed(String) + + // MARK: - User Profile + case fetchMyProfileCompleted(UserProfile) } @CasePathable @@ -144,7 +153,7 @@ struct AppCoordinator { state.isCheckingAuth = false state.route = .auth(AuthReducer.State()) return .none - + case let .checkOnboardingStatusResult(.success(status)): state.isCheckingAuth = false switch status { @@ -161,16 +170,22 @@ struct AppCoordinator { notificationClient: notificationClient ) ] - + + if state.userProfile == nil { + effects.append(fetchUserProfile(client: authClient)) + } + // pending 딥링크가 있으면 처리 if let pendingDeepLink = state.pendingNotificationDeepLink { state.pendingNotificationDeepLink = nil effects.append(.send(.route(.mainTab(.notificationDeepLinkReceived(pendingDeepLink))))) } - + return .merge(effects) - - case .coupleConnection, .profileSetup, .anniversarySetup: + + case .coupleConnection, + .profileSetup, + .anniversarySetup: state.route = .onboarding(OnboardingCoordinator.State( initialStatus: status, pendingReceivedCode: state.pendingInviteCode @@ -178,7 +193,7 @@ struct AppCoordinator { state.pendingInviteCode = nil } return .none - + case let .checkOnboardingStatusResult(.failure(error)): state.isCheckingAuth = false if let networkError = error as? NetworkError, @@ -186,31 +201,31 @@ struct AppCoordinator { state.route = .auth(AuthReducer.State()) return .none } - + state.route = .onboarding(OnboardingCoordinator.State( pendingReceivedCode: state.pendingInviteCode )) state.pendingInviteCode = nil return .none - + case let .deepLinkReceived(code): state.pendingInviteCode = code - + if case .onboarding = state.route { return .send(.route(.onboarding(.deepLinkReceived(code: code)))) } return .none - + case let .notificationDeepLinkReceived(deepLink): // 메인탭 상태가 아니면 pending으로 저장 guard case .mainTab = state.route else { state.pendingNotificationDeepLink = deepLink return .none } - + state.pendingNotificationDeepLink = nil return .send(.route(.mainTab(.notificationDeepLinkReceived(deepLink)))) - + case .route(.auth(.delegate(.loginSucceeded))): return .merge( // 1. 온보딩 상태 체크 @@ -228,7 +243,7 @@ struct AppCoordinator { notificationClient: notificationClient ) ) - + case let .route(.onboarding(.delegate(.onboardingCompleted(isPushEnabled, isMarketingEnabled, isNightEnabled)))): state.route = .mainTab(MainTabReducer.State()) // 온보딩 완료 시: initSettings + FCM 토큰 등록 @@ -237,7 +252,7 @@ struct AppCoordinator { .run { [pushClient, notificationClient] _ in // 1. 권한 결과 + 사용자 선택값으로 initSettings 호출 _ = try? await notificationClient.initSettings(isPushEnabled, isMarketingEnabled, isNightEnabled) - + // 2. 권한 허용 시 FCM 토큰 등록 if isPushEnabled { await pushClient.registerForRemoteNotifications() @@ -251,26 +266,29 @@ struct AppCoordinator { subscribeTokenRefreshEffect( pushClient: pushClient, notificationClient: notificationClient - ) + ), + fetchUserProfile(client: authClient) ] - + if let pendingDeepLink = state.pendingNotificationDeepLink { state.pendingNotificationDeepLink = nil effects.append(.send(.route(.mainTab(.notificationDeepLinkReceived(pendingDeepLink))))) } - + return .merge(effects) - + case .route(.onboarding(.delegate(.logoutRequested))): return .run { [authClient] send in try? await authClient.signOut() await send(.checkAuthResult(.failure(NSError(domain: "Logout", code: 0)))) } - + case .route(.mainTab(.delegate(.logoutCompleted))), - .route(.mainTab(.delegate(.withdrawCompleted))), - .route(.mainTab(.delegate(.sessionExpired))): + .route(.mainTab(.delegate(.withdrawCompleted))), + .route(.mainTab(.delegate(.sessionExpired))): state.route = .auth(AuthReducer.State()) + state.userProfile = nil + analyticsClient.setUserProfile(nil) return .none case .route: @@ -287,6 +305,17 @@ struct AppCoordinator { } try? await notificationClient.registerFCMToken(token, deviceId) } + + case let .fetchMyProfileCompleted(profile): + state.userProfile = profile + analyticsClient.setUserProfile( + AnalyticsUserProfile( + id: Int64(profile.id), + name: profile.name + ) + ) + + return .none } } .ifLet(\.route.auth, action: \.route.auth) { @@ -337,3 +366,14 @@ private func subscribeTokenRefreshEffect( } } } + +private func fetchUserProfile(client: AuthClient) -> Effect { + .run { send in + do { + let profile = try await client.fetchMyProfile() + await send(.fetchMyProfileCompleted(profile)) + } catch { + TXLogger(label: "Analytics").error("Failed to fetch user profile for analytics: \(error)") + } + } +} diff --git a/Projects/Core/Analytics/Interface/Sources/AnalyticsClient.swift b/Projects/Core/Analytics/Interface/Sources/AnalyticsClient.swift new file mode 100644 index 00000000..cdac7e0e --- /dev/null +++ b/Projects/Core/Analytics/Interface/Sources/AnalyticsClient.swift @@ -0,0 +1,57 @@ +// +// File.swift +// CoreAnalyticsInterface +// +// Created by 정지훈 on 5/3/26. +// + +import Foundation + +import ComposableArchitecture + +/// 앱의 분석 이벤트와 사용자 식별 정보를 기록하는 클라이언트입니다. +/// +/// ## 사용 예시 +/// ```swift +/// @Dependency(\.analyticsClient) var analyticsClient +/// +/// analyticsClient.setUserProfile((id: 1, name: "Twix")) +/// analyticsClient.logEvent(event) +/// ``` +public struct AnalyticsClient { + public var setUserProfile: (AnalyticsUserProfile?) -> Void + public var logEvent: (any AnalyticsEvent) -> Void + + /// 분석 클라이언트의 동작을 클로저로 주입합니다. + /// + /// ## 사용 예시 + /// ```swift + /// let client = AnalyticsClient( + /// setUserProfile: { _ in }, + /// logEvent: { _ in } + /// ) + /// ``` + public init( + setUserProfile: @escaping (AnalyticsUserProfile?) -> Void, + logEvent: @escaping (any AnalyticsEvent) -> Void + ) { + self.setUserProfile = setUserProfile + self.logEvent = logEvent + } +} + +extension AnalyticsClient: TestDependencyKey { + public static var testValue: AnalyticsClient { + Self( + setUserProfile: { _ in }, + logEvent: { _ in } + ) + } +} + +public extension DependencyValues { + var analyticsClient: AnalyticsClient { + get { self[AnalyticsClient.self] } + set { self[AnalyticsClient.self] = newValue } + } +} diff --git a/Projects/Core/Analytics/Interface/Sources/AnalyticsEvent.swift b/Projects/Core/Analytics/Interface/Sources/AnalyticsEvent.swift new file mode 100644 index 00000000..b799de9c --- /dev/null +++ b/Projects/Core/Analytics/Interface/Sources/AnalyticsEvent.swift @@ -0,0 +1,24 @@ +// +// AnalyticsEvent.swift +// CoreAnalyticsInterface +// +// Created by 정지훈 on 5/3/26. +// + +import Foundation + +/// 분석 도구로 전송할 이벤트가 따라야 하는 공통 인터페이스입니다. +/// +/// ## 사용 예시 +/// ```swift +/// enum HomeAnalyticsEvent: AnalyticsEvent { +/// case homeViewed +/// +/// var name: String { "home_viewed" } +/// var parameters: [String: Any]? { nil } +/// } +/// ``` +public protocol AnalyticsEvent { + var name: String { get } + var parameters: [String: Any]? { get } +} diff --git a/Projects/Core/Analytics/Interface/Sources/AnalyticsUserProfile.swift b/Projects/Core/Analytics/Interface/Sources/AnalyticsUserProfile.swift new file mode 100644 index 00000000..80e0fcb5 --- /dev/null +++ b/Projects/Core/Analytics/Interface/Sources/AnalyticsUserProfile.swift @@ -0,0 +1,18 @@ +// +// AnalyticsUserProfile.swift +// CoreAnalyticsInterface +// +// Created by 정지훈 on 5/12/26. +// + +import Foundation + +public struct AnalyticsUserProfile { + public let id: Int64 + public let name: String + + public init(id: Int64, name: String) { + self.id = id + self.name = name + } +} diff --git a/Projects/Core/Analytics/Interface/Sources/Source.swift b/Projects/Core/Analytics/Interface/Sources/Source.swift new file mode 100644 index 00000000..dd60f38e --- /dev/null +++ b/Projects/Core/Analytics/Interface/Sources/Source.swift @@ -0,0 +1,8 @@ +// +// Source.swift +// +// +// Created by Jihun on 05/03/26. +// + +/// Remove Or Edit diff --git a/Projects/Core/Analytics/Project.swift b/Projects/Core/Analytics/Project.swift new file mode 100644 index 00000000..12f9bdbc --- /dev/null +++ b/Projects/Core/Analytics/Project.swift @@ -0,0 +1,42 @@ +import ProjectDescription +import ProjectDescriptionHelpers + +let project = Project.makeModule( + name: Module.Core.name + Module.Core.analytics.rawValue, + targets: [ + .core( + interface: .analytics, + config: .init( + dependencies: [ + .external(dependency: .ComposableArchitecture) + ] + ) + ), + .core( + implements: .analytics, + config: .init( + dependencies: [ + .core(interface: .analytics), + .external(dependency: .ComposableArchitecture), + .external(dependency: .FirebaseAnalytics) + ] + ) + ), + .core( + testing: .analytics, + config: .init( + dependencies: [ + .core(interface: .analytics) + ] + ) + ), + .core( + tests: .analytics, + config: .init( + dependencies: [ + .core(testing: .analytics) + ] + ) + ) + ] +) diff --git a/Projects/Core/Analytics/Sources/AnalyticsClient+Live.swift b/Projects/Core/Analytics/Sources/AnalyticsClient+Live.swift new file mode 100644 index 00000000..778b9653 --- /dev/null +++ b/Projects/Core/Analytics/Sources/AnalyticsClient+Live.swift @@ -0,0 +1,27 @@ +// +// AnalyticsClient+Live.swift +// CoreAnalytics +// +// Created by 정지훈 on 5/3/26. +// + +import ComposableArchitecture +import CoreAnalyticsInterface +import FirebaseAnalytics +import Foundation + +// Firebase Analytics SDK를 TCA Dependency liveValue로 연결합니다. +extension AnalyticsClient: @retroactive DependencyKey { + public static let liveValue = AnalyticsClient( + setUserProfile: { profile in + Analytics.setUserID(profile.map { String($0.id) }) + Analytics.setUserProperty(profile?.name, forName: "name") + }, + logEvent: { event in + Analytics.logEvent( + event.name, + parameters: event.parameters + ) + } + ) +} diff --git a/Projects/Core/Analytics/Sources/Source.swift b/Projects/Core/Analytics/Sources/Source.swift new file mode 100644 index 00000000..dd60f38e --- /dev/null +++ b/Projects/Core/Analytics/Sources/Source.swift @@ -0,0 +1,8 @@ +// +// Source.swift +// +// +// Created by Jihun on 05/03/26. +// + +/// Remove Or Edit diff --git a/Projects/Core/Analytics/Testing/Sources/Source.swift b/Projects/Core/Analytics/Testing/Sources/Source.swift new file mode 100644 index 00000000..dd60f38e --- /dev/null +++ b/Projects/Core/Analytics/Testing/Sources/Source.swift @@ -0,0 +1,8 @@ +// +// Source.swift +// +// +// Created by Jihun on 05/03/26. +// + +/// Remove Or Edit diff --git a/Projects/Core/Analytics/Tests/Sources/Source.swift b/Projects/Core/Analytics/Tests/Sources/Source.swift new file mode 100644 index 00000000..dd60f38e --- /dev/null +++ b/Projects/Core/Analytics/Tests/Sources/Source.swift @@ -0,0 +1,8 @@ +// +// Source.swift +// +// +// Created by Jihun on 05/03/26. +// + +/// Remove Or Edit diff --git a/Projects/Domain/PhotoLog/Interface/Sources/DTO/PhotoLogCreateRequestDTO.swift b/Projects/Domain/PhotoLog/Interface/Sources/DTO/PhotoLogCreateRequestDTO.swift index 6482f5c9..b64db73a 100644 --- a/Projects/Domain/PhotoLog/Interface/Sources/DTO/PhotoLogCreateRequestDTO.swift +++ b/Projects/Domain/PhotoLog/Interface/Sources/DTO/PhotoLogCreateRequestDTO.swift @@ -2,7 +2,7 @@ // PhotoLogCreateRequestDTO.swift // DomainPhotoLogInterface // -// Created by Codex on 2/6/26. +// Created by Jihun on 2/6/26. // import Foundation diff --git a/Projects/Domain/PhotoLog/Interface/Sources/DTO/PhotoLogCreateResponseDTO.swift b/Projects/Domain/PhotoLog/Interface/Sources/DTO/PhotoLogCreateResponseDTO.swift index 79c273cc..7bf0449f 100644 --- a/Projects/Domain/PhotoLog/Interface/Sources/DTO/PhotoLogCreateResponseDTO.swift +++ b/Projects/Domain/PhotoLog/Interface/Sources/DTO/PhotoLogCreateResponseDTO.swift @@ -2,7 +2,7 @@ // PhotoLogCreateResponseDTO.swift // DomainPhotoLogInterface // -// Created by Codex on 2/6/26. +// Created by Jihun on 2/6/26. // import Foundation diff --git a/Projects/Domain/PhotoLog/Interface/Sources/DTO/PhotoLogUploadURLResponseDTO.swift b/Projects/Domain/PhotoLog/Interface/Sources/DTO/PhotoLogUploadURLResponseDTO.swift index 07578bc1..189bfae1 100644 --- a/Projects/Domain/PhotoLog/Interface/Sources/DTO/PhotoLogUploadURLResponseDTO.swift +++ b/Projects/Domain/PhotoLog/Interface/Sources/DTO/PhotoLogUploadURLResponseDTO.swift @@ -2,7 +2,7 @@ // PhotoLogUploadURLResponseDTO.swift // DomainPhotoLogInterface // -// Created by Codex on 2/6/26. +// Created by Jihun on 2/6/26. // import Foundation diff --git a/Projects/Domain/PhotoLog/Interface/Sources/Endpoint/PhotoLogEndpoint.swift b/Projects/Domain/PhotoLog/Interface/Sources/Endpoint/PhotoLogEndpoint.swift index 6f6d99e0..825bb2ee 100644 --- a/Projects/Domain/PhotoLog/Interface/Sources/Endpoint/PhotoLogEndpoint.swift +++ b/Projects/Domain/PhotoLog/Interface/Sources/Endpoint/PhotoLogEndpoint.swift @@ -2,7 +2,7 @@ // PhotoLogEndpoint.swift // DomainPhotoLogInterface // -// Created by Codex on 2/6/26. +// Created by Jihun on 2/6/26. // import Foundation diff --git a/Projects/Domain/PhotoLog/Interface/Sources/PhotoLogClient.swift b/Projects/Domain/PhotoLog/Interface/Sources/PhotoLogClient.swift index 67cdb21d..ec383e10 100644 --- a/Projects/Domain/PhotoLog/Interface/Sources/PhotoLogClient.swift +++ b/Projects/Domain/PhotoLog/Interface/Sources/PhotoLogClient.swift @@ -2,7 +2,7 @@ // PhotoLogClient.swift // DomainPhotoLogInterface // -// Created by Codex on 2/6/26. +// Created by Jihun on 2/6/26. // import ComposableArchitecture diff --git a/Projects/Domain/PhotoLog/Sources/PhotoLogClient+Live.swift b/Projects/Domain/PhotoLog/Sources/PhotoLogClient+Live.swift index 12a8ef28..d7216cfd 100644 --- a/Projects/Domain/PhotoLog/Sources/PhotoLogClient+Live.swift +++ b/Projects/Domain/PhotoLog/Sources/PhotoLogClient+Live.swift @@ -2,7 +2,7 @@ // PhotoLogClient+Live.swift // DomainPhotoLog // -// Created by Codex on 2/6/26. +// Created by Jihun on 2/6/26. // import ComposableArchitecture diff --git a/Projects/Domain/Stats/Interface/Sources/StatsClient.swift b/Projects/Domain/Stats/Interface/Sources/StatsClient.swift index 6875239b..afa73f8b 100644 --- a/Projects/Domain/Stats/Interface/Sources/StatsClient.swift +++ b/Projects/Domain/Stats/Interface/Sources/StatsClient.swift @@ -9,6 +9,7 @@ import Foundation import ComposableArchitecture import CoreNetworkInterface +import DomainCommonInterface /// 통계 데이터 조회 API를 추상화한 클라이언트입니다. /// diff --git a/Projects/Domain/Stats/Project.swift b/Projects/Domain/Stats/Project.swift index 1752e4ab..98e8c906 100644 --- a/Projects/Domain/Stats/Project.swift +++ b/Projects/Domain/Stats/Project.swift @@ -8,6 +8,7 @@ let project = Project.makeModule( interface: .stats, config: .init( dependencies: [ + .domain(interface: .common), .core(interface: .network), .external(dependency: .ComposableArchitecture) ] @@ -17,6 +18,7 @@ let project = Project.makeModule( implements: .stats, config: .init( dependencies: [ + .domain(interface: .common), .domain(interface: .stats), .core(interface: .network), .external(dependency: .ComposableArchitecture) diff --git a/Projects/Feature/Auth/Project.swift b/Projects/Feature/Auth/Project.swift index 7e4df56f..259325a3 100644 --- a/Projects/Feature/Auth/Project.swift +++ b/Projects/Feature/Auth/Project.swift @@ -21,6 +21,7 @@ let project = Project.makeModule( .domain(interface: .auth), .domain(implements: .auth), .core(implements: .logging), + .core(interface: .analytics), .shared(implements: .designSystem), .external(dependency: .ComposableArchitecture) ] diff --git a/Projects/Feature/Auth/Sources/Analytics/AuthAnalyticsEvent.swift b/Projects/Feature/Auth/Sources/Analytics/AuthAnalyticsEvent.swift new file mode 100644 index 00000000..8d08a177 --- /dev/null +++ b/Projects/Feature/Auth/Sources/Analytics/AuthAnalyticsEvent.swift @@ -0,0 +1,27 @@ +// +// AuthAnalyticsEvent.swift +// FeatureAuth +// +// Created by Jihun on 5/9/26. +// + +import CoreAnalyticsInterface +import Foundation + +enum AuthAnalyticsEvent: AnalyticsEvent { + case loginViewed + + var name: String { + switch self { + case .loginViewed: + "login_viewed" + } + } + + var parameters: [String: Any]? { + switch self { + case .loginViewed: + nil + } + } +} diff --git a/Projects/Feature/Auth/Sources/Reducer/AuthReducer.swift b/Projects/Feature/Auth/Sources/Reducer/AuthReducer.swift index 00edc8f9..a2bc489e 100644 --- a/Projects/Feature/Auth/Sources/Reducer/AuthReducer.swift +++ b/Projects/Feature/Auth/Sources/Reducer/AuthReducer.swift @@ -6,6 +6,7 @@ // import ComposableArchitecture +import CoreAnalyticsInterface import CoreLogging import DomainAuthInterface import Foundation @@ -24,6 +25,8 @@ import Foundation /// ``` @Reducer public struct AuthReducer { + @Dependency(\.analyticsClient) var analyticsClient + @ObservableState public struct State: Equatable { public var isLoading = false @@ -34,6 +37,7 @@ public struct AuthReducer { } public enum Action { + case onAppear case appleLoginButtonTapped case kakaoLoginButtonTapped case googleLoginButtonTapped @@ -52,6 +56,10 @@ public struct AuthReducer { public var body: some ReducerOf { Reduce { state, action in switch action { + case .onAppear: + analyticsClient.logEvent(AuthAnalyticsEvent.loginViewed) + return .none + case .appleLoginButtonTapped: return Self.handleLogin(provider: .apple, state: &state) diff --git a/Projects/Feature/Auth/Sources/View/AuthView.swift b/Projects/Feature/Auth/Sources/View/AuthView.swift index 3f05ef7d..afc16ae6 100644 --- a/Projects/Feature/Auth/Sources/View/AuthView.swift +++ b/Projects/Feature/Auth/Sources/View/AuthView.swift @@ -24,7 +24,7 @@ public struct AuthView: View { backgroundIllustration foregroundContent } - .background(Color.Common.white) + .background(Color.Common.white) .txLoading(isPresented: store.isLoading) .alert( "로그인 실패", diff --git a/Projects/Feature/GoalDetail/Project.swift b/Projects/Feature/GoalDetail/Project.swift index a1ed3447..f406f3d1 100644 --- a/Projects/Feature/GoalDetail/Project.swift +++ b/Projects/Feature/GoalDetail/Project.swift @@ -20,6 +20,7 @@ let project = Project.makeModule( dependencies: [ .feature(interface: .goalDetail), .feature(interface: .proofPhoto), + .core(interface: .analytics), .core(interface: .captureSession), .domain(interface: .photoLog), .shared(implements: .designSystem), diff --git a/Projects/Feature/GoalDetail/Sources/Analytics/GoalDetailAnalyticsEvent.swift b/Projects/Feature/GoalDetail/Sources/Analytics/GoalDetailAnalyticsEvent.swift new file mode 100644 index 00000000..ca3671d1 --- /dev/null +++ b/Projects/Feature/GoalDetail/Sources/Analytics/GoalDetailAnalyticsEvent.swift @@ -0,0 +1,34 @@ +// +// GoalDetailAnalyticsEvent.swift +// FeatureGoalDetail +// +// Created by Jihun on 5/9/26. +// + +import CoreAnalyticsInterface +import Foundation + +enum GoalDetailAnalyticsEvent: AnalyticsEvent { + case emojiReactionSent(emoji: String) + case pokeSent + + var name: String { + switch self { + case .emojiReactionSent: + "emoji_reaction_sent" + case .pokeSent: + "poke_sent" + } + } + + var parameters: [String: Any]? { + switch self { + case let .emojiReactionSent(emoji): + [ + "emoji": emoji + ] + case .pokeSent: + nil + } + } +} diff --git a/Projects/Feature/GoalDetail/Sources/Detail/FlyingReactionSupport.swift b/Projects/Feature/GoalDetail/Sources/Detail/FlyingReactionSupport.swift index ed401d7d..8e701d79 100644 --- a/Projects/Feature/GoalDetail/Sources/Detail/FlyingReactionSupport.swift +++ b/Projects/Feature/GoalDetail/Sources/Detail/FlyingReactionSupport.swift @@ -2,7 +2,7 @@ // FlyingReactionSupport.swift // FeatureGoalDetail // -// Created by Codex on 2/25/26. +// Created by Jihun on 2/25/26. // import SwiftUI diff --git a/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailReducer+Impl.swift b/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailReducer+Impl.swift index e4636d3c..04ebd4a5 100644 --- a/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailReducer+Impl.swift +++ b/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailReducer+Impl.swift @@ -8,6 +8,7 @@ import Foundation import ComposableArchitecture +import CoreAnalyticsInterface import CoreCaptureSessionInterface import DomainGoalInterface import DomainPhotoLogInterface @@ -77,6 +78,8 @@ extension GoalDetailReducer { @Dependency(\.captureSessionClient) var captureSessionClient @Dependency(\.goalClient) var goalClient @Dependency(\.photoLogClient) var photoLogClient + @Dependency(\.analyticsClient) var analyticsClient + let timeFormatter = RelativeTimeFormatter() // swiftlint: disable closure_body_length @@ -121,6 +124,7 @@ extension GoalDetailReducer { PokeCooldownManager.recordPoke(goalId: goalId) do { try await goalClient.pokePartner(goalId) + analyticsClient.logEvent(GoalDetailAnalyticsEvent.pokeSent) await send(.showToast(.poke(message: "상대방을 찔렀어요!"))) } catch { PokeCooldownManager.removePoke(goalId: goalId) @@ -154,6 +158,10 @@ extension GoalDetailReducer { do { let request = PhotoLogUpdateReactionRequestDTO(reaction: reactionEmoji.rawValue) _ = try await photoLogClient.updateReaction(photoLogId, request) + analyticsClient + .logEvent( + GoalDetailAnalyticsEvent.emojiReactionSent(emoji: reactionEmoji.rawValue) + ) } catch { await send(.reactionUpdateFailed(previousReaction: previousReaction)) } diff --git a/Projects/Feature/Home/Interface/Sources/Home/HomeGoalItem.swift b/Projects/Feature/Home/Interface/Sources/Home/HomeGoalItem.swift index dd44c59c..7e443a93 100644 --- a/Projects/Feature/Home/Interface/Sources/Home/HomeGoalItem.swift +++ b/Projects/Feature/Home/Interface/Sources/Home/HomeGoalItem.swift @@ -2,7 +2,7 @@ // HomeGoalItem.swift // FeatureHomeInterface // -// Created by Codex on 4/10/26. +// Created by Jihun on 4/10/26. // import Foundation diff --git a/Projects/Feature/Home/Project.swift b/Projects/Feature/Home/Project.swift index ee379c1e..ecb166ef 100644 --- a/Projects/Feature/Home/Project.swift +++ b/Projects/Feature/Home/Project.swift @@ -17,6 +17,7 @@ let project = Project.makeModule( .feature(interface: .makeGoal), .feature(interface: .settings), .feature(interface: .stats), + .core(interface: .analytics), .shared(implements: .designSystem), .external(dependency: .ComposableArchitecture) ] @@ -38,6 +39,7 @@ let project = Project.makeModule( .feature(interface: .settings), .feature(interface: .stats), .feature(interface: .home), + .core(interface: .analytics), .shared(implements: .designSystem), .shared(implements: .util), .external(dependency: .ComposableArchitecture) diff --git a/Projects/Feature/Home/Sources/Analytics/HomeAnalyticsEvent.swift b/Projects/Feature/Home/Sources/Analytics/HomeAnalyticsEvent.swift new file mode 100644 index 00000000..32cae7a6 --- /dev/null +++ b/Projects/Feature/Home/Sources/Analytics/HomeAnalyticsEvent.swift @@ -0,0 +1,29 @@ +// +// HomeAnalyticsEvent.swift +// FeatureHome +// +// Created by Jihun on 5/9/26. +// + +import CoreAnalyticsInterface +import Foundation + +enum HomeAnalyticsEvent: AnalyticsEvent { + case selectGoalClicked(kind: String) + + var name: String { + switch self { + case .selectGoalClicked: + "select_goal_clicked" + } + } + + var parameters: [String: Any]? { + switch self { + case let .selectGoalClicked(kind): + [ + "kind": kind + ] + } + } +} diff --git a/Projects/Feature/Home/Sources/Home/HomeReducer+Impl.swift b/Projects/Feature/Home/Sources/Home/HomeReducer+Impl.swift index ea98b1d5..940113a0 100644 --- a/Projects/Feature/Home/Sources/Home/HomeReducer+Impl.swift +++ b/Projects/Feature/Home/Sources/Home/HomeReducer+Impl.swift @@ -9,6 +9,7 @@ import Foundation import SwiftUI import ComposableArchitecture +import CoreAnalyticsInterface import CoreCaptureSessionInterface import DomainGoalInterface import DomainNotificationInterface @@ -93,6 +94,7 @@ extension HomeReducer { @Dependency(\.captureSessionClient) var captureSessionClient @Dependency(\.photoLogClient) var photoLogClient @Dependency(\.notificationClient) var notificationClient + @Dependency(\.analyticsClient) var analyticsClient // swiftlint:disable:next closure_body_length let reducer = Reduce { state, action in @@ -269,6 +271,7 @@ extension HomeReducer { case let .addGoalButtonTapped(category): state.isAddGoalPresented = false + analyticsClient.logEvent(HomeAnalyticsEvent.selectGoalClicked(kind: category.rawValue)) return .send(.delegate(.goToMakeGoal(category))) case .editButtonTapped: diff --git a/Projects/Feature/MainTab/Project.swift b/Projects/Feature/MainTab/Project.swift index b76f019d..4ae1062c 100644 --- a/Projects/Feature/MainTab/Project.swift +++ b/Projects/Feature/MainTab/Project.swift @@ -19,6 +19,7 @@ let project = Project.makeModule( .feature(interface: .makeGoal), .core(implements: .logging), .core(interface: .push), + .core(interface: .analytics), .external(dependency: .ComposableArchitecture) ] + Module.Feature.allCases.filter { $0 != .mainTab }.map { .feature(implements: $0) } ) diff --git a/Projects/Feature/MainTab/Sources/Analytics/MainTabAnalyticsEvent.swift b/Projects/Feature/MainTab/Sources/Analytics/MainTabAnalyticsEvent.swift new file mode 100644 index 00000000..ac9e358b --- /dev/null +++ b/Projects/Feature/MainTab/Sources/Analytics/MainTabAnalyticsEvent.swift @@ -0,0 +1,39 @@ +// +// MainTabAnalyticsEvent.swift +// FeatureMainTab +// +// Created by Jihun on 5/9/26. +// + +import CoreAnalyticsInterface +import CorePushInterface +import Foundation + +enum MainTabAnalyticsEvent: AnalyticsEvent { + case openedByPush(deepLink: NotificationDeepLink) + + var name: String { + switch self { + case .openedByPush: + "open_by_push" + } + } + + var parameters: [String: Any]? { + let type: String + switch self { + case let .openedByPush(deepLink): + switch deepLink { + case .poke: type = "poke" + case .partnerConnected: type = "partner_connected" + case .goalCompleted: type = "goal_completed" + case .reaction: type = "reaction" + case .dailyGoalAchieved: type = "daily_goal_achieved" + case .goalEnded: type = "goal_ended" + case .marketing: type = "marketing" + } + } + + return ["type": type] + } +} diff --git a/Projects/Feature/MainTab/Sources/Reducer/MainTabReducer.swift b/Projects/Feature/MainTab/Sources/Reducer/MainTabReducer.swift index e062e87e..54a9ddf5 100644 --- a/Projects/Feature/MainTab/Sources/Reducer/MainTabReducer.swift +++ b/Projects/Feature/MainTab/Sources/Reducer/MainTabReducer.swift @@ -8,6 +8,7 @@ import Foundation import ComposableArchitecture +import CoreAnalyticsInterface import CoreLogging import CorePushInterface import DomainNotificationInterface @@ -102,6 +103,7 @@ public struct MainTabReducer { public init() { } @Dependency(\.notificationClient) var notificationClient + @Dependency(\.analyticsClient) var analyticsClient public var body: some ReducerOf { BindingReducer() @@ -147,7 +149,8 @@ public struct MainTabReducer { case let .notificationDeepLinkReceived(deepLink): state.selectedTab = .home state.home.routes = [] - + analyticsClient.logEvent(MainTabAnalyticsEvent.openedByPush(deepLink: deepLink)) + let notificationIdString = deepLink.notificationId let markAsReadEffect: Effect = .run { [notificationClient] _ in guard let notificationId = Int64(notificationIdString) else { return } diff --git a/Projects/Feature/MakeGoal/Interface/Sources/GoalCategory.swift b/Projects/Feature/MakeGoal/Interface/Sources/GoalCategory.swift index e21966fe..c3fe1008 100644 --- a/Projects/Feature/MakeGoal/Interface/Sources/GoalCategory.swift +++ b/Projects/Feature/MakeGoal/Interface/Sources/GoalCategory.swift @@ -18,7 +18,7 @@ import SharedDesignSystem /// let category = GoalCategory.health /// print(category.title) /// ``` -public enum GoalCategory: CaseIterable, Equatable { +public enum GoalCategory: String, CaseIterable, Equatable { case custom case health case vitamin diff --git a/Projects/Feature/MakeGoal/Project.swift b/Projects/Feature/MakeGoal/Project.swift index cdba5d83..3e67707c 100644 --- a/Projects/Feature/MakeGoal/Project.swift +++ b/Projects/Feature/MakeGoal/Project.swift @@ -23,6 +23,7 @@ let project = Project.makeModule( dependencies: [ .feature(interface: .makeGoal), .feature(interface: .common), + .core(interface: .analytics), .domain(interface: .goal), .shared(implements: .designSystem), .external(dependency: .ComposableArchitecture) diff --git a/Projects/Feature/MakeGoal/Sources/Analytics/MakeGoalAnalyticsEvent.swift b/Projects/Feature/MakeGoal/Sources/Analytics/MakeGoalAnalyticsEvent.swift new file mode 100644 index 00000000..f0a80303 --- /dev/null +++ b/Projects/Feature/MakeGoal/Sources/Analytics/MakeGoalAnalyticsEvent.swift @@ -0,0 +1,30 @@ +// +// MakeGoalAnalyticsEvent.swift +// FeatureMakeGoal +// +// Created by Jihun on 5/9/26. +// + +import CoreAnalyticsInterface +import Foundation + +enum MakeGoalAnalyticsEvent: AnalyticsEvent { + case created(goalId: Int64, kind: String) + + var name: String { + switch self { + case .created: + "goal_created" + } + } + + var parameters: [String: Any]? { + switch self { + case let .created(goalId, kind): + [ + "goal_id": "\(goalId)", + "kind": kind + ] + } + } +} diff --git a/Projects/Feature/MakeGoal/Sources/MakeGoalReducer+Impl.swift b/Projects/Feature/MakeGoal/Sources/MakeGoalReducer+Impl.swift index 5f192c63..a75ac514 100644 --- a/Projects/Feature/MakeGoal/Sources/MakeGoalReducer+Impl.swift +++ b/Projects/Feature/MakeGoal/Sources/MakeGoalReducer+Impl.swift @@ -8,6 +8,7 @@ import Foundation import ComposableArchitecture +import CoreAnalyticsInterface import DomainGoalInterface import FeatureMakeGoalInterface import SharedDesignSystem @@ -16,12 +17,14 @@ extension MakeGoalReducer { // swiftlint:disable:next function_body_length public init() { @Dependency(\.goalClient) var goalClient + @Dependency(\.analyticsClient) var analyticsClient // swiftlint:disable:next closure_body_length let reducer = Reduce { state, action in switch action { // MARK: - LifeCycle case .onAppear: - if case .edit = state.mode, let goalId = state.editingGoalId { + if case .edit = state.mode, + let goalId = state.editingGoalId { state.isLoading = true return .run { send in do { @@ -36,7 +39,7 @@ extension MakeGoalReducer { case .onDisappear: return .none - + case let .fetchGoalCompleted(goal): state.isLoading = false state.goalTitle = goal.title @@ -67,7 +70,7 @@ extension MakeGoalReducer { state.isEndDateOn = true } return .send(.updateDateText) - + case .fetchGoalFailed: state.isLoading = false return .send(.showToast(.warning(message: "목표 정보를 불러오지 못했어요"))) @@ -203,11 +206,11 @@ extension MakeGoalReducer { state.isLoading = true let endDateString: String? = state.isEndDateOn - ? TXCalendarUtil.apiDateString(for: state.endDate) - : nil - + ? TXCalendarUtil.apiDateString(for: state.endDate) + : nil switch state.mode { case .add: + let category = state.category.rawValue state.submitMessage = "등록 중..." let request = GoalCreateRequestDTO( name: state.goalTitle, @@ -219,7 +222,11 @@ extension MakeGoalReducer { ) return .run { send in do { - _ = try await goalClient.createGoal(request) + let goal = try await goalClient.createGoal(request) + analyticsClient + .logEvent( + MakeGoalAnalyticsEvent.created(goalId: goal.id, kind: category) + ) await send(.delegate(.navigateBack)) } catch { await send(.createGoalFailed) diff --git a/Projects/Feature/Onboarding/Project.swift b/Projects/Feature/Onboarding/Project.swift index f13d210e..2bab7477 100644 --- a/Projects/Feature/Onboarding/Project.swift +++ b/Projects/Feature/Onboarding/Project.swift @@ -14,6 +14,7 @@ let project = Project.makeModule( resources: ["Resources/**"], dependencies: [ .feature(interface: .onboarding), + .core(interface: .analytics), .domain(interface: .onboarding), .core(interface: .push), .shared(implements: .designSystem), diff --git a/Projects/Feature/Onboarding/Sources/Analytics/OnboardingAnalyticsEvent.swift b/Projects/Feature/Onboarding/Sources/Analytics/OnboardingAnalyticsEvent.swift new file mode 100644 index 00000000..c1636d10 --- /dev/null +++ b/Projects/Feature/Onboarding/Sources/Analytics/OnboardingAnalyticsEvent.swift @@ -0,0 +1,36 @@ +// +// OnboardingAnalyticsEvent.swift +// FeatureOnboarding +// +// Created by Jihun on 5/9/26. +// + +import CoreAnalyticsInterface +import Foundation + +enum OnboardingAnalyticsEvent: Hashable, AnalyticsEvent { + case inviteViewed + case profileSetupViewed + case anniversarySetupViewed + case onboardingCompleted + + var name: String { + switch self { + case .inviteViewed: + "invite_viewed" + case .profileSetupViewed: + "profile_setup_viewed" + case .anniversarySetupViewed: + "anniversary_setup_viewed" + case .onboardingCompleted: + "onboarding_completed" + } + } + + var parameters: [String: Any]? { + switch self { + case .inviteViewed, .profileSetupViewed, .anniversarySetupViewed, .onboardingCompleted: + nil + } + } +} diff --git a/Projects/Feature/Onboarding/Sources/OnboardingCoordinator.swift b/Projects/Feature/Onboarding/Sources/OnboardingCoordinator.swift index 2238a17d..571e9c93 100644 --- a/Projects/Feature/Onboarding/Sources/OnboardingCoordinator.swift +++ b/Projects/Feature/Onboarding/Sources/OnboardingCoordinator.swift @@ -6,6 +6,7 @@ // import ComposableArchitecture +import CoreAnalyticsInterface import CorePushInterface import DomainOnboardingInterface import Foundation @@ -30,6 +31,7 @@ public struct OnboardingCoordinator { private var pushClient @Dependency(\.continuousClock) private var clock + @Dependency(\.analyticsClient) private var analyticsClient private enum CancelID { case couplePolling @@ -38,6 +40,7 @@ public struct OnboardingCoordinator { @ObservableState public struct State: Equatable { var routes: [OnboardingRoute] = [] + var loggedAnalyticsEvents: Set = [] var connect: OnboardingConnectReducer.State var codeInput: OnboardingCodeInputReducer.State? var profile: OnboardingProfileReducer.State? @@ -146,8 +149,21 @@ public struct OnboardingCoordinator { // MARK: - LifeCycle case .onAppear: - // 커플 연결 단계가 아니면 폴링 불필요 - guard state.initialStatus == .coupleConnection else { return .none } + switch state.initialStatus { + case .coupleConnection: + logOnboardingEvent(.inviteViewed, state: &state, analyticsClient: analyticsClient) + + case .profileSetup: + logOnboardingEvent(.profileSetupViewed, state: &state, analyticsClient: analyticsClient) + return .none + + case .anniversarySetup: + logOnboardingEvent(.anniversarySetupViewed, state: &state, analyticsClient: analyticsClient) + return .none + + case .completed: + return .none + } var effects: [Effect] = [.send(.startCouplePolling)] @@ -209,6 +225,7 @@ public struct OnboardingCoordinator { state.profile = OnboardingProfileReducer.State() state.routes.removeAll(where: { $0 == .codeInput }) state.routes.append(.profile) + logOnboardingEvent(.profileSetupViewed, state: &state, analyticsClient: analyticsClient) return .cancel(id: CancelID.couplePolling) case .coupleConnection: @@ -259,6 +276,7 @@ public struct OnboardingCoordinator { receivedCode: receivedCode ) state.routes.append(.codeInput) + logOnboardingEvent(.inviteViewed, state: &state, analyticsClient: analyticsClient) return .none // MARK: - Deep Link @@ -275,6 +293,7 @@ public struct OnboardingCoordinator { receivedCode: code ) state.routes.append(.codeInput) + logOnboardingEvent(.inviteViewed, state: &state, analyticsClient: analyticsClient) } return .none @@ -293,6 +312,7 @@ public struct OnboardingCoordinator { ) state.pendingReceivedCode = nil state.routes.append(.codeInput) + logOnboardingEvent(.inviteViewed, state: &state, analyticsClient: analyticsClient) return .none case .connect: @@ -309,6 +329,7 @@ public struct OnboardingCoordinator { state.isCouplePolling = false state.profile = OnboardingProfileReducer.State() state.routes.append(.profile) + logOnboardingEvent(.profileSetupViewed, state: &state, analyticsClient: analyticsClient) return .cancel(id: CancelID.couplePolling) case .codeInput: @@ -345,6 +366,7 @@ public struct OnboardingCoordinator { // 기념일 설정 필요 → Dday로 이동 state.dday = OnboardingDdayReducer.State() state.routes.append(.dday) + logOnboardingEvent(.anniversarySetupViewed, state: &state, analyticsClient: analyticsClient) return .none default: @@ -353,6 +375,7 @@ public struct OnboardingCoordinator { // 일단 Dday로 진행 (사용자 경험 우선) state.dday = OnboardingDdayReducer.State() state.routes.append(.dday) + logOnboardingEvent(.anniversarySetupViewed, state: &state, analyticsClient: analyticsClient) return .none } @@ -361,6 +384,7 @@ public struct OnboardingCoordinator { // 프로필 등록은 이미 성공했으므로 다음 단계로 진행하는 것이 UX에 유리 state.dday = OnboardingDdayReducer.State() state.routes.append(.dday) + logOnboardingEvent(.anniversarySetupViewed, state: &state, analyticsClient: analyticsClient) return .none // MARK: - Dday Delegate @@ -389,11 +413,14 @@ public struct OnboardingCoordinator { case let .notificationModalConfirmed(isMarketing, isNight): state.isNotificationModalPresented = false - return .send(.delegate(.onboardingCompleted( - isPushEnabled: state.isPushPermissionGranted, - isMarketingEnabled: isMarketing, - isNightEnabled: isNight - ))) + logOnboardingEvent(.onboardingCompleted, state: &state, analyticsClient: analyticsClient) + return .send( + .delegate(.onboardingCompleted( + isPushEnabled: state.isPushPermissionGranted, + isMarketingEnabled: isMarketing, + isNightEnabled: isNight + )) + ) case .delegate: return .none @@ -415,3 +442,12 @@ private func popLastRoute(_ routes: inout [OnboardingRoute]) { guard !routes.isEmpty else { return } routes.removeLast() } + +private func logOnboardingEvent( + _ event: OnboardingAnalyticsEvent, + state: inout OnboardingCoordinator.State, + analyticsClient: AnalyticsClient +) { + guard state.loggedAnalyticsEvents.insert(event).inserted else { return } + analyticsClient.logEvent(event) +} diff --git a/Projects/Feature/ProofPhoto/Project.swift b/Projects/Feature/ProofPhoto/Project.swift index 3f446de0..f12dd547 100644 --- a/Projects/Feature/ProofPhoto/Project.swift +++ b/Projects/Feature/ProofPhoto/Project.swift @@ -21,6 +21,7 @@ let project = Project.makeModule( dependencies: [ .feature(interface: .proofPhoto), .core(interface: .captureSession), + .core(interface: .analytics), .domain(interface: .goal), .domain(interface: .photoLog), .shared(implements: .designSystem), diff --git a/Projects/Feature/ProofPhoto/Sources/Analytics/ProofPhotoAnalyticsEvent+Parameter.swift b/Projects/Feature/ProofPhoto/Sources/Analytics/ProofPhotoAnalyticsEvent+Parameter.swift new file mode 100644 index 00000000..47383d92 --- /dev/null +++ b/Projects/Feature/ProofPhoto/Sources/Analytics/ProofPhotoAnalyticsEvent+Parameter.swift @@ -0,0 +1,17 @@ +// +// ProofPhotoAnalyticsEvent+Parameter.swift +// FeatureProofPhoto +// +// Created by 정지훈 on 5/12/26. +// + +import Foundation + +extension ProofPhotoAnalyticsEvent { + struct Upload { + let goalId: Int64 + let targetDate: String + let durationMS: Double + let fileSizeKB: Double + } +} diff --git a/Projects/Feature/ProofPhoto/Sources/Analytics/ProofPhotoAnalyticsEvent.swift b/Projects/Feature/ProofPhoto/Sources/Analytics/ProofPhotoAnalyticsEvent.swift new file mode 100644 index 00000000..1f1864c3 --- /dev/null +++ b/Projects/Feature/ProofPhoto/Sources/Analytics/ProofPhotoAnalyticsEvent.swift @@ -0,0 +1,38 @@ +// +// ProofPhotoAnalyticsEvent.swift +// FeatureProofPhoto +// +// Created by Jihun on 5/9/26. +// + +import CoreAnalyticsInterface +import Foundation + +enum ProofPhotoAnalyticsEvent: AnalyticsEvent { + case uploaded(Upload) + case opened + + var name: String { + switch self { + case .uploaded: + "photo_uploaded" + + case .opened: + "proof_photo_opened" + } + } + + var parameters: [String: Any]? { + switch self { + case let .uploaded(parameter): + [ + "goal_id": "\(parameter.goalId)", + "target_Date": parameter.targetDate, + "duration_ms": parameter.durationMS, + "file_size_kb": parameter.fileSizeKB + ] + + case .opened: nil + } + } +} diff --git a/Projects/Feature/ProofPhoto/Sources/ProofPhoto/ProofPhotoReducer+Impl.swift b/Projects/Feature/ProofPhoto/Sources/ProofPhoto/ProofPhotoReducer+Impl.swift index 2fdf196e..7a36bdd1 100644 --- a/Projects/Feature/ProofPhoto/Sources/ProofPhoto/ProofPhotoReducer+Impl.swift +++ b/Projects/Feature/ProofPhoto/Sources/ProofPhoto/ProofPhotoReducer+Impl.swift @@ -7,10 +7,12 @@ import AVFoundation import ComposableArchitecture +import CoreAnalyticsInterface import CoreCaptureSessionInterface import DomainGoalInterface import DomainPhotoLogInterface import FeatureProofPhotoInterface +import Foundation import PhotosUI import SharedDesignSystem import SharedUtil @@ -26,6 +28,7 @@ extension ProofPhotoReducer { public init() { @Dependency(\.captureSessionClient) var captureSessionClient @Dependency(\.photoLogClient) var photoLogClient + @Dependency(\.analyticsClient) var analyticsClient // swiftlint: disable closure_body_length let reducer = Reduce { state, action in @@ -36,7 +39,7 @@ extension ProofPhotoReducer { return .run { [isFlashOn = state.isFlashOn] send in captureSessionClient.setFlashEnabled(isFlashOn) let session = await captureSessionClient.setUpCaptureSession(.back) - + analyticsClient.logEvent(ProofPhotoAnalyticsEvent.opened) await send(.setupCaptureSessionCompleted(session: session)) } @@ -133,6 +136,7 @@ extension ProofPhotoReducer { return .run { send in do { + let uploadStartedAt = Date() let optimizedImageData = ImageUploadOptimizer.optimizedJPEGData(from: imageData) let uploadResponse = try await photoLogClient.fetchUploadURL(goalId) try await photoLogClient.uploadImageData(optimizedImageData, uploadResponse.uploadUrl) @@ -154,6 +158,17 @@ extension ProofPhotoReducer { reaction: nil, createdAt: "방금" ) + analyticsClient + .logEvent( + ProofPhotoAnalyticsEvent.uploaded( + .init( + goalId: goalId, + targetDate: verificationDate, + durationMS: Date().timeIntervalSince(uploadStartedAt) * 1000, + fileSizeKB: Double(optimizedImageData.count) / 1024 + ) + ) + ) await send( .delegate( .completedUploadPhoto( diff --git a/Projects/Feature/Stats/Project.swift b/Projects/Feature/Stats/Project.swift index d78d04c0..45c10a5e 100644 --- a/Projects/Feature/Stats/Project.swift +++ b/Projects/Feature/Stats/Project.swift @@ -28,6 +28,7 @@ let project = Project.makeModule( .feature(interface: .makeGoal), .domain(implements: .stats), .domain(interface: .stats), + .core(interface: .analytics), .shared(implements: .designSystem), .external(dependency: .ComposableArchitecture) ] diff --git a/Projects/Feature/Stats/Sources/Analytics/StatsAnalyticsEvent.swift b/Projects/Feature/Stats/Sources/Analytics/StatsAnalyticsEvent.swift new file mode 100644 index 00000000..6b8dc4c5 --- /dev/null +++ b/Projects/Feature/Stats/Sources/Analytics/StatsAnalyticsEvent.swift @@ -0,0 +1,27 @@ +// +// StatsAnalyticsEvent.swift +// FeatureStats +// +// Created by Jihun on 5/9/26. +// + +import CoreAnalyticsInterface +import Foundation + +enum StatsAnalyticsEvent: AnalyticsEvent { + case viewed + + var name: String { + switch self { + case .viewed: + "stats_viewed" + } + } + + var parameters: [String: Any]? { + switch self { + case .viewed: + nil + } + } +} diff --git a/Projects/Feature/Stats/Sources/Stats/StatsReducer+Impl.swift b/Projects/Feature/Stats/Sources/Stats/StatsReducer+Impl.swift index 8d2dad07..8f7e1c23 100644 --- a/Projects/Feature/Stats/Sources/Stats/StatsReducer+Impl.swift +++ b/Projects/Feature/Stats/Sources/Stats/StatsReducer+Impl.swift @@ -8,6 +8,7 @@ import Foundation import ComposableArchitecture +import CoreAnalyticsInterface import DomainCommonInterface import DomainStatsInterface import FeatureCommonInterface @@ -24,12 +25,14 @@ extension StatsReducer { // swiftlint:disable:next function_body_length public init() { @Dependency(\.statsClient) var statsClient + @Dependency(\.analyticsClient) var analyticsClient // swiftlint:disable:next closure_body_length let reducer = Reduce { state, action in switch action { // MARK: - LifeCycle case .onAppear: + analyticsClient.logEvent(StatsAnalyticsEvent.viewed) return .send(.fetchStats) // MARK: - UserAction diff --git a/Projects/Shared/DesignSystem/Sources/Components/Modal/Content/TXSelectListContent.swift b/Projects/Shared/DesignSystem/Sources/Components/Modal/Content/TXSelectListContent.swift index ecf9c037..34ffc025 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/Modal/Content/TXSelectListContent.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/Modal/Content/TXSelectListContent.swift @@ -2,7 +2,7 @@ // TXSelectListContent.swift // SharedDesignSystem // -// Created by Codex on 4/7/26. +// Created by Jihun on 4/7/26. // import SwiftUI diff --git a/Projects/Shared/Util/Sources/RelativeTimeFormatter.swift b/Projects/Shared/Util/Sources/RelativeTimeFormatter.swift index ccbac91c..f3df8be3 100644 --- a/Projects/Shared/Util/Sources/RelativeTimeFormatter.swift +++ b/Projects/Shared/Util/Sources/RelativeTimeFormatter.swift @@ -2,7 +2,7 @@ // RelativeTimeFormatter.swift // SharedUtil // -// Created by Codex on 02/09/26. +// Created by Jihun on 02/09/26. // import Foundation diff --git a/Tuist/ProjectDescriptionHelpers/Module.swift b/Tuist/ProjectDescriptionHelpers/Module.swift index 8199e77a..18ee17ef 100644 --- a/Tuist/ProjectDescriptionHelpers/Module.swift +++ b/Tuist/ProjectDescriptionHelpers/Module.swift @@ -91,6 +91,7 @@ public extension Module { /// 네트워크, 로깅, 저장소 등 기술적 기반을 담당하며, /// Feature/Domain에서 재사용되도록 설계됩니다. enum Core: String, CaseIterable { + case analytics = "Analytics" case push = "Push" case captureSession = "CaptureSession" case network = "Network" diff --git a/Tuist/ProjectDescriptionHelpers/Target/Dependency/TargetDependency+External.swift b/Tuist/ProjectDescriptionHelpers/Target/Dependency/TargetDependency+External.swift index 03e637a7..7b51de0a 100644 --- a/Tuist/ProjectDescriptionHelpers/Target/Dependency/TargetDependency+External.swift +++ b/Tuist/ProjectDescriptionHelpers/Target/Dependency/TargetDependency+External.swift @@ -16,6 +16,7 @@ public extension TargetDependency { /// 각 case는 SPM 패키지를 나타내며, /// `TargetDependency.external(dependency:)`와 유기적으로 사용됩니다. enum External: String { + case FirebaseAnalytics case FirebaseCore case FirebaseMessaging case FirebaseRemoteConfig From ebb0df29878ae0a0cec2d224c5c99ddbe743d212 Mon Sep 17 00:00:00 2001 From: Jihun <75370733+jihun32@users.noreply.github.com> Date: Wed, 13 May 2026 12:39:29 +0900 Subject: [PATCH 08/44] =?UTF-8?q?chore:=20PR-DDay=20yml=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20-=20#293=20(#294)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: jihun --- .github/workflows/pr-dday-labeler.yml | 84 +++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 .github/workflows/pr-dday-labeler.yml diff --git a/.github/workflows/pr-dday-labeler.yml b/.github/workflows/pr-dday-labeler.yml new file mode 100644 index 00000000..a9576eda --- /dev/null +++ b/.github/workflows/pr-dday-labeler.yml @@ -0,0 +1,84 @@ +name: PR D-Day Labeler + +on: + pull_request_target: + types: [opened, reopened, ready_for_review] + schedule: + # Runs every day at 00:15 KST. + - cron: "15 15 * * *" + workflow_dispatch: + +permissions: + issues: write + pull-requests: write + +jobs: + update-dday-label: + runs-on: ubuntu-latest + steps: + - name: Update PR D-Day labels + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const ddayLabels = ["D-3", "D-2", "D-1"]; + const dayMs = 24 * 60 * 60 * 1000; + + function targetLabelFor(pr) { + const createdAt = new Date(pr.created_at).getTime(); + const elapsedDays = Math.floor((Date.now() - createdAt) / dayMs); + const day = Math.max(1, 3 - elapsedDays); + return `D-${day}`; + } + + async function updatePullRequest(pr) { + if (pr.draft) { + console.log(`Skip draft PR #${pr.number}`); + return; + } + + const targetLabel = targetLabelFor(pr); + const currentLabels = pr.labels.map((label) => label.name); + const labelsToRemove = currentLabels.filter( + (label) => ddayLabels.includes(label) && label !== targetLabel + ); + const hasTargetLabel = currentLabels.includes(targetLabel); + + for (const label of labelsToRemove) { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + name: label, + }); + console.log(`Removed ${label} from PR #${pr.number}`); + } + + if (!hasTargetLabel) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + labels: [targetLabel], + }); + console.log(`Added ${targetLabel} to PR #${pr.number}`); + } else { + console.log(`PR #${pr.number} already has ${targetLabel}`); + } + } + + if (context.payload.pull_request) { + await updatePullRequest(context.payload.pull_request); + return; + } + + const pulls = await github.paginate(github.rest.pulls.list, { + owner: context.repo.owner, + repo: context.repo.repo, + state: "open", + per_page: 100, + }); + + for (const pr of pulls) { + await updatePullRequest(pr); + } From ff83559cc21715f5f2b6fb98b14336cbd5f3cd3a Mon Sep 17 00:00:00 2001 From: Jiyong Jung Date: Wed, 13 May 2026 08:25:29 +0900 Subject: [PATCH 09/44] =?UTF-8?q?fix:=20Calender=20=ED=83=80=EC=9E=84?= =?UTF-8?q?=EC=A1=B4=EC=9D=84=20=EB=AA=85=EC=8B=9C=EC=A0=81=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95=20-=20#290?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/Shared/Util/Sources/CalendarNow.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Projects/Shared/Util/Sources/CalendarNow.swift b/Projects/Shared/Util/Sources/CalendarNow.swift index 044c5194..5bf1d0b9 100644 --- a/Projects/Shared/Util/Sources/CalendarNow.swift +++ b/Projects/Shared/Util/Sources/CalendarNow.swift @@ -15,7 +15,11 @@ public struct CalendarNow: Equatable, Hashable { public init( date: Date = Date(), - calendar: Calendar = Calendar(identifier: .gregorian) + calendar: Calendar = { + var c = Calendar(identifier: .gregorian) + c.timeZone = .current + return c + }() ) { self.year = calendar.component(.year, from: date) self.month = calendar.component(.month, from: date) From 1c429cc11164e938d88a1713b9bc1dd5353df40f Mon Sep 17 00:00:00 2001 From: Jiyong Jung Date: Wed, 13 May 2026 08:25:55 +0900 Subject: [PATCH 10/44] =?UTF-8?q?fix:=20=ED=98=84=EC=9E=AC=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=EC=9D=B4=20=EA=B3=A0=EC=A0=95=EB=90=98=EB=8A=94=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95=20-=20#290?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/Feature/Home/Interface/Sources/Home/HomeReducer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Projects/Feature/Home/Interface/Sources/Home/HomeReducer.swift b/Projects/Feature/Home/Interface/Sources/Home/HomeReducer.swift index f93913ee..e514c954 100644 --- a/Projects/Feature/Home/Interface/Sources/Home/HomeReducer.swift +++ b/Projects/Feature/Home/Interface/Sources/Home/HomeReducer.swift @@ -51,7 +51,7 @@ public struct HomeReducer { public var pendingDeletePhotologID: Int64? public var hasCards: Bool { !items.isEmpty } public var isEmptyVisible: Bool { !isLoading && items.isEmpty } - public let nowDate = CalendarNow() + public var nowDate: CalendarNow { CalendarNow() } public var toast: TXToastType? public var modal: TXModalStyle? public var isProofPhotoPresented: Bool = false From 4fd83eb82ec654f9ef34b1ff64d3ee1cc590024c Mon Sep 17 00:00:00 2001 From: Jiyong Jung Date: Thu, 14 May 2026 09:16:57 +0900 Subject: [PATCH 11/44] =?UTF-8?q?feat:=20Crashlytics=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20=EB=B0=8F=20=EC=97=90=EB=9F=AC=20=EC=B6=94=EC=A0=81=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=20=EA=B0=9C=EC=84=A0=20(#287)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: FirebaseCrashlytics SPM 의존성 및 모듈 등록 - #286 * feat: Crashlytics dSYM 업로드 스크립트 추가 - #286 * feat: Core/Crashlytics 모듈 추가 - #286 * feat: Core 에러 타입에 CustomNSError 적용 - #286 * feat: Domain 에러 타입에 CustomNSError 적용 및 AuthClient 에러 wrapping - #286 * chore: App 타겟에 Crashlytics 의존성 및 dSYM 스크립트 추가 - #286 * feat: AppDelegate에 Crashlytics 수집 설정 추가 - #286 * feat: AppCoordinator에 Crashlytics 유저 식별자 및 오류 추적 추가 - #286 * feat: Auth Feature에 Crashlytics 로그인 실패 추적 추가 - #286 * feat: ProofPhoto Feature에 Crashlytics 오류 추적 추가 - #286 * feat: Onboarding Feature에 초대 코드 실패 토스트 및 Crashlytics 추적 추가 - #286 * chore: 버전 1.1.2로 업데이트 - #286 * fix: 컴파일 에러 수정 - #286 * fix: CalendarNow 변수명 SwiftLint identifier_name 위반 수정 - #286 * refactor: CrashlyticsClient를 화면별 이벤트 enum 패턴으로 전환 - #286 * refactor: AuthLoginError.caseName 제거 - #286 --- Projects/App/Project.swift | 7 +- Projects/App/Sources/AppDelegate.swift | 7 ++ .../Crashlytics/AppCrashlyticsEvent.swift | 33 +++++++++ .../App/Sources/Reducer/AppCoordinator.swift | 18 +++-- .../Sources/CaptureSessionError.swift | 15 ++++ .../Interface/Sources/CrashlyticsClient.swift | 68 +++++++++++++++++++ .../Interface/Sources/CrashlyticsEvent.swift | 50 ++++++++++++++ .../Interface/Sources/CrashlyticsKey.swift | 44 ++++++++++++ Projects/Core/Crashlytics/Project.swift | 26 +++++++ .../Sources/CrashlyticsClient+Live.swift | 24 +++++++ .../Interface/Sources/NetworkError.swift | 28 ++++++++ .../Core/Push/Sources/PushClient+Live.swift | 13 ++++ .../Sources/KeychainTokenStorage.swift | 34 +++++++++- .../Sources/Entity/AuthLoginError.swift | 34 ++++++++++ .../Domain/Auth/Sources/AuthClient+Live.swift | 28 ++++++-- .../Sources/Entity/OnboardingError.swift | 18 +++++ Projects/Feature/Auth/Project.swift | 1 + .../Crashlytics/AuthCrashlyticsEvent.swift | 33 +++++++++ .../Auth/Sources/Reducer/AuthReducer.swift | 9 +++ Projects/Feature/Onboarding/Project.swift | 1 + .../OnboardingCrashlyticsEvent.swift | 18 +++++ .../Sources/OnboardingCoordinator.swift | 18 ++++- .../Sources/OnboardingCoordinatorView.swift | 1 + Projects/Feature/ProofPhoto/Project.swift | 1 + .../ProofPhotoCrashlyticsEvent.swift | 51 ++++++++++++++ .../ProofPhoto/ProofPhotoReducer+Impl.swift | 40 ++++++++++- .../Shared/Util/Sources/CalendarNow.swift | 6 +- Tuist/Package.swift | 3 +- Tuist/ProjectDescriptionHelpers/Module.swift | 1 + .../Scripts/CrashlyticsScript.swift | 30 ++++++++ .../TargetDependency+External.swift | 1 + 31 files changed, 637 insertions(+), 24 deletions(-) create mode 100644 Projects/App/Sources/Crashlytics/AppCrashlyticsEvent.swift create mode 100644 Projects/Core/Crashlytics/Interface/Sources/CrashlyticsClient.swift create mode 100644 Projects/Core/Crashlytics/Interface/Sources/CrashlyticsEvent.swift create mode 100644 Projects/Core/Crashlytics/Interface/Sources/CrashlyticsKey.swift create mode 100644 Projects/Core/Crashlytics/Project.swift create mode 100644 Projects/Core/Crashlytics/Sources/CrashlyticsClient+Live.swift create mode 100644 Projects/Feature/Auth/Sources/Crashlytics/AuthCrashlyticsEvent.swift create mode 100644 Projects/Feature/Onboarding/Sources/Crashlytics/OnboardingCrashlyticsEvent.swift create mode 100644 Projects/Feature/ProofPhoto/Sources/Crashlytics/ProofPhotoCrashlyticsEvent.swift create mode 100644 Tuist/ProjectDescriptionHelpers/Scripts/CrashlyticsScript.swift diff --git a/Projects/App/Project.swift b/Projects/App/Project.swift index 7a2cafce..7aa720fd 100644 --- a/Projects/App/Project.swift +++ b/Projects/App/Project.swift @@ -31,7 +31,7 @@ private let commonInfoPlist: [String: Plist.Value] = Project.Environment.InfoPli "DEEPLINK_HOST": "$(DEEPLINK_HOST)", "API_BASE_URL": "$(API_BASE_URL)", "NSCameraUsageDescription": "UseCamera", - "CFBundleShortVersionString": "1.1.1" + "CFBundleShortVersionString": "1.1.2" ], uniquingKeysWith: { current, _ in current }) private let commonDependencies: [TargetDependency] = [ @@ -45,7 +45,8 @@ private let commonDependencies: [TargetDependency] = [ .external(dependency: .GoogleSignIn), .external(dependency: .FirebaseCore), .external(dependency: .FirebaseMessaging), - .external(dependency: .FirebaseRemoteConfig) + .external(dependency: .FirebaseRemoteConfig), + .core(implements: .crashlytics) ] private let commonBuildSettings: SettingsDictionary = [ @@ -75,7 +76,7 @@ let project = Project( config: .init( infoPlist: .extendingDefault(with: commonInfoPlist), entitlements: .file(path: "Support/Twix.entitlements"), - scripts: [.swiftLint], + scripts: [.swiftLint, .crashlyticsUploadSymbols], dependencies: commonDependencies + [ .core(implements: .analytics) ], diff --git a/Projects/App/Sources/AppDelegate.swift b/Projects/App/Sources/AppDelegate.swift index 1e10b9c5..f02186a6 100644 --- a/Projects/App/Sources/AppDelegate.swift +++ b/Projects/App/Sources/AppDelegate.swift @@ -7,6 +7,7 @@ import CoreLogging import FirebaseCore +import FirebaseCrashlytics import FirebaseMessaging import UIKit import UserNotifications @@ -21,6 +22,12 @@ final class AppDelegate: NSObject, UIApplicationDelegate { // Firebase 초기화 FirebaseApp.configure() + #if DEBUG + Crashlytics.crashlytics().setCrashlyticsCollectionEnabled(false) + #else + Crashlytics.crashlytics().setCrashlyticsCollectionEnabled(true) + #endif + // 푸시 알림 delegate 설정 UNUserNotificationCenter.current().delegate = self Messaging.messaging().delegate = self diff --git a/Projects/App/Sources/Crashlytics/AppCrashlyticsEvent.swift b/Projects/App/Sources/Crashlytics/AppCrashlyticsEvent.swift new file mode 100644 index 00000000..149a53d8 --- /dev/null +++ b/Projects/App/Sources/Crashlytics/AppCrashlyticsEvent.swift @@ -0,0 +1,33 @@ +// +// AppCrashlyticsEvent.swift +// Twix +// + +import CoreCrashlyticsInterface +import Foundation + +enum AppCrashlyticsLogEvent: CrashlyticsLogEvent { + case sessionExpiredAtOnboardingStatusCheck + + var message: String { + switch self { + case .sessionExpiredAtOnboardingStatusCheck: + "session expired at onboarding status check" + } + } +} + +enum AppCrashlyticsRecordEvent: CrashlyticsRecordEvent { + case appStartupFailed + case onboardingStatusCheckFailed + + var customKeys: [String: String] { + switch self { + case .appStartupFailed: + [CrashlyticsKey.screen: "app_startup"] + + case .onboardingStatusCheckFailed: + [CrashlyticsKey.screen: "startup_onboarding_check"] + } + } +} diff --git a/Projects/App/Sources/Reducer/AppCoordinator.swift b/Projects/App/Sources/Reducer/AppCoordinator.swift index 6873142a..971b655b 100644 --- a/Projects/App/Sources/Reducer/AppCoordinator.swift +++ b/Projects/App/Sources/Reducer/AppCoordinator.swift @@ -8,6 +8,7 @@ import ComposableArchitecture import CoreAnalytics import CoreAnalyticsInterface +import CoreCrashlyticsInterface import CoreLogging import CoreNetworkInterface import CorePushInterface @@ -35,7 +36,11 @@ struct AppCoordinator { @Dependency(\.notificationClient) var notificationClient - @Dependency(\.analyticsClient) var analyticsClient + @Dependency(\.analyticsClient) + var analyticsClient + + @Dependency(\.crashlyticsClient) + var crashlytics private let authReducer: AuthReducer private let onboardingCoordinator: OnboardingCoordinator @@ -149,9 +154,10 @@ struct AppCoordinator { } return .none - case .checkAuthResult(.failure): + case .checkAuthResult(.failure(let error)): state.isCheckingAuth = false state.route = .auth(AuthReducer.State()) + crashlytics.record(error, AppCrashlyticsRecordEvent.appStartupFailed) return .none case let .checkOnboardingStatusResult(.success(status)): @@ -198,10 +204,11 @@ struct AppCoordinator { state.isCheckingAuth = false if let networkError = error as? NetworkError, case .authorizationError = networkError { + crashlytics.log(AppCrashlyticsLogEvent.sessionExpiredAtOnboardingStatusCheck) state.route = .auth(AuthReducer.State()) return .none } - + crashlytics.record(error, AppCrashlyticsRecordEvent.onboardingStatusCheckFailed) state.route = .onboarding(OnboardingCoordinator.State( pendingReceivedCode: state.pendingInviteCode )) @@ -225,8 +232,9 @@ struct AppCoordinator { state.pendingNotificationDeepLink = nil return .send(.route(.mainTab(.notificationDeepLinkReceived(deepLink)))) - - case .route(.auth(.delegate(.loginSucceeded))): + + case let .route(.auth(.delegate(.loginSucceeded(authResult)))): + crashlytics.setUserIdentifier("\(authResult.userId)") return .merge( // 1. 온보딩 상태 체크 .run { [onboardingClient] send in diff --git a/Projects/Core/CaptureSession/Interface/Sources/CaptureSessionError.swift b/Projects/Core/CaptureSession/Interface/Sources/CaptureSessionError.swift index 2bc2df58..5259eb34 100644 --- a/Projects/Core/CaptureSession/Interface/Sources/CaptureSessionError.swift +++ b/Projects/Core/CaptureSession/Interface/Sources/CaptureSessionError.swift @@ -13,3 +13,18 @@ public enum CaptureSessionError: Error { case photoDataUnavailable case deviceInputNotCreated } + +// MARK: - CustomNSError + +extension CaptureSessionError: CustomNSError { + public static var errorDomain: String { "org.yapp.twix.capture" } + + public var errorCode: Int { + switch self { + case .sessionDeallocated: return 1 + case .sessionNotConfigured: return 2 + case .photoDataUnavailable: return 3 + case .deviceInputNotCreated: return 4 + } + } +} diff --git a/Projects/Core/Crashlytics/Interface/Sources/CrashlyticsClient.swift b/Projects/Core/Crashlytics/Interface/Sources/CrashlyticsClient.swift new file mode 100644 index 00000000..2d75993f --- /dev/null +++ b/Projects/Core/Crashlytics/Interface/Sources/CrashlyticsClient.swift @@ -0,0 +1,68 @@ +// +// CrashlyticsClient.swift +// CoreCrashlyticsInterface +// + +import ComposableArchitecture +import Foundation + +/// Crashlytics non-fatal 오류 추적 클라이언트입니다. +/// +/// 화면/Feature별로 정의된 이벤트 타입을 통해 메시지·커스텀 키를 일관되게 전달합니다. +/// +/// ## 사용 예시 +/// ```swift +/// @Dependency(\.crashlyticsClient) var crashlytics +/// +/// // non-fatal 오류 기록 +/// crashlytics.record( +/// error, +/// ProofPhotoCrashlyticsRecordEvent.uploadFailed(step: "fetchURL", goalId: 1) +/// ) +/// +/// // 브레드크럼 로그 +/// crashlytics.log(ProofPhotoCrashlyticsLogEvent.uploadStep(.fetchURL, goalId: 1)) +/// +/// // 유저 식별자 설정 (로그인 성공 시 1회) +/// crashlytics.setUserIdentifier(userId) +/// ``` +public struct CrashlyticsClient: Sendable { + public var record: @Sendable (Error, any CrashlyticsRecordEvent) -> Void + public var log: @Sendable (any CrashlyticsLogEvent) -> Void + public var setUserIdentifier: @Sendable (String) -> Void + + public init( + record: @escaping @Sendable (Error, any CrashlyticsRecordEvent) -> Void, + log: @escaping @Sendable (any CrashlyticsLogEvent) -> Void, + setUserIdentifier: @escaping @Sendable (String) -> Void + ) { + self.record = record + self.log = log + self.setUserIdentifier = setUserIdentifier + } +} + +// MARK: - TestDependencyKey + +extension CrashlyticsClient: TestDependencyKey { + public static let testValue = Self( + record: { _, _ in }, + log: { _ in }, + setUserIdentifier: { _ in } + ) + + public static let previewValue = Self( + record: { _, _ in }, + log: { _ in }, + setUserIdentifier: { _ in } + ) +} + +// MARK: - DependencyValues + +public extension DependencyValues { + var crashlyticsClient: CrashlyticsClient { + get { self[CrashlyticsClient.self] } + set { self[CrashlyticsClient.self] = newValue } + } +} diff --git a/Projects/Core/Crashlytics/Interface/Sources/CrashlyticsEvent.swift b/Projects/Core/Crashlytics/Interface/Sources/CrashlyticsEvent.swift new file mode 100644 index 00000000..eba2d994 --- /dev/null +++ b/Projects/Core/Crashlytics/Interface/Sources/CrashlyticsEvent.swift @@ -0,0 +1,50 @@ +// +// CrashlyticsEvent.swift +// CoreCrashlyticsInterface +// + +import Foundation + +/// Crashlytics 브레드크럼 로그 이벤트가 따라야 하는 공통 인터페이스입니다. +/// +/// ## 사용 예시 +/// ```swift +/// enum ProofPhotoCrashlyticsLogEvent: CrashlyticsLogEvent { +/// case uploadStep(UploadStep, goalId: Int64) +/// +/// var message: String { +/// switch self { +/// case let .uploadStep(step, goalId): +/// "upload_step: \(step.rawValue), goalId=\(goalId)" +/// } +/// } +/// } +/// ``` +public protocol CrashlyticsLogEvent: Sendable { + var message: String { get } +} + +/// Crashlytics non-fatal 오류 기록 이벤트가 따라야 하는 공통 인터페이스입니다. +/// +/// `customKeys`에 화면 식별자(`CrashlyticsKey.screen`) 등 컨텍스트 정보를 담아 전달합니다. +/// +/// ## 사용 예시 +/// ```swift +/// enum ProofPhotoCrashlyticsRecordEvent: CrashlyticsRecordEvent { +/// case uploadFailed(step: String, goalId: Int64) +/// +/// var customKeys: [String: String] { +/// switch self { +/// case let .uploadFailed(step, goalId): +/// [ +/// CrashlyticsKey.screen: "proof_photo_upload", +/// CrashlyticsKey.uploadStep: step, +/// CrashlyticsKey.goalId: "\(goalId)" +/// ] +/// } +/// } +/// } +/// ``` +public protocol CrashlyticsRecordEvent: Sendable { + var customKeys: [String: String] { get } +} diff --git a/Projects/Core/Crashlytics/Interface/Sources/CrashlyticsKey.swift b/Projects/Core/Crashlytics/Interface/Sources/CrashlyticsKey.swift new file mode 100644 index 00000000..a2d58d87 --- /dev/null +++ b/Projects/Core/Crashlytics/Interface/Sources/CrashlyticsKey.swift @@ -0,0 +1,44 @@ +// +// CrashlyticsKey.swift +// CoreCrashlyticsInterface +// + +/// Crashlytics custom key 상수 모음입니다. +/// +/// `crashlyticsClient.record(error, [CrashlyticsKey.screen: "home"])` 형태로 사용합니다. +public enum CrashlyticsKey { + + // MARK: - 사용자 컨텍스트 + public static let userId = "user_id" + public static let goalId = "goal_id" + + // MARK: - 화면 컨텍스트 + public static let screen = "screen" + + // MARK: - 네트워크 + public static let networkEndpoint = "network_endpoint" + public static let networkMethod = "network_method" + public static let httpStatusCode = "http_status_code" + public static let networkErrorType = "network_error_type" + public static let retryCount = "retry_count" + + // MARK: - 이미지 업로드 + // fetchURL | uploadS3 | createLog + public static let uploadStep = "upload_step" + public static let originalImageBytes = "original_image_bytes" + public static let optimizedImageBytes = "optimized_image_bytes" + // 이미지 최적화 실패로 원본을 그대로 사용한 경우 "true" + public static let optimizationFallback = "optimization_fallback" + + // MARK: - 카메라 + public static let captureErrorType = "capture_error_type" + + // MARK: - 인증 + public static let authProvider = "auth_provider" + public static let authErrorType = "auth_error_type" + + // MARK: - 키체인 + // save | load | delete + public static let keychainOperation = "keychain_operation" + public static let keychainOsStatus = "keychain_os_status" +} diff --git a/Projects/Core/Crashlytics/Project.swift b/Projects/Core/Crashlytics/Project.swift new file mode 100644 index 00000000..b4c96787 --- /dev/null +++ b/Projects/Core/Crashlytics/Project.swift @@ -0,0 +1,26 @@ +import ProjectDescription +import ProjectDescriptionHelpers + +let project = Project.makeModule( + name: Module.Core.name + Module.Core.crashlytics.rawValue, + targets: [ + .core( + interface: .crashlytics, + config: .init( + dependencies: [ + .external(dependency: .ComposableArchitecture) + ] + ) + ), + .core( + implements: .crashlytics, + config: .init( + dependencies: [ + .core(interface: .crashlytics), + .external(dependency: .ComposableArchitecture), + .external(dependency: .FirebaseCrashlytics) + ] + ) + ) + ] +) diff --git a/Projects/Core/Crashlytics/Sources/CrashlyticsClient+Live.swift b/Projects/Core/Crashlytics/Sources/CrashlyticsClient+Live.swift new file mode 100644 index 00000000..92d8eca5 --- /dev/null +++ b/Projects/Core/Crashlytics/Sources/CrashlyticsClient+Live.swift @@ -0,0 +1,24 @@ +// +// CrashlyticsClient+Live.swift +// CoreCrashlytics +// + +import ComposableArchitecture +import CoreCrashlyticsInterface +import FirebaseCrashlytics + +extension CrashlyticsClient: DependencyKey { + public static let liveValue = Self( + record: { error, event in + let instance = Crashlytics.crashlytics() + event.customKeys.forEach { instance.setCustomValue($0.value, forKey: $0.key) } + instance.record(error: error) + }, + log: { event in + Crashlytics.crashlytics().log(event.message) + }, + setUserIdentifier: { userId in + Crashlytics.crashlytics().setUserID(userId) + } + ) +} diff --git a/Projects/Core/Network/Interface/Sources/NetworkError.swift b/Projects/Core/Network/Interface/Sources/NetworkError.swift index 851260d9..ec815870 100644 --- a/Projects/Core/Network/Interface/Sources/NetworkError.swift +++ b/Projects/Core/Network/Interface/Sources/NetworkError.swift @@ -52,3 +52,31 @@ extension NetworkError { } } } + +// MARK: - CustomNSError + +extension NetworkError: CustomNSError { + public static var errorDomain: String { "org.yapp.twix.network" } + + public var errorCode: Int { + switch self { + case .invalidURLError: return 1 + case .invalidResponseError: return 2 + case .authorizationError: return 3 + case .badRequestError: return 4 + case .notFoundError: return 5 + case .serverError: return 6 + case .decodingError: return 7 + case .encodingError: return 8 + case .unknownError: return 9 + } + } + + public var errorUserInfo: [String: Any] { + var info: [String: Any] = [NSLocalizedDescriptionKey: errorMessage] + if case .badRequestError(let code) = self, let code { + info["serverCode"] = code + } + return info + } +} diff --git a/Projects/Core/Push/Sources/PushClient+Live.swift b/Projects/Core/Push/Sources/PushClient+Live.swift index 688d284d..a2445395 100644 --- a/Projects/Core/Push/Sources/PushClient+Live.swift +++ b/Projects/Core/Push/Sources/PushClient+Live.swift @@ -100,6 +100,19 @@ public enum PushError: Error, Sendable { case authorizationDenied } +// MARK: - CustomNSError + +extension PushError: CustomNSError { + public static var errorDomain: String { "org.yapp.twix.push" } + + public var errorCode: Int { + switch self { + case .tokenNotAvailable: return 1 + case .authorizationDenied: return 2 + } + } +} + // MARK: - Notification.Name extension Notification.Name { diff --git a/Projects/Core/Storage/Sources/KeychainTokenStorage.swift b/Projects/Core/Storage/Sources/KeychainTokenStorage.swift index ac1ae856..81acb5f6 100644 --- a/Projects/Core/Storage/Sources/KeychainTokenStorage.swift +++ b/Projects/Core/Storage/Sources/KeychainTokenStorage.swift @@ -156,19 +156,47 @@ public enum KeychainError: Error, LocalizedError { switch self { case .saveFailed(let status): return "Keychain 저장 실패: \(status)" - + case .loadFailed(let status): return "Keychain 불러오기 실패: \(status)" - + case .deleteFailed(let status): return "Keychain 삭제 실패: \(status)" - + case .invalidData: return "Keychain 데이터 형식 오류" } } } +// MARK: - CustomNSError + +extension KeychainError: CustomNSError { + public static var errorDomain: String { "org.yapp.twix.keychain" } + + public var errorCode: Int { + switch self { + case .saveFailed: return 1 + case .loadFailed: return 2 + case .deleteFailed: return 3 + case .invalidData: return 4 + } + } + + public var errorUserInfo: [String: Any] { + var info: [String: Any] = [NSLocalizedDescriptionKey: errorDescription ?? ""] + switch self { + case .saveFailed(let status), + .loadFailed(let status), + .deleteFailed(let status): + info["osStatus"] = Int(status) + case .invalidData: + break + } + return info + } +} + extension TokenStorageClient: DependencyKey { public static let liveValue = Self(storage: KeychainTokenStorage()) } diff --git a/Projects/Domain/Auth/Interface/Sources/Entity/AuthLoginError.swift b/Projects/Domain/Auth/Interface/Sources/Entity/AuthLoginError.swift index b13e334a..bfa5c4ac 100644 --- a/Projects/Domain/Auth/Interface/Sources/Entity/AuthLoginError.swift +++ b/Projects/Domain/Auth/Interface/Sources/Entity/AuthLoginError.swift @@ -45,3 +45,37 @@ extension AuthLoginError: LocalizedError { } } } + +// MARK: - CustomNSError + +extension AuthLoginError: CustomNSError { + public static var errorDomain: String { "org.yapp.twix.auth" } + + public var errorCode: Int { + switch self { + case .unsupportedProvider: return 1 + case .missingCredential: return 2 + case .userCanceled: return 3 + case .providerError: return 4 + case .serverError: return 5 + case .networkError: return 6 + case .storageFailed: return 7 + case .tokenRefreshFailed: return 8 + } + } + + public var errorUserInfo: [String: Any] { + var info: [String: Any] = [ + NSLocalizedDescriptionKey: errorDescription ?? localizedDescription + ] + switch self { + case .providerError(let underlying), + .networkError(let underlying), + .storageFailed(let underlying): + info[NSUnderlyingErrorKey] = underlying as NSError + default: + break + } + return info + } +} diff --git a/Projects/Domain/Auth/Sources/AuthClient+Live.swift b/Projects/Domain/Auth/Sources/AuthClient+Live.swift index 78d5b370..b43884fe 100644 --- a/Projects/Domain/Auth/Sources/AuthClient+Live.swift +++ b/Projects/Domain/Auth/Sources/AuthClient+Live.swift @@ -82,7 +82,13 @@ private extension AuthClient { logger.debug("loginResult: \(loginResult)") let endpoint = createAuthEndpoint(from: loginResult) - let response: SignInResponse = try await networkClient.request(endpoint: endpoint) + + let response: SignInResponse + do { + response = try await networkClient.request(endpoint: endpoint) + } catch { + throw AuthLoginError.networkError(error) + } let token = Token( accessToken: response.accessToken, @@ -90,7 +96,11 @@ private extension AuthClient { expiresAt: Date().addingTimeInterval(oneHourInSeconds) ) - try await tokenManager.saveTokenToStorage(token) + do { + try await tokenManager.saveTokenToStorage(token) + } catch { + throw AuthLoginError.storageFailed(error) + } return AuthResult( token: token, @@ -110,7 +120,13 @@ private extension AuthClient { } let endpoint = AuthEndpoint.refresh(refreshToken: currentRefreshToken) - let response: RefreshResponse = try await networkClient.request(endpoint: endpoint) + + let response: RefreshResponse + do { + response = try await networkClient.request(endpoint: endpoint) + } catch { + throw AuthLoginError.networkError(error) + } let token = Token( accessToken: response.accessToken, @@ -118,7 +134,11 @@ private extension AuthClient { expiresAt: Date().addingTimeInterval(oneHourInSeconds) ) - try await tokenManager.saveTokenToStorage(token) + do { + try await tokenManager.saveTokenToStorage(token) + } catch { + throw AuthLoginError.storageFailed(error) + } return token } diff --git a/Projects/Domain/Onboarding/Interface/Sources/Entity/OnboardingError.swift b/Projects/Domain/Onboarding/Interface/Sources/Entity/OnboardingError.swift index 54d2fb94..b25e15b4 100644 --- a/Projects/Domain/Onboarding/Interface/Sources/Entity/OnboardingError.swift +++ b/Projects/Domain/Onboarding/Interface/Sources/Entity/OnboardingError.swift @@ -44,3 +44,21 @@ extension OnboardingError: LocalizedError { } } } + +// MARK: - CustomNSError + +extension OnboardingError: CustomNSError { + public static var errorDomain: String { "org.yapp.twix.onboarding" } + + public var errorCode: Int { + switch self { + case .invalidInviteCode: return 1 + case .inviteCodeNotFound: return 2 + case .alreadyConnected: return 3 + case .alreadyOnboarded: return 4 + case .networkError: return 5 + case .serverError: return 6 + case .unknown: return 7 + } + } +} diff --git a/Projects/Feature/Auth/Project.swift b/Projects/Feature/Auth/Project.swift index 259325a3..a216e45f 100644 --- a/Projects/Feature/Auth/Project.swift +++ b/Projects/Feature/Auth/Project.swift @@ -22,6 +22,7 @@ let project = Project.makeModule( .domain(implements: .auth), .core(implements: .logging), .core(interface: .analytics), + .core(interface: .crashlytics), .shared(implements: .designSystem), .external(dependency: .ComposableArchitecture) ] diff --git a/Projects/Feature/Auth/Sources/Crashlytics/AuthCrashlyticsEvent.swift b/Projects/Feature/Auth/Sources/Crashlytics/AuthCrashlyticsEvent.swift new file mode 100644 index 00000000..d32b5bae --- /dev/null +++ b/Projects/Feature/Auth/Sources/Crashlytics/AuthCrashlyticsEvent.swift @@ -0,0 +1,33 @@ +// +// AuthCrashlyticsEvent.swift +// FeatureAuth +// + +import CoreCrashlyticsInterface +import DomainAuthInterface +import Foundation + +enum AuthCrashlyticsRecordEvent: CrashlyticsRecordEvent { + case loginFailed(AuthLoginError?) + + var customKeys: [String: String] { + switch self { + case let .loginFailed(error): + [CrashlyticsKey.authErrorType: errorType(for: error)] + } + } + + private func errorType(for error: AuthLoginError?) -> String { + switch error { + case .unsupportedProvider: "unsupportedProvider" + case .missingCredential: "missingCredential" + case .userCanceled: "userCanceled" + case .providerError: "providerError" + case .serverError: "serverError" + case .networkError: "networkError" + case .storageFailed: "storageFailed" + case .tokenRefreshFailed: "tokenRefreshFailed" + case .none: "unknown" + } + } +} diff --git a/Projects/Feature/Auth/Sources/Reducer/AuthReducer.swift b/Projects/Feature/Auth/Sources/Reducer/AuthReducer.swift index a2bc489e..6c8a2c71 100644 --- a/Projects/Feature/Auth/Sources/Reducer/AuthReducer.swift +++ b/Projects/Feature/Auth/Sources/Reducer/AuthReducer.swift @@ -7,6 +7,7 @@ import ComposableArchitecture import CoreAnalyticsInterface +import CoreCrashlyticsInterface import CoreLogging import DomainAuthInterface import Foundation @@ -92,6 +93,9 @@ private extension AuthReducer { @Dependency(\.authClient) static var authClient + @Dependency(\.crashlyticsClient) + static var crashlytics + @Dependency(\.authLogger) static var logger @@ -139,6 +143,11 @@ private extension AuthReducer { state.errorMessage = errorMessage(for: error) #if DEBUG logger.error("로그인 실패 - \(error.localizedDescription)") + #else + crashlytics.record( + error, + AuthCrashlyticsRecordEvent.loginFailed(error as? AuthLoginError) + ) #endif return .none } diff --git a/Projects/Feature/Onboarding/Project.swift b/Projects/Feature/Onboarding/Project.swift index 2bab7477..6cf71a21 100644 --- a/Projects/Feature/Onboarding/Project.swift +++ b/Projects/Feature/Onboarding/Project.swift @@ -17,6 +17,7 @@ let project = Project.makeModule( .core(interface: .analytics), .domain(interface: .onboarding), .core(interface: .push), + .core(interface: .crashlytics), .shared(implements: .designSystem), .external(dependency: .ComposableArchitecture) ] diff --git a/Projects/Feature/Onboarding/Sources/Crashlytics/OnboardingCrashlyticsEvent.swift b/Projects/Feature/Onboarding/Sources/Crashlytics/OnboardingCrashlyticsEvent.swift new file mode 100644 index 00000000..2bd13078 --- /dev/null +++ b/Projects/Feature/Onboarding/Sources/Crashlytics/OnboardingCrashlyticsEvent.swift @@ -0,0 +1,18 @@ +// +// OnboardingCrashlyticsEvent.swift +// FeatureOnboarding +// + +import CoreCrashlyticsInterface +import Foundation + +enum OnboardingCrashlyticsRecordEvent: CrashlyticsRecordEvent { + case inviteCodeFetchFailed + + var customKeys: [String: String] { + switch self { + case .inviteCodeFetchFailed: + [CrashlyticsKey.screen: "onboarding_connect"] + } + } +} diff --git a/Projects/Feature/Onboarding/Sources/OnboardingCoordinator.swift b/Projects/Feature/Onboarding/Sources/OnboardingCoordinator.swift index 571e9c93..9211d770 100644 --- a/Projects/Feature/Onboarding/Sources/OnboardingCoordinator.swift +++ b/Projects/Feature/Onboarding/Sources/OnboardingCoordinator.swift @@ -7,6 +7,7 @@ import ComposableArchitecture import CoreAnalyticsInterface +import CoreCrashlyticsInterface import CorePushInterface import DomainOnboardingInterface import Foundation @@ -31,7 +32,10 @@ public struct OnboardingCoordinator { private var pushClient @Dependency(\.continuousClock) private var clock - @Dependency(\.analyticsClient) private var analyticsClient + @Dependency(\.analyticsClient) + private var analyticsClient + @Dependency(\.crashlyticsClient) + private var crashlytics private enum CancelID { case couplePolling @@ -48,6 +52,7 @@ public struct OnboardingCoordinator { var myInviteCode: String var pendingReceivedCode: String? var isLoadingInviteCode: Bool = false + var toast: TXToastType? var initialStatus: OnboardingStatus var isCouplePolling: Bool = false @@ -100,6 +105,9 @@ public struct OnboardingCoordinator { case fetchInviteCodeResponse(Result) case fetchStatusResponse(Result) + // MARK: - Toast + case showToast(TXToastType) + // MARK: - Navigation case navigateToCodeInputWithCode(myInviteCode: String, receivedCode: String) @@ -264,9 +272,13 @@ public struct OnboardingCoordinator { } return .none - case .fetchInviteCodeResponse(.failure): + case let .fetchInviteCodeResponse(.failure(error)): state.isLoadingInviteCode = false - // 에러 발생 시 임시 코드 사용 (또는 에러 처리) + crashlytics.record(error, OnboardingCrashlyticsRecordEvent.inviteCodeFetchFailed) + return .send(.showToast(.warning(message: "초대 코드를 불러오지 못했어요. 잠시 후 다시 시도해주세요."))) + + case let .showToast(toast): + state.toast = toast return .none // MARK: - Navigation diff --git a/Projects/Feature/Onboarding/Sources/OnboardingCoordinatorView.swift b/Projects/Feature/Onboarding/Sources/OnboardingCoordinatorView.swift index 7d2b2751..be15e870 100644 --- a/Projects/Feature/Onboarding/Sources/OnboardingCoordinatorView.swift +++ b/Projects/Feature/Onboarding/Sources/OnboardingCoordinatorView.swift @@ -56,6 +56,7 @@ public struct OnboardingCoordinatorView: View { .onAppear { store.send(.onAppear) } + .txToast(item: $store.toast) .fullScreenCover(isPresented: $store.isNotificationModalPresented) { notificationPermissionModal } diff --git a/Projects/Feature/ProofPhoto/Project.swift b/Projects/Feature/ProofPhoto/Project.swift index f12dd547..08d967ee 100644 --- a/Projects/Feature/ProofPhoto/Project.swift +++ b/Projects/Feature/ProofPhoto/Project.swift @@ -22,6 +22,7 @@ let project = Project.makeModule( .feature(interface: .proofPhoto), .core(interface: .captureSession), .core(interface: .analytics), + .core(interface: .crashlytics), .domain(interface: .goal), .domain(interface: .photoLog), .shared(implements: .designSystem), diff --git a/Projects/Feature/ProofPhoto/Sources/Crashlytics/ProofPhotoCrashlyticsEvent.swift b/Projects/Feature/ProofPhoto/Sources/Crashlytics/ProofPhotoCrashlyticsEvent.swift new file mode 100644 index 00000000..1962598f --- /dev/null +++ b/Projects/Feature/ProofPhoto/Sources/Crashlytics/ProofPhotoCrashlyticsEvent.swift @@ -0,0 +1,51 @@ +// +// ProofPhotoCrashlyticsEvent.swift +// FeatureProofPhoto +// + +import CoreCrashlyticsInterface +import Foundation + +enum ProofPhotoUploadStep: String, Sendable { + case fetchURL + case uploadS3 + case createLog +} + +enum ProofPhotoCrashlyticsLogEvent: CrashlyticsLogEvent { + case uploadStep(ProofPhotoUploadStep, goalId: Int64, imageBytes: Int?) + + var message: String { + switch self { + case let .uploadStep(step, goalId, imageBytes): + if let imageBytes { + "upload_step: \(step.rawValue), goalId=\(goalId), size=\(imageBytes)" + } else { + "upload_step: \(step.rawValue), goalId=\(goalId)" + } + } + } +} + +enum ProofPhotoCrashlyticsRecordEvent: CrashlyticsRecordEvent { + case captureFailed(errorType: String) + case uploadFailed(step: ProofPhotoUploadStep, goalId: Int64, originalImageBytes: Int) + + var customKeys: [String: String] { + switch self { + case let .captureFailed(errorType): + [ + CrashlyticsKey.screen: "proof_photo_camera", + CrashlyticsKey.captureErrorType: errorType + ] + + case let .uploadFailed(step, goalId, originalImageBytes): + [ + CrashlyticsKey.screen: "proof_photo_upload", + CrashlyticsKey.uploadStep: step.rawValue, + CrashlyticsKey.goalId: "\(goalId)", + CrashlyticsKey.originalImageBytes: "\(originalImageBytes)" + ] + } + } +} diff --git a/Projects/Feature/ProofPhoto/Sources/ProofPhoto/ProofPhotoReducer+Impl.swift b/Projects/Feature/ProofPhoto/Sources/ProofPhoto/ProofPhotoReducer+Impl.swift index 7a36bdd1..3df807c4 100644 --- a/Projects/Feature/ProofPhoto/Sources/ProofPhoto/ProofPhotoReducer+Impl.swift +++ b/Projects/Feature/ProofPhoto/Sources/ProofPhoto/ProofPhotoReducer+Impl.swift @@ -9,6 +9,7 @@ import AVFoundation import ComposableArchitecture import CoreAnalyticsInterface import CoreCaptureSessionInterface +import CoreCrashlyticsInterface import DomainGoalInterface import DomainPhotoLogInterface import FeatureProofPhotoInterface @@ -29,6 +30,7 @@ extension ProofPhotoReducer { @Dependency(\.captureSessionClient) var captureSessionClient @Dependency(\.photoLogClient) var photoLogClient @Dependency(\.analyticsClient) var analyticsClient + @Dependency(\.crashlyticsClient) var crashlytics // swiftlint: disable closure_body_length let reducer = Reduce { state, action in @@ -54,10 +56,16 @@ extension ProofPhotoReducer { return .run { send in do { let imageData = try await captureSessionClient.capturePhoto() - + await send(.captureCompleted(imageData: imageData)) captureSessionClient.stopRunning() } catch { + crashlytics.record( + error, + ProofPhotoCrashlyticsRecordEvent.captureFailed( + errorType: String(describing: error) + ) + ) await send(.captureFailed) } } @@ -135,12 +143,32 @@ extension ProofPhotoReducer { state.isUploading = true return .run { send in + let originalSize = imageData.count + var uploadStep: ProofPhotoUploadStep = .fetchURL do { let uploadStartedAt = Date() let optimizedImageData = ImageUploadOptimizer.optimizedJPEGData(from: imageData) + crashlytics.log(ProofPhotoCrashlyticsLogEvent.uploadStep( + .fetchURL, + goalId: goalId, + imageBytes: nil + )) let uploadResponse = try await photoLogClient.fetchUploadURL(goalId) + + uploadStep = .uploadS3 + crashlytics.log(ProofPhotoCrashlyticsLogEvent.uploadStep( + .uploadS3, + goalId: goalId, + imageBytes: optimizedImageData.count + )) try await photoLogClient.uploadImageData(optimizedImageData, uploadResponse.uploadUrl) - + + uploadStep = .createLog + crashlytics.log(ProofPhotoCrashlyticsLogEvent.uploadStep( + .createLog, + goalId: goalId, + imageBytes: nil + )) let createRequest = PhotoLogCreateRequestDTO( goalId: goalId, fileName: uploadResponse.fileName, @@ -178,6 +206,14 @@ extension ProofPhotoReducer { ) ) } catch { + crashlytics.record( + error, + ProofPhotoCrashlyticsRecordEvent.uploadFailed( + step: uploadStep, + goalId: goalId, + originalImageBytes: originalSize + ) + ) await send(.uploadFailed) } } diff --git a/Projects/Shared/Util/Sources/CalendarNow.swift b/Projects/Shared/Util/Sources/CalendarNow.swift index 5bf1d0b9..871af7ac 100644 --- a/Projects/Shared/Util/Sources/CalendarNow.swift +++ b/Projects/Shared/Util/Sources/CalendarNow.swift @@ -16,9 +16,9 @@ public struct CalendarNow: Equatable, Hashable { public init( date: Date = Date(), calendar: Calendar = { - var c = Calendar(identifier: .gregorian) - c.timeZone = .current - return c + var gregorian = Calendar(identifier: .gregorian) + gregorian.timeZone = .current + return gregorian }() ) { self.year = calendar.component(.year, from: date) diff --git a/Tuist/Package.swift b/Tuist/Package.swift index 2748ee8a..27a577ce 100644 --- a/Tuist/Package.swift +++ b/Tuist/Package.swift @@ -15,7 +15,8 @@ import PackageDescription "FirebaseCore": .staticLibrary, "FirebaseAnalytics": .staticLibrary, "FirebaseRemoteConfig" : .staticLibrary, - "FirebaseMessaging": .staticLibrary + "FirebaseMessaging": .staticLibrary, + "FirebaseCrashlytics": .staticLibrary ] ) #endif diff --git a/Tuist/ProjectDescriptionHelpers/Module.swift b/Tuist/ProjectDescriptionHelpers/Module.swift index 18ee17ef..7a264bba 100644 --- a/Tuist/ProjectDescriptionHelpers/Module.swift +++ b/Tuist/ProjectDescriptionHelpers/Module.swift @@ -97,6 +97,7 @@ public extension Module { case network = "Network" case logging = "Logging" case storage = "Storage" + case crashlytics = "Crashlytics" /// Core 타겟 이름의 기본 prefix입니다. public static let name: String = "Core" diff --git a/Tuist/ProjectDescriptionHelpers/Scripts/CrashlyticsScript.swift b/Tuist/ProjectDescriptionHelpers/Scripts/CrashlyticsScript.swift new file mode 100644 index 00000000..139204dc --- /dev/null +++ b/Tuist/ProjectDescriptionHelpers/Scripts/CrashlyticsScript.swift @@ -0,0 +1,30 @@ +import ProjectDescription + +public extension TargetScript { + /// Firebase Crashlytics dSYM 업로드 스크립트입니다. + /// + /// Archive 빌드 시 생성된 dSYM을 Firebase Console에 업로드해 크래시 스택을 심볼화합니다. + /// SPM 기준 경로를 사용하며, `basedOnDependencyAnalysis: false`로 항상 실행됩니다. + /// Tuist는 SPM 패키지를 Tuist/.build/checkouts/에 관리합니다. + /// `run` 대신 `upload-symbols`를 직접 호출해 dSYM 경로와 GoogleService-Info.plist를 명시합니다. + static let crashlyticsUploadSymbols = TargetScript.post( + script: """ + UPLOAD_SYMBOLS=$(find "$SRCROOT" -name "upload-symbols" -path "*/Crashlytics/*" 2>/dev/null | head -1) + if [ -z "$UPLOAD_SYMBOLS" ]; then + UPLOAD_SYMBOLS=$(find "$SRCROOT/../.." -maxdepth 6 -name "upload-symbols" -path "*/Crashlytics/*" 2>/dev/null | head -1) + fi + + if [ -z "$UPLOAD_SYMBOLS" ]; then + echo "warning: Firebase Crashlytics upload-symbols not found. Run tuist install." + exit 0 + fi + + "$UPLOAD_SYMBOLS" \ + -gsp "$SRCROOT/Resources/GoogleService-Info.plist" \ + -p ios \ + "$DWARF_DSYM_FOLDER_PATH/$DWARF_DSYM_FILE_NAME" + """, + name: "Firebase Crashlytics", + basedOnDependencyAnalysis: false + ) +} diff --git a/Tuist/ProjectDescriptionHelpers/Target/Dependency/TargetDependency+External.swift b/Tuist/ProjectDescriptionHelpers/Target/Dependency/TargetDependency+External.swift index 7b51de0a..c78b39e1 100644 --- a/Tuist/ProjectDescriptionHelpers/Target/Dependency/TargetDependency+External.swift +++ b/Tuist/ProjectDescriptionHelpers/Target/Dependency/TargetDependency+External.swift @@ -20,6 +20,7 @@ public extension TargetDependency { case FirebaseCore case FirebaseMessaging case FirebaseRemoteConfig + case FirebaseCrashlytics case GoogleSignIn case GoogleSignInSwift From 3a2136ef1be58bad31ae960f59659a07d27584da Mon Sep 17 00:00:00 2001 From: Jiyong Jung Date: Wed, 13 May 2026 14:18:23 +0900 Subject: [PATCH 12/44] =?UTF-8?q?feat:=20=EA=B8=B0=EB=85=90=EC=9D=BC=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=EB=93=B1=EB=A1=9D=20=EC=8B=9C=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=EB=A1=9C=20=EC=95=88=EB=82=B4=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=80=EA=B2=BD=20-=20#289?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Dday/OnboardingDdayReducer.swift | 18 ++++++++++++++++-- .../Sources/Dday/OnboardingDdayView.swift | 3 +++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/Projects/Feature/Onboarding/Sources/Dday/OnboardingDdayReducer.swift b/Projects/Feature/Onboarding/Sources/Dday/OnboardingDdayReducer.swift index 04295872..e5a61d4b 100644 --- a/Projects/Feature/Onboarding/Sources/Dday/OnboardingDdayReducer.swift +++ b/Projects/Feature/Onboarding/Sources/Dday/OnboardingDdayReducer.swift @@ -32,6 +32,7 @@ public struct OnboardingDdayReducer { var showCalendarSheet: Bool = false var isLoading: Bool = false var toast: TXToastType? + var modal: TXModalStyle? public init() { self.selectedDate = TXCalendarDate() @@ -53,6 +54,9 @@ public struct OnboardingDdayReducer { // MARK: - API Response case setAnniversaryResponse(Result) + // MARK: - Modal + case modalConfirmTapped + // MARK: - Delegate case delegate(Delegate) @@ -100,14 +104,24 @@ public struct OnboardingDdayReducer { case let .setAnniversaryResponse(.failure(error)): state.isLoading = false - // 이미 온보딩이 완료된 경우 (G4000), 성공과 동일하게 처리 if let onboardingError = error as? OnboardingError, onboardingError == .alreadyOnboarded { - return .send(.delegate(.ddayCompleted)) + state.modal = .info( + image: .Icon.Illustration.heart, + title: "메이트가 기념일을 등록했어요!", + subtitle: "이미 우리의 기념일이 저장됐어요.\n이제 함께 시작해봐요 :)", + leftButtonText: "확인", + rightButtonText: "시작하기" + ) + return .none } state.toast = .fit(message: "기념일 등록에 실패했어요. 다시 시도해주세요") return .none + case .modalConfirmTapped: + state.modal = nil + return .send(.delegate(.ddayCompleted)) + case .delegate: return .none } diff --git a/Projects/Feature/Onboarding/Sources/Dday/OnboardingDdayView.swift b/Projects/Feature/Onboarding/Sources/Dday/OnboardingDdayView.swift index 2ed671d7..c3203d98 100644 --- a/Projects/Feature/Onboarding/Sources/Dday/OnboardingDdayView.swift +++ b/Projects/Feature/Onboarding/Sources/Dday/OnboardingDdayView.swift @@ -60,6 +60,9 @@ public struct OnboardingDdayView: View { ) } .txLoading(isPresented: store.isLoading) + .txModal(item: $store.modal) { _ in + store.send(.modalConfirmTapped) + } } } From 152db689b9cafcb1c3ccd55b065112178735dfd7 Mon Sep 17 00:00:00 2001 From: Jiyong Jung Date: Wed, 13 May 2026 14:19:24 +0900 Subject: [PATCH 13/44] =?UTF-8?q?feat:=20=EA=B8=B0=EB=85=90=EC=9D=BC=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=EC=97=90=EC=84=9C=20=EC=83=81=EB=8C=80=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EC=99=84=EB=A3=8C=20=ED=8F=B4=EB=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20-=20#289?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Dday/OnboardingDdayReducer.swift | 64 ++++++++++++++++--- .../Sources/Dday/OnboardingDdayView.swift | 3 + .../Sources/OnboardingCoordinator.swift | 2 +- 3 files changed, 60 insertions(+), 9 deletions(-) diff --git a/Projects/Feature/Onboarding/Sources/Dday/OnboardingDdayReducer.swift b/Projects/Feature/Onboarding/Sources/Dday/OnboardingDdayReducer.swift index e5a61d4b..12c1f84a 100644 --- a/Projects/Feature/Onboarding/Sources/Dday/OnboardingDdayReducer.swift +++ b/Projects/Feature/Onboarding/Sources/Dday/OnboardingDdayReducer.swift @@ -25,6 +25,12 @@ import SharedDesignSystem public struct OnboardingDdayReducer { @Dependency(\.onboardingClient) private var onboardingClient + @Dependency(\.continuousClock) + private var clock + + public enum CancelID: Hashable { + case polling + } @ObservableState public struct State: Equatable { @@ -43,6 +49,9 @@ public struct OnboardingDdayReducer { // MARK: - Binding case binding(BindingAction) + // MARK: - LifeCycle + case onAppear + // MARK: - User Action case backButtonTapped case dateSelectorTapped @@ -54,6 +63,10 @@ public struct OnboardingDdayReducer { // MARK: - API Response case setAnniversaryResponse(Result) + // MARK: - Partner Polling + case pollingTick + case pollingResult(Result) + // MARK: - Modal case modalConfirmTapped @@ -75,6 +88,14 @@ public struct OnboardingDdayReducer { case .binding: return .none + case .onAppear: + return .run { [clock] send in + for await _ in clock.timer(interval: .seconds(3)) { + await send(.pollingTick) + } + } + .cancellable(id: CancelID.polling, cancelInFlight: true) + case .backButtonTapped: return .send(.delegate(.navigateBack)) @@ -89,14 +110,17 @@ public struct OnboardingDdayReducer { case .completeButtonTapped: guard let date = state.selectedDate.date, !state.isLoading else { return .none } state.isLoading = true - return .run { send in - do { - try await onboardingClient.setAnniversary(date) - await send(.setAnniversaryResponse(.success(()))) - } catch { - await send(.setAnniversaryResponse(.failure(error))) + return .merge( + .cancel(id: CancelID.polling), + .run { send in + do { + try await onboardingClient.setAnniversary(date) + await send(.setAnniversaryResponse(.success(()))) + } catch { + await send(.setAnniversaryResponse(.failure(error))) + } } - } + ) case .setAnniversaryResponse(.success): state.isLoading = false @@ -113,11 +137,35 @@ public struct OnboardingDdayReducer { leftButtonText: "확인", rightButtonText: "시작하기" ) - return .none + return .cancel(id: CancelID.polling) } state.toast = .fit(message: "기념일 등록에 실패했어요. 다시 시도해주세요") return .none + case .pollingTick: + return .run { [onboardingClient] send in + do { + let status = try await onboardingClient.fetchStatus() + await send(.pollingResult(.success(status))) + } catch { + await send(.pollingResult(.failure(error))) + } + } + + case let .pollingResult(.success(status)): + guard status == .completed else { return .none } + state.modal = .info( + image: .Icon.Illustration.heart, + title: "메이트가 기념일을 등록했어요!", + subtitle: "이미 우리의 기념일이 저장됐어요.\n이제 함께 시작해봐요 :)", + leftButtonText: "확인", + rightButtonText: "시작하기" + ) + return .cancel(id: CancelID.polling) + + case .pollingResult(.failure): + return .none + case .modalConfirmTapped: state.modal = nil return .send(.delegate(.ddayCompleted)) diff --git a/Projects/Feature/Onboarding/Sources/Dday/OnboardingDdayView.swift b/Projects/Feature/Onboarding/Sources/Dday/OnboardingDdayView.swift index c3203d98..55b6faf1 100644 --- a/Projects/Feature/Onboarding/Sources/Dday/OnboardingDdayView.swift +++ b/Projects/Feature/Onboarding/Sources/Dday/OnboardingDdayView.swift @@ -59,6 +59,9 @@ public struct OnboardingDdayView: View { } ) } + .onAppear { + store.send(.onAppear) + } .txLoading(isPresented: store.isLoading) .txModal(item: $store.modal) { _ in store.send(.modalConfirmTapped) diff --git a/Projects/Feature/Onboarding/Sources/OnboardingCoordinator.swift b/Projects/Feature/Onboarding/Sources/OnboardingCoordinator.swift index 9211d770..26a3d1fa 100644 --- a/Projects/Feature/Onboarding/Sources/OnboardingCoordinator.swift +++ b/Projects/Feature/Onboarding/Sources/OnboardingCoordinator.swift @@ -403,7 +403,7 @@ public struct OnboardingCoordinator { case .dday(.delegate(.navigateBack)): popLastRoute(&state.routes) state.dday = nil - return .none + return .cancel(id: OnboardingDdayReducer.CancelID.polling) case .dday(.delegate(.ddayCompleted)): return .send(.startNotificationPermission) From 47b589b5fb8d5a588d8fb54249e9831ad2e30c95 Mon Sep 17 00:00:00 2001 From: Jiyong Jung Date: Wed, 13 May 2026 14:25:57 +0900 Subject: [PATCH 14/44] =?UTF-8?q?fix:=20=EC=BB=A4=ED=94=8C=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B4=88=EB=8C=80=EC=9E=A5=20=EC=A3=BC=EC=86=8C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20-=20#289?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/App/Project.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Projects/App/Project.swift b/Projects/App/Project.swift index 7aa720fd..52daecaa 100644 --- a/Projects/App/Project.swift +++ b/Projects/App/Project.swift @@ -61,7 +61,7 @@ private let commonBuildSettings: SettingsDictionary = [ "KAKAO_APP_KEY": "fb2997e54bfe080cc5c1d9706d1251f4", "GOOGLE_CLIENT_ID": "48737424560-adiebqu29lsflj85v9vrd4e4a3cp6sa3.apps.googleusercontent.com", "GOOGLE_REVERSED_CLIENT_ID": "com.googleusercontent.apps.48737424560-adiebqu29lsflj85v9vrd4e4a3cp6sa3", - "DEEPLINK_HOST": "keepiluv.jiyong.xyz", + "DEEPLINK_HOST": "keepiluv.teamtwix.com", "API_BASE_URL": "https://api.dev.teamtwix.com" ] From ef66117c41c1ff478819f7e7d7b54393a1441732 Mon Sep 17 00:00:00 2001 From: Jiyong Jung Date: Wed, 13 May 2026 21:44:25 +0900 Subject: [PATCH 15/44] =?UTF-8?q?fix:=20=EA=B8=B0=EB=85=90=EC=9D=BC=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=8B=9C=20=EB=8D=B0=EC=9D=B4=ED=8A=B8?= =?UTF-8?q?=ED=94=BC=EC=BB=A4=EC=97=90=EC=84=9C=20=EA=B3=BC=EA=B1=B0?= =?UTF-8?q?=EB=A5=BC=20=EC=84=A0=ED=83=9D=20=EA=B0=80=EB=8A=A5=ED=95=98?= =?UTF-8?q?=EA=B2=8C=20=EB=B3=80=EA=B2=BD=20-=20#289?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Calendar/BottomSheet/TXCalendarBottomSheet.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Projects/Shared/DesignSystem/Sources/Components/Calendar/BottomSheet/TXCalendarBottomSheet.swift b/Projects/Shared/DesignSystem/Sources/Components/Calendar/BottomSheet/TXCalendarBottomSheet.swift index ff4a30d8..f53d5162 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/Calendar/BottomSheet/TXCalendarBottomSheet.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/Calendar/BottomSheet/TXCalendarBottomSheet.swift @@ -216,7 +216,7 @@ private extension TXCalendarBottomSheet { func datePickerView(height: CGFloat) -> some View { HStack(spacing: 0) { Picker("Year", selection: $selectedDate.year) { - ForEach(2026...2099, id: \.self) { year in + ForEach(1940...2099, id: \.self) { year in Text(verbatim: "\(year)년").tag(year) } } From 0c959c43231e671af0744affc00577f252b0ad93 Mon Sep 17 00:00:00 2001 From: Jiyong Jung Date: Thu, 14 May 2026 14:54:21 +0900 Subject: [PATCH 16/44] =?UTF-8?q?fix:=20=ED=99=88,=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?QA=20=EB=B0=98=EC=98=81=20(#299)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 변경된 에셋으로 교체 - #298 * feat: 알림 센터에서 시간 시간 표기 추가 - #298 * fix: 로그인 화면 UI 버그 수정 - #298 * fix: 이미지 품질 개선 - #298 * docs: RelativeTimeFormatter에 주석 추가 - #298 --- .../Feature/Auth/Sources/View/AuthView.swift | 13 +-- Projects/Feature/Notification/Project.swift | 1 + .../Sources/NotificationView.swift | 23 ++++-- .../illust_invite.imageset/Contents.json | 18 ++-- .../illust_invite.imageset/illust_invite.png | Bin 12620 -> 0 bytes .../illust_invite.imageset/illust_invite.svg | 78 ++++++++++++++++++ .../illust_invite@2x.png | Bin 29870 -> 0 bytes .../illust_invite@3x.png | Bin 46253 -> 0 bytes .../illust_singing.imageset/Contents.json | 18 ++-- .../illust_singing.png | Bin 22962 -> 0 bytes .../illust_singing.svg | 69 ++++++++++++++++ .../illust_singing@2x.png | Bin 55064 -> 0 bytes .../illust_singing@3x.png | Bin 89268 -> 0 bytes .../Util/Sources/RelativeTimeFormatter.swift | 44 ++++++++-- 14 files changed, 219 insertions(+), 45 deletions(-) delete mode 100644 Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Illustration/illust_invite.imageset/illust_invite.png create mode 100644 Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Illustration/illust_invite.imageset/illust_invite.svg delete mode 100644 Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Illustration/illust_invite.imageset/illust_invite@2x.png delete mode 100644 Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Illustration/illust_invite.imageset/illust_invite@3x.png delete mode 100644 Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Illustration/illust_singing.imageset/illust_singing.png create mode 100644 Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Illustration/illust_singing.imageset/illust_singing.svg delete mode 100644 Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Illustration/illust_singing.imageset/illust_singing@2x.png delete mode 100644 Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Illustration/illust_singing.imageset/illust_singing@3x.png diff --git a/Projects/Feature/Auth/Sources/View/AuthView.swift b/Projects/Feature/Auth/Sources/View/AuthView.swift index afc16ae6..755d4093 100644 --- a/Projects/Feature/Auth/Sources/View/AuthView.swift +++ b/Projects/Feature/Auth/Sources/View/AuthView.swift @@ -94,12 +94,13 @@ private extension AuthView { } var titleView: some View { - VStack(alignment: .leading, spacing: 0) { - Text("함께니까 멈추지 않아요.") - Text("지금 바로 키피럽 시작하기!") - } - .typography(.h3_22eb) - .foregroundStyle(Color.Gray.gray500) + Text(""" + 함께니까 멈추지 않아요. + 지금 바로 키피럽 시작하기! + """) + .typography(.h3_22eb) + .foregroundStyle(Color.Gray.gray500) + .frame(maxWidth: .infinity, alignment: .leading) } var loginButtonsSection: some View { diff --git a/Projects/Feature/Notification/Project.swift b/Projects/Feature/Notification/Project.swift index 647b6c64..e4b68425 100644 --- a/Projects/Feature/Notification/Project.swift +++ b/Projects/Feature/Notification/Project.swift @@ -20,6 +20,7 @@ let project = Project.makeModule( .feature(interface: .notification), .domain(interface: .notification), .shared(implements: .designSystem), + .shared(implements: .util), .external(dependency: .ComposableArchitecture) ] ) diff --git a/Projects/Feature/Notification/Sources/NotificationView.swift b/Projects/Feature/Notification/Sources/NotificationView.swift index 26d4f850..3ccd68b3 100644 --- a/Projects/Feature/Notification/Sources/NotificationView.swift +++ b/Projects/Feature/Notification/Sources/NotificationView.swift @@ -8,6 +8,7 @@ import ComposableArchitecture import FeatureNotificationInterface import SharedDesignSystem +import SharedUtil import SwiftUI /// 알림 화면입니다. @@ -134,13 +135,21 @@ private extension NotificationView { Button { store.send(.notificationTapped(item)) } label: { - HStack(spacing: 0) { - Text(item.message) - .typography(.b1_14b) - .foregroundStyle(Color.Gray.gray500) - .multilineTextAlignment(.leading) - .lineLimit(2) - .frame(width: 276, alignment: .leading) + HStack(spacing: Spacing.spacing9) { + HStack(alignment: .firstTextBaseline, spacing: 0) { + Text(item.message) + .typography(.b1_14b) + .foregroundStyle(Color.Gray.gray500) + .multilineTextAlignment(.leading) + .lineLimit(2) + + Text(RelativeTimeFormatter().displayText(from: item.createdAt)) + .typography(.b1_14b) + .foregroundStyle(Color.Gray.gray200) + .fixedSize(horizontal: true, vertical: false) + .padding(.leading, Spacing.spacing3) + } + .frame(width: 280, alignment: .leading) Spacer(minLength: 0) diff --git a/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Illustration/illust_invite.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Illustration/illust_invite.imageset/Contents.json index 52030d18..83eac1a6 100644 --- a/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Illustration/illust_invite.imageset/Contents.json +++ b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Illustration/illust_invite.imageset/Contents.json @@ -1,23 +1,15 @@ { "images" : [ { - "filename" : "illust_invite.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "illust_invite@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "illust_invite@3x.png", - "idiom" : "universal", - "scale" : "3x" + "filename" : "illust_invite.svg", + "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true } } diff --git a/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Illustration/illust_invite.imageset/illust_invite.png b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Illustration/illust_invite.imageset/illust_invite.png deleted file mode 100644 index b625d6bb438b993218312d0c4e70ac39e47bd16d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12620 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 diff --git a/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Illustration/illust_invite.imageset/illust_invite.svg b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Illustration/illust_invite.imageset/illust_invite.svg new file mode 100644 index 00000000..a377c678 --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Illustration/illust_invite.imageset/illust_invite.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Illustration/illust_invite.imageset/illust_invite@2x.png b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Illustration/illust_invite.imageset/illust_invite@2x.png deleted file mode 100644 index 1068a1f7537e9acc43d4cb5547deed10d2052262..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29870 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 diff --git a/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Illustration/illust_invite.imageset/illust_invite@3x.png b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Illustration/illust_invite.imageset/illust_invite@3x.png deleted file mode 100644 index fab201a2494eeecaf2baf8292fa109869e4444af..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 46253 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 diff --git a/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Illustration/illust_singing.imageset/illust_singing.svg b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Illustration/illust_singing.imageset/illust_singing.svg new file mode 100644 index 00000000..a27c591d --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Illustration/illust_singing.imageset/illust_singing.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Illustration/illust_singing.imageset/illust_singing@2x.png b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Illustration/illust_singing.imageset/illust_singing@2x.png deleted file mode 100644 index eba1a20dc7f1da441a232df766887c412a576669..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 55064 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 VWmq+#XFrK*_Hgrdt)MW{{|DCKsWbop diff --git a/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Illustration/illust_singing.imageset/illust_singing@3x.png b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Illustration/illust_singing.imageset/illust_singing@3x.png deleted file mode 100644 index 8af04bc65d31815d9ad162572b88f78e67bab3cc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 89268 zcmd42hdBYTq_%HDf? z5bihn{_guvxIG>o9^T{ix~}VWUC(R2gH%=INr-8PK_CzbR6$N11j0uGzw1PVz?I3C z>^$JlO?w4xM-Yfn=<*lmdGxb4z(pKKb$J<3UN7AW@DKi5>6g+VP+=(euMq(VM-K{> zlh$~Tvwn8l>A{iblB+jPKQn6(#ogK4HhI`CiLx}g50>^^EZPeW1ac~hNi1&(*gL`x z$63hxA6nJg*dTKzry_nZo7%+lwXsRRU29b@2PJI;mA<@{J*D%`GC}nn+UAk_&2r@2 zi;3+{9vdH!av%!76Vl*SPtCjE@++?L;;k3&(V57Ns^5A<_w+!ni(ZSTXXkPNssI1` zkqYTemNX>0CM+d*GbAL0mxF_&_(XvF`kK@yZj!J({R??b&7Fx-t37dX@zALcYuM*F z*Z0LH{ZKZpPR?8w*-kRD8Nwt{Bly>M9&qs}RGs7K>s!&V_N+{H`1<;~UB4tHU&MgW zYffvXxl5vX_8U7=-j=P7d-c|`nEtuwQg2c*!%AR)+>xc0M%X>CsKLd>#h}Ks@yz9$7g0FA7Ag4L8Uj!Mx~-C{&(SB$|ggO9u}rpQhWV`so`sBWS`*oJPXq zg=|K||J_qk@`EIo*4kKGe|2(pet;?62pOKfrlTk|Lt4F3WOCa2+n|_@-Sx#yOWm;f zz0Z!CR_;Smh?jU7!i%ie+f?Li0NdbZU^Mf;AyS+wbS4wx8C9=kVy|Q@tuAckjxqa& zj4v+}By_~PS`PIjilL*trbdsZn`?N9mCGW<$*=L(^-e2LeudrPXYxT&|L9Sll>@fM z%lQ8NyZhY4G*RUBbI2u)u=!#sgo%j>YJ!eQQg!e*Vsk{-PXEsJ{gRLffDIi&N8J>Z zAn(-F6pQu>$56^xJ+kW zS#x!U;K=>c#P;TH)@!=pE4wq=+S>PrXcJiY;LR6;`F1SDdX?!jTNN-?*v)?P4<9}} zH1QX_e!7SODkgwBSsljAm1?kQFMONsUTOQ@ZaF6NPa>)bfZlSMhGW??$aA=dnk%ht zM()?;JP>s;72vx*Z;Kb2T=b?eMa?|Dxb>Bl%JKkh#XaPovT{q_^n2~sry>G-37i*= zZX-|aL;`)HcFNs@XaCM9W$5Pv$g%YSFNu2VTvPNQ_42CDP-sHe?AKHpybOZc3$hK{ zD`FY8Xh@MB&SkYd*0#3a@6pleh21LLaxgKOtN4*`(I$2MRG8Jv8_)$y8}|sTup-OJ z>FE+g*7Ep#{~T&ZSeQqi8 zDX?p{z1dWT?LC#ed9}dmQMpDH zL_4r)*Xxv$ni{d3(+^rR8{q1h0Ee|Bgk#D-2xnld1SCt-^mVM8(bE;OYElC)47!AI z2Mw+b+go$jVu5z*ht2C=T0+6X`))F;xxWln0S?#PYdRrRpJ5qWpfMQ7cr{rvlSEPz zz~Ri4180@9^^mkWc@=WSVi-OcnHM$P;dW0=Hl-x<=+(w}@zOJg$kJ*XKnK%AuFnYd z=R=s2XEQv7FL_j|iBNm;S87GnfZpw&HAcVM9&H*FpV8mD7kiC8K2&HTB(C}H*dct4 z*pB=5eA$)}fO0yI0)i*c*Ny}gov)v>h{_ut9_EED-q2<(OrdvtWM^Ynimy2PzFMm^ zTD;b7aKT0Vn#2bguu0CZ{j}iMFN`I}tWF2tm`S{f;sy}U&epcKmB8}D%+={t{O73& z{*nDp&e(p~d8p%3FUeX@I6I|CWm39^taIqlUc~<%*!W3VHjq&7DEIO64RmQOlvI-# zW5!S}A@P~;#trHp&outoi^~f75N8;z*V#Qzmr-+TvG?RJ1RZfjniK{GXcUd|*OeZl z93FXS&)Q@M@I+pjNW9LFs(ELNYU`n=qvM{2o*Cuvwbi7+`EOk6lyhP-i$*?Y@2g#z z0^VVXZ~x9i^-I-9h+!6?qJU%vZbs~Fhs-G(XXk@JiNiLlzxK4>C8Ym}FL2QQ(t)g8 zBAY{Vj*=m(ystVJV>}opK?6y;`B&L|f1@(C6U~2mpXAmpzRZ99i9ZrWJx30Xzih&(}DV)P78AY6Mrr{EAczn_r8R$_dyiW@@f@j1D*7 zFH8s%8DF{t1j@obt3zI>@$@y0*~_D)k@+K1(2pTEb?Xdx6h6jDkhn1)Of?%Fl|On~ z+@@xi*;S5^Dy@(6K`VYSn8gfSL+`#2Ipo;#X5`Pr@?b4Bo@~X8(74epyR`5vJK&I& zfA0xsgxod7G&sAuzLF=I?S0p9$@^r_ms2ycB~@1} zguPOMK?sg172&maUZ?WeVvV(P0}C6>0GF-zh`oSQ3X>tyq0`qX3)#95XS7L?3}w?L z;5PRDyz%x=kL5K23ePz`R~N=iSAIBHec%0iNjZOeMA;cxdc4x}{1#YZJo)S6nSWvK2Qe&dXfbsIIhDFd)Y_@brnUX`o}4p+Df!+`P+(Wa2n1G-jEG(F z_gf-Mce3ffjW=bvo@3i@=WhG-&Pv&WeK_ z5B?>YZuIjLDn$pAaDL00_2i#&D;F`W#2=m4)FpM#dN970RnlT!Vo8$puCoKcP<=*R zud7Wr10L@i2U8qxu+h2pQ0>XP3<#@_)D;#C&th6u$@AG;Wn$_IPK!k(b6PtwSqG;y z5s}*eIRjx>PPQ<-Dp}rHGV40BGgW*Z=5+QAD}^w?%V;ZtORIND+44t}ZeewtkyezF z7z{>!-*x%@nCJgaQe0o4LO8FpC8DywEo72nPg|Fu2RHcq+)G!DbN6&lQ@Z%R5|UihWxas?dtG4l z>At)8W@lfg7vZ5ORaw(R%s(CC(qFjPZn|rAgEnXPqQ@QfWDm(WyvPE5!Jf|v8BEx* zq+gow*|Egho}4;qTpg%SaaEIm&RS$$0ewD?32OvL>M)OF$AXJszWx?q_(45AJ$12L z9k@m5+})4UhOc5WpY_$?>f+YaVQrGg0RH~ikoemRt4DX?At=THFR)>5n)1=9mWDz) z&d=1#6bGvP3vD|+=cPe^Bz&dnAe9yL!pFZ-P`oehbA*m>pQ#3e4aHPStNm}uCl9o_ zH(kh=;>>ZwbltE4ZB3h9vt&+JwC?6*GIYFUY#GLADskDl5l32P*XRswA=o$GG$$DG z>(}+^jeav_o^vAA22X^Tg(C3vx6mMAwn zSd}WQjy@sA>8F44OFc?T$7?5KFJI2rjvELd=K82B%X*8~SlfG?))E2+1OAX9&fJ16 zXnk_J(M&|SUajYoAu2Bgzx5sP%}**n&Lq!|OpFZLr^?<1Cy5fcbMxqHJSis8U=v39fULLz~xpI3a+~NY~g$r zvM{)D0ci~oQPJE}z6GQkU*={5G%@_(*8S61q(H$TZ|(h~3w=9&_5Rh>Rq`prQ-da* ziADZ<2@0awg)X8beV6dtc)ET%diwwI7YfRg0B&&kB84anG$LA`ZU$PS;?B>{Zz5!( z(DZL{>ncF6j?OHqn?$cDR?YciG+Bx^V=bqFS_L|b|NS#eu~Yls$5c&C%~0qUiaLD4W&)U1(WF!^XP#cD7Ub(IWFTuGand2&A-}+C;C-0+62F&=@>F%SJ6; zyxa}PwH~Kq0g|!9^0}RDzj*C4iGYtsZp{OsmOlI}Y(s57bjD?tm@A8R>8^Nii_!@* z<*cc=0#~v%<=pL&yj9et0(sKC$OxG=r>HDK)Be*Pg)2{{LuOJ$#)@comNwJu#DR`j zcN|YlYXIDL-9^P#bCkf9mLBczcfVLqzvD}Nz0Dku*J7AdDcUsO7n`l+8`OS)WTLY& zS5Z+>l;H3eofndJLN1SLhG#Roj#51g^|Xeuo$MbRr?)!(A!M{YyTUF)>S-=cqtjT; zcOI47^LZ5Se!zR)Gxo*-yYrz7_3WIBf2+`AphmXWL8Hy6jZ%%SJtOG=^*NYEm9F2F zr77C;=&SD7Y!k(S5ob*S@gu7?aJ6CitBG5vf;NNte4CjFaYCB-44&l=Kzy!#RV#%}>1F>Nn2C5cuCtK$2Xyhz>$MLPNs(o3R!x@110)?7t? z@lch^)jGJ%%n{1n823mk(Ub?j&e&}Wf|qBnvIW>*8C!oWH`ug>dh&5N{k%Qv>#n~L z5}PZl2{f@JYW|x#*E`rY1E`gzaxq-+ED36+Q1OIY`Q5?MwO`b9KHky*^DEM?Tcl_J zFFTY>VFTdelUwPCvS3(oRn-TG&}hd}lis5zoJmn(jK8?Mov+?x;HdJWMZ^A!JZB+^ z6c7|-(X*=D#bpBXwKi=Y9iD#r(&#I(nFUE>=LBQ5VCIrNZjM2N-xGe2U8lbB{glq~ zfH7MW;LGcSg)9-Sj%=|zxR>#B??FTJsYE6dgv{~JvwWUUlgFJ;uFi6B1h#=^c`Gu} zn)Zt>dV21M*P4m-pRV^@^j3}$d||@#@0``U=WH2!VF7nRn5O6WAC6h$ zjGh;?O`;Kl(@U1?4xP*?yr3zhvR*y9z6 zkN|9e=0>55_{CCH42pIZFC9J;5Rx?FqwWqU3l2OjpjFF!eD%;7mv z+DAhavb-cgIVPsoHJYO?J?DW*=o*$(6bZeoXM=noC436IZZO_LyviZl zx5U8-vhxT;X{eXD6@nJqU|jlY%$YwS)5DYQvJ;_yPvB(>uRiny%7m zDHbcB(1;Nj)4{HM*<*`AzeRKE+Qkz$_zf$(keoSxA!!(`J2i{;-gl2Dq80moH~-8Y z&MsTUKUJ<4!@hc@unIEeC2<7zXeb^4;KoTubQ~r@BE<|ipgt}4IH88gr8Bta-5tg` zs$cJirJM`<<>^0%H!Q}P`#tpbKA5`Gc*?0)w94Y?5gPQ7k2Ss|YSpivwroZ~mc8e! zj7$ly4v(?E+>@p^X(ajLI?R>2fp7=n@q{(U%R10A%(wel6H_$;d>96rp#Q?E4oAVs zKsLkwI^^!oak|cwc@11}iSR|W*r&+@oIh72Lzo7DDomFm;-u+JF21YrMhlm|Vc$9g zQ^S56vVR7It=0%8I9zuM{mI2~nru@;<|$aZwyh-XdBoGJ#I&DH{j!uKZZZs2D-P5H zi(B4fc&M8M#A>EC`Wx|hcZ(y>;t34737i-74ESHt58bcL`R{mbPBEq%#MD>bB0*Dp!vvaLE&`SG(yFf?@(qnsm_6;V#3TrMOtDw6!(T>V z+iX?TDegFNEzh~N55sn^q+k)i3uJ{l)d@lZU#kPgq4HeY{aanQ;XAvh6Z#<HgeW ziMOSH08|~xKO(^MC&vLwk7A$9vA?A%hw>xOxo|6 zROHi+RzrI%E64wdA)tkGrEm3q{w1FGrwsv?|G`K_0aS|Vga6aNr{tKt@fH#Kw!N?R zReAERxj2qgo`nye1RtL%<4Ku6r1R?)@og&r7M9+cbIFOyRs_fK;>DR9@*en(sOjks zaOL^+nwtUj412SOQkj&*{&j*g9(x56GdibkhQHcY+q$1{bAP+Ufp;yJ%TSSOJrAxE zC5P{rJek%`Chie?KOH07`m%=-$(Bqj(bs;Z4{r8 zYK56#-I2KYJS#o~{*9+jlMdAMN}z22(tPcMfs>OHw}h80FquoMpVu^JM|Ns2v_)}f zGB?8+c=I$^+_+vA!z8+cV9V(4^mop->c1pHt+wB*tCI=#bqxD1g2+t&giEwd>na_n z-D5`rtL(DzQbHFvo$k}o(NP~^B1lKKp9w@PYfSJ4{S}rxvDM;8z&J*vncs9bSdlWe&yd z+6b^t$mGR(I;ZI{*%F2mD&7Gy?7dmCe4Y+?D#ik#bowmh1E;fL*1m%nJ1Fc?f}7Lv z?t+o0$_J@3G%MKo@78lqFVXSINzsht^jDH?WhM_PFRNO|k;g#Bvlp;V;4YW&de+x( zGqhA7B*yb>kl5+){3>4Wn`w&=(DM+`ON zrd?(AZ|k)8LIp z7`7psp{H;O6>&nxuW8_Ux0D|LQf5hvG1AB!coM`$kL%?}6t6ECN0~nx_kZuq2t+iS z<5<-;K9YWhsz`hTA(#Al4#m_0j|(Iaxkaqe6a4t-ieS?{FPHVP9W4%> zFPCLN{x>j}3|gAqr-keufU*IKPt5 z50(}WHiW@pt2rXUveyiVJOZSy9hAF5y=_3yodd-FA2@>6_m=CFt0z{YOBsJT7$NH( z3?%nKq0&FS$eD4HurO()W{;)OF@}n!175A8HsN!9fKD1=rn3R^3Fc@^zU(401Zv%p z^1yK;hVVT=-iHwXb(2+0)ynyS8}_}?26dUt#3v9yMK8@Dux^HSNZ*1s;b@j3OM3=TIBHu;;tz+jhbt1@ms913ujUtgLBtOx;q1Ys)kuCk-V zT2lp&nOGG2{2&*=Q)*7bD=K|7ATVv>rmX(PD8b*hF}YAYKp=85qwT80+&?ZNpujT? zeh;Qn!Qa|L%Vx&J#DwZHDg9d7@k`ukxR9I){xj9euFh!@hQ-NgJfl+)Ef}yjS3G*m zP9%&C{X_1c;O%`;@#&&F2^^t8Zv@Z}0yR)RcmTN%cIVS7NHKYoyDDG)U?7FLY*l_M zKlaYgp`oEc0X8s=^?@LXD7!>H{TPbUFBWKG90xM|c-TCL*wnpu8YeFn9Rakxhmg3E zWBUQK(f`sRq0~7b8}8u=&N*T0kqpks6{h>|NV(b^Pn!d%K?hqp7n7lM2w>Hbuwjkv z{X);;Rc?ns@n|C>B+aWehmtqEv(HMNu9R$!0uj!)fu+G`K#U+I&!E75NAtiWw}|=P z(=ob?-(jX_uDUT1<--$cXz!ozONr4gMQ!~FvKj*-=EM4BMfy6@nz0p6J9G#ifFrtZ z0bEnx1zZkLEHaUmm0dbWR?%k#OR5&dMRE`%eBl=o)6f$+P3vh;cs})}J z^JTBzWi~H$N)CCxJqbj#M7n=4KUd4Dsze^-zCeaiHcoWl(%KD|{C;C6TA2LO%~pLq z*p}NLIx>+h_!+V-l=*;J+duKsg$iIT3d(@XV)HZ*%Sgx_P5@m39iH<|^x)H0v=usf zLzf^zlPOXNEW)`>bhE5m@71fm0hb;dv^mg*lcP`2&hZ#1%sdsd#0&;KwoWM}DT{8X zo9G+yYbkjsOA=zf&&(-AGg`nluuUQ5j?i*^VMlD1aH;A+^ty+eQ@Y#c^x`RYkJVW` zrxOkJ^ynx7zUGN3q5K648zwqad0*nOk@)p~iZe;zN_Wm4Vg{>&p|&llAA^TkCIPW5 zy)sr8o&4@(8@1o+bjYmG<@S3=D=swh7mjg#&1>2CXRDicp?0uj5LF{S*8z$Srw*68SH zpZjn+)h4y&F&*3))fZBRT*kT5nv_y3jsw=V%JZtQh3W=>&15nS@F$UiyTVx) z1;x=m^VLH)H{Fcz@gTm5R4!aU^Mf>qeiMH{`7r9S0+1sw)8Rw8$S4EvX-hUj*lb_jdN;k${Jo!J0$aMwkva;R^q=i`bT0Q1a z46Yehp_oh! zGQ6p3_pSp8>9C)K&#<-vdu@x%Jqp3IxKGB>-{TcbB*HNpCeojvpb0jhf;rKfx7U^LwTgLPAEnVz05H7<;8cNG3VmhR;1cLnHamV{UG2(no&l<;AE(7Aj|$_y0L z%qx>}IzS&CaY7MIa=H~kwW}4128FAw0JtA`l8ors{ql_jh z;rvk;_G6+4Vnk)29k|f5P86?wS(YU&2*w7UmHf7cd)I|35bwVg$rCj49Z=Rey-H=; zZ~LJHc4flxfSA#_GuEFU@fiw)c@ZBR70TEVdBsnmM`unnjUn{RE7g0Ar+gK{mtl@F z-5aAppzF52w5DM%8+)~q1vRizae=hq7=1AM@A%l55cbJt8|3*&03}R33T{6qZcD`s z!+!)HSWt{L)w^YDe28EVQRD^G86+h^24RArcjar~*(D#tE24S8wm>T|REe0AEQsh2 zQ3N`0EK5T7LFm5(kjM{(-vT@T&nSe%Av#hkcD~dyiD)|O)I;uL!VsclR-1^Lk{O}E z%k_=nx?(o}Kw7=2bMZo>`(;sqInZM*X_l?{JQTlnmy$VWC-)PS=I%EI;u_ER+t$q2 zZ~P2lU?ZSnZs~dX&Wxy>P*?ur3$d`f5OwA}{)zK*D^s>|N#D|q|v#7zGIpl08%>0E6VT`+x`YP5} z0)V7T(M^*ckO?e18kHUpGGO$Wi_A=V^{sUDCDKRKIZh^5$|` znV2*a%7+qc6_d&1`N&eODk#>R6^|4ZM3>zsdC3eYQ0rJCJy@o*TvyfD7aAWL)Avvi zjL0=+w;zr7H==?lHgP`PJOSXse_GCbvr~o@+4yutYEV)hq>Cg%^8I&zp#JQJ=~~nC)PReY@3*P;kN=q3 z*=^|+YUzPJB(4mVRN5-xqn_R~Y~qIDk6_1~ZdP`yT8_@cGlxul4oit}RK+_TlF)=Z zAG>f$z7Z(UXlVO*C|)4IB6ltS8g3jnG-a(E931e{c)ikne<|Z-A$;gJbdj;nDo2%e zEaVR+D}!R%R9IQjk>2E?U>3X)ub&R=*@=rHM||TVe3qBIRzj$l!%YO}g3mlTM3~c- zs2t2uHNJ|Va)JJ6SVM7S5k=5UXARCVGh%)NV`(Un5c8Fe2e4+ZoCmH z)qJBJev-N`Rmya3`=`OH%uZq8$d{y(>wxUw=;>;vt zqwB3V1ILW`lbI8YH%q?PsrzoL|3SqolfI+L@1n8cfPsg-!iU~3Dk?JO$b3L*`^u`_ zxia=?M^j!pjmt}@-)s}MBtZ8@r4U4nUAc%l4d^bpDT7M%F@C5DvX#~Z*N#Sc30C@w ztvq!fjW0{)_1xa)E!2vQ>ReYL@@^0Vn~9zQkg>mG4`Fy@?p;K|oKf^Ho-lgu1Mh=h zDzW&GbV}(#AndUT7;qZ%JhJlHSE4^7sB}2uMtmYr^gg-a4M%HTX9!`hx)nNOEjg+( zgl}Y9kElH7NZ6b)9pp{wG-rn8Mw-I}5&2$k_4I^$%nhOcF&uQ1EWN|Y?j^KZo3UD+ zS(W!XGCD4ud7tu!tmBvavhDE?dQUlGemDLa_p{<`bq^XfwQ zk?ycmW4)1kUU7X4!CA%9zMUuc>K;GuW9Wq;=H4$V3Elp^xcsw#k}o7PUVk+&s)IMW zFa3Nw3pfOEEgCH%k)y`Dh*`MPm<3wFIYYl!zsFde^uN59@3&t&0=d zs!Sm;p*R_PS@n+vc0?EF#FsSx8fR<=@({SD=IB1bd0BK>5uAtioqpt9tyoh-Ng;+B z&aVpAGn~rolQFx$+wH~`Hyn1gbyEUfcs3f1PIn@f_^knC*?m|n3-j^{B2m48e8Y&; z^w$VHxd;Aw{_(~Zcj^173Q;jW9f?!C;}OF%?B8Q*LspXPZ~gWPHHC#nIF$gOpp75* z->Lv@8PXs;tf}-fr77xt|Na#S#y5>ULeFwMIEN=;APgZ#LAAF>JCbaO-mCC&>7Uuq z4k8NQ^q&8?JL7%k02ttzN##-hjYqN&lVRzjhgH@4g=N9jF*e*(Gbpi3S$(VQz%4bzoz>`uVfj1J#7hg3{W z3``VK#C!G{p7eQ1#~%O_#ytutiv2R*CC@%yyqst>f9iSey4bWCe>vGSPa&;-BPi$m z883p1&F(KqzRA!7@_?CApERPTZB@1VoarI+xcyPK?{7reE8LL5nWbRGm$cuef?dj3 z1uCk?`|30ID^rcX$HSP;R_Oo6l_32ULN8Xm0fAT0)nM+G-M*B{4L~Y)sK>^TyNI&K z<8?rJcv8{PNNJ*)ika!*NTj2~LP_@ULayhazan>v%58kXzc+4Ab8WaBecnWx198YW ze1ns){Gvnhg7^5Z_kpAmN6pFTTO~$y1*HVxKP2abKHZZ5{Y{F!|Kx_(anH`C=@W31fYdWzr@q3-PQ=)Yj37rvSAJ0r<05_qu3! z)w<39ah0b!D5aSk;{5bUL5_ta&~Q-vFf+^Ut#|WUrChjs>~U#IHe5^X+eDdeQfZYM zFP8;$fQ^c~6ps*~{nLw;k^t$85JA?{8!%vO^L%$E3LQskMPwHBlAP*;yF1YAGSvX^ zotBPHEE8uit_Z}oEP&%fI?A;C{>0Bj-Oyg+6KUD{!R7F6#9d&Ldn{_Bxp4!!l>i&k zA~W_?_?>s3M1>h*b|MtA_OgZebydCm4MIYSeA0pj1^WD1)0LK}e z9gY_WD@-nr#*UJ$s1|C?+tErbE>{i~NQ8>Np`Jx`?q$FXxVgDAy*6)v5RVjOg1U^5 z9H-{8={D1$8sFatkuziS@*WQSX3ov9(L35gLAnpNNdoWOLUO)2?jH4$MQ-2r`a9S7 zlH>-o6`rz}mrIQjS6M#KDAU3N@in1Cr#<$1$iHGGvQ90bxOLfY$VyfHDWyDDxy zifMwJ#eepj97=?0^wPRKNOT{L0o=VBZ=zmoGr^C6`jsWWN$c;b)#8G>$Nc+S3kid` zG_2arSpEKMC|L?LTbHgx~B6L>1_d-kX z2F*k_^oy1=vMxl|AL&19kX@p>;H_J{tzZ|bL-yPUzrlH=Ec4Gm#rKcOqxT;k8y)}1 z)34@y|CUk_MAY~d@+mm3JmSQIR@0pMIZ1_!QC_6r`o!WL8!Lp8@*@1UqNvAt{Rf@z!pGE&JuiFY(Vs~YHh>BH^D|p(>)e(4 zQBDsEkiFJ;O}hI*vdbp#ql3b|vbzgF@t(6@hufEW~HzGkP$0V(C?XB6M> zD!reb2uqctjW86$`cBdD6N3ua-3G=J+$J;w$9bL2fq-rcaQsq53~cRr!E4~?`#7LT zEsLv8N3~X_-PX`j2RXw284gBbuy1D{VAZ?{DAOz8`q<+`1EX4=LMcccj1T9+Hs!l? z)M>~|-^HK1U&ruL-dX1R=DJF>MihE?VNp)aJ=OE{7rnCYE};t^NE{Anw1j}{{i*Mc z@7Xp^?F`T zFa&)qWugqh=V#yCFee)SEnKWq&&NodfCpK12{vC{{O)U-7+0)ulY5TiL$@t+Uo@F6 z91Ab{y;JwfGS=v4;D@I4cYq0yePg)E&!~*%&K-O){xuvAp*gW!7qaql?8_ewo$WOC zj32%;EDWsZmY!Sd)iAt_3)Sh1J=e~VYF8*1r_=|R#V7M|eHWEA4u9S)w=FwPn3Q$K zgQQe$k&ui($C5)^8Im~EBf z$Pj|g$$r}5Z?qt#suyc&TT`j&HJdg4IzGv1^1;?mpOv<2@FRioXov{^QNrK}9%82GS; z#$=L&*Conny`xsip<*ue(U+F&w6_7d{Y+!%VE+<4Lb~Pu0t}E24uR9bE}Ij7h>)t~ zw*sI+z3jm{Ts=|urSf1epPxyr%n72625Ug1I|dqB|7nL&?Jl}1KfQBP%D|rhYQQw$ zeDc@Q_X8W}u0ZW-t>Fiy6JaFKpJ*b7q%wYj?iibcde-K6+1YgU^~E;^3#>-jGkh%f zTUshKlQhB>@7BW0?gK{%zKKjUjJtZ#8Qc#dkp*h4>kJ}!(!C`pg(Dk1J3p+o=ZK6M zRs#tFOL9iuG(fz5jF6OI2EstC37}lV&2}}Vl5oejCqqCQynt{2l|b?kSK;CtbB$%% zhcU{d`<}F=ebRwIHt55d`sn6Mhe{r{+|?HpdGCVaaps$(5Y(Ub*S21_1as$=;NuQ2 zN3D@>31_9)m9K2&UAP+l+{phTI4b)w-3@R(vd<|%^Ead^;ukGr=R1F|Ei5dMpT%eS zP3i-26aaeshKh7Vz_gpx9FQS0NLtv)4$V&d;**1Bz&f=EzKk}N+HfQN??TIz-SE;m z9@0;RLP`DrnUWiC`ZvPzH4~R#xKE*4<9)gJDy%%5olBE2WL-ooVi z4p2pljk;R_X9Gq(cU!=r-_|DG#Fw(Qw(+j)l8c%WBt*wd!g4=)d5)Kx(f6g!u;jw! zyt;ok3ow&75@i*o4rHy9olWVy2JcO&2ZgdgJ|Y06Txk;(Ky(3l>Mb9Vo13ewBvaGC z>_k+7YPZ9SY?zB!%;f;I42X7J#yYKNXdVE4=0t9cSdrrw8Ip@rzia8)%r>F;G&z78 zAznhkQ?^3bVok%Ms+O|`(ieWu-NQ+el%U*Qviup;_lk@LaRJszj1c?nZ`Kxv4&tHr zVrV!*_f3`#deCFHZ-P=}Vn8;TPYziQr0C#1Jkv8X-sy}qu?uyppjOG^5;g6t}i}L=}Jv%%5 z2_$vnv4YGa;8ewhLM@4&K4H83lx#F1m^cOUG4XQz$<@_0nbg*mkzt^%t?vBKt_}RY z>4qKR6IAVw;Y7VWAbSHYX(WN53x*ndlBZXx-oFKMkfSO!~_u&MXt@)`->oj0o z%D%+!(UA7s*kZYcKm}zj*3^;Qo^IVuaZ2(i!5~6mHQ#73PS9pT&!dTvLG!&NJ_}I> zHhZi(FuLtu3SR~JDvej%YbPRC2ajk1m7jC+dW+>~ zt2ii4>9!zI3V88B1==cz9+slym;xj)50)L`|D478d=Btrw!>r=8Z*ooe+7__g$9Jt zfu>i>3P_R5_$D(v$TwVr;L4RFii?g85^7^x;qjxYw!ko!&ue71!Y;Q*&)RW^AofEQ zAlb%b*Pled*a8F4W1x5odkhyhhH)(bhOV48v4ylw2)1O}dzcXLDjMO{s0TeH(@{I4ueBp>!kL*r%yDe8W#MHp|o z;H1t3@>XTqLt6gPnVIBOo|4>o-~%Wcaw^F9an!@&(fzW&HTla^b*nR3OV6!7JT#|# zVTC3AG7D7MFC8hWRR;9rDO;r7tLl?h|1gf#LWk4|=FBthC{S3`FA&JykPA~*8E^^D z&#Y;43W=$}Uz-Sp?X0c46%{#e1&!s)sN*GV6b`gCphL?Dc&lE?^9cz*V%rVK?2EB0 zZezwK$N4`$snGajC6F!F!?+3LIG`|je5e9UBNp`nNF_6wn4TFS{q=%!;pTXCHvZwi zU36deqU|Bifl<)UO95<3sIbG!8-t?f@14goFf2-=NZ1rcy%~ z+J+cOVOQZY_SGlnQpJ^s`F;^&A{F^ByC)zYF-8SH-kQdC%EnLrP(&|5QJ9h)JK7JK zTeq`F(L+#I*JB<3ve;Sa$V%mCuCQUT4k%@4uxF;BCkJycG4OhY*L<)C?7VqtevL?&BAfo*GESG%(=U^w{vf~Jnk|jd^&W){<${~##Iind( zINr4}I~_i>jHwvG@^O@L&J)wd<{1qZmK<{AK2TNv$6D@V1;3#hh7@DyQORfJHLnN6 zahj1@rAiuW(Rh@)dyK`XLApA9%NvnlifjRPm&3JEXI1$2i3DVDpo8@Ccb+hb1}RiA z?NI4LJj1tB4eArX$orv~i>&Z>PtzT+8idK{G-L5#iJzUVz<2qiB9YkIU?_PeTk{X# zASNsNS}kPcD@x6JDNTI%1c z;$4{Bzev{B(*sh2oe52gA0b-A(??_{$AzqiL&i@&s| zK5^71``&*Zm$2kFB48@>GYny+rs&whgE7CE0FL~*ZROowYQ<66LNcR*$>6kB6=^QV z0kunzc(n3oi(uQFt1I5}3as7te)DIjgy6*o13{ZVKn4q@c2p+W_7NFnmIsGd=` zLwNpHbt>ALAd%UBCN0^q!YY8-@G>UcuOk6T5v~Z26%Z=Gam29~)+dQdeM_yiQf#GP z96(_N0P{c|bY2jeWPx(E?fPQ8FM8K_~H>P@ai(Tc~Pg8tbtOspg*Tw!B4eI%`va$j_ zSo^&5kQ!XXz9_Wf6{0}9vejg-6*#}qmuNjhe<`TntX6No5QjfJ8x~zyGd!m}{?D67 z&oR&xVAkMxc3(Q~xZ87*Jq83C0C+(dr3S(95ck5&E!1y;Us{uf1~AK`deUSPN|r!w zFk?!`M5p=*0r}2^FB;^xR<_#JCh5=4;)uXggL*r_IIhMn{B^=DTOiv8`$`Rc@wX7=oVfYAhI*D=}qFqiu@-xu@LHv`g-pd zb}IM+NB;ie*f+k$1)e34r-ntNIn6jX#IwYaRoSS{dtCOuIOdp|=-bEZlSor|vlLWjI@PiD+v zrdZqBJ-;6|&7}v0hW%}3MIH&7Y_Ml*3^~a_kxg3-h*xuY+Nak&p@G1+0NkNB%Dq>U z|2iR3TrlG2ue{@d@qs|;uupndAAoO{<;)@A(Lkr|tzr|0y$aJ^=9gD6%H54aY{h0} zwr1F^hw6vT=E|^fIw?6$Q-MH;qLfSE2>fpHa&6c(WkTY` zOzQ+C_sS?nNU{JnZk_ESs%179*iNQzWDBtCm`+ex`_ zSSy9(Zg2*LE&cW^2O>|vJ-lN<G(5gux6;WrZ0=5i**c71J<*i zjxJTmJQl(M^>Z75CwI*8P-Krsa0P{g#x%_GA2xG*t9L#7?;*6QPaWu&r2@LFWE90T zDIe=3gYl|fY0}#ucd*n4G$~TJre<6%LERuVMob~ol%X5{cUhx01To=l%**44!`_g&7PlcJte_&O2U3nUe!oGeSpC zIju!@`pzYHo089h#rO^l5KQ6^^9jmpT@KSAe@csEngpl`Kq5e3`Hm|sscSsy7czG4 zF0kE16A((#)ESTE7WiPJ+fd_`3Tm?tv-mzLfI#u3b#=CFy)yRIB!%az$zrNHN z1geqQ!YWXWnjg1jzN_%Q`GUp`j*l_*RpD5C^n7Px7MR@{B7FqlZ6vmrnUOa<*Rw|k z0{$3p22;k~iuhUP_;K{fxoX|^J_oS8$Hs854dW6UsNaDI*-u_nf=tl7zaLl<2eZ;X z)qLZK#?jTFRMiqqBM?3*9BS+vSk^Rv}M%k1-GZUG|c5Fhjv*)puY%(*>Ilue7Ki}*6{pq@#*Xw@W z<2fIX$8FU`EC_om^v^broIjFSCx2+>Q~R{m6^TpiYpuFkfsL{Avqx}rBxt)I{_h)D zIwz9mxbs$&pYl}BHBVt{9iwCEEAla^x;0#I8(miC4Oz`P9}%6zaP~J31{YZ%kj?yV zKj}IKGAD$4_^;~Q8~1SAL8GO03qJOb3Fy;G1vaSLPE$s&miz6+lc;?TQ2`KL`Pkr} zH}s&tkiYb&bPkbL?J9<^VkK-E*&V-{zvkqA!K`(5CoL}3Tg~iZQFSRG9pNFOlTp## z>+cy~$Vv#wp*?zN^RRv})Uqx&Q#g`x1*KnSUCJ-BrXT!3Yp_M9_@*7$r>q%XyxSti z40*rlOCan~U;L)a~?J5rJSmUvEzQ-a?-}|1` zmg{1EW^RrG$!du|id+lybNj%U$9YJ5Ejo^t_NR!V2l`(+H|bsVTWQ1gC)YH{;5u^W zPW!dO$|kK#`h9}hpgF6cT(hc#{IyoANz2W{6nry+;r{%|Nf92yA*~SK=cn#o2$*Z! zVyu@BU|w)ZdVclqdzza;Xegv`1O-#7+xGVzn^3IvswcKcGDygvNtQ?WX;htGb z4FASO_LsFFvO&k7+|2R9&KCl?Z;O`vnd!M5$316pa zg6+f=(cChMg5QM2loIq3E(8p9ISlz z{4bjzQr%!TAAyr^{c@p$WYHW7&^$RgVdbZbvL}GlgVV_}IEyM3=4T+yW67{d1sn)l zV~zAdXqCnCj?;2gw-o@v@ESNtOO9a}pH>|dWRR#?`rKol+tuTGs!s0UpNM@Zv@aNt zK?S=6IkCZQ)vCbXA)>fQZi_urTH>EDCT^Bk65CYW;X{b@RnSI=-o(1NkU|tev=nXq z`N?#SE%RE&+P-7Cp8#?htxeebJT1k_e2gR@Y`y>;lP`At;mfkbwprH0cnCy9AQ{&= z+YY~z*T`hg0UNcnd9Eirxn@oKbfL2TmhelYgOBz)go6sA1l(=+VX=xpD58Ki;~&T+ z9FtV^cc`Zwc1)_{Z^MS((^~!>1;{FbHWcmyu;22^UEB0oP7d3TF|OdJ?ORx z)#EeD`K5PjX8Sue9fevCIYL(2V+;ME)z@vR%(M<3n1=v%s4$XIO`eg`rgX}myJ7}- zUZxi}RbQIvOjGcSmBsX`*?y_mL#I=__XvD3VR9R6=7z}A9TfroN><#ro8PrC3E9ai z0mpp#EDk(k#>&Du%L?e@DkF8DcAzYP*=v|aqnmy}93eKqNj&>8F;RbCx3X%-vA^H< zb7Y8`qs$F;uAWHpipzJ4HwfFQ$)<9@Q>Y>?fLc)?+_vIwQFn=D`wpM~MG!u`i_9AIak;n;9QN0x7=K zQifLmY>z@0C}DxtItjxSZk`*$rc=hPc^`9Uv3Vhf@h{gnKZe$pcGYmAFn`0T#6^J`JQVYO>gvYdtb@wFV2vKhyK*=-6fc zs)+Vd;|`4D6ljDUA+gtp)Z{;ane!PH%jf9QR*Tpjt6y-iw~c}s!+T+Bnpec@xVei! zNVkd8aAokOS>u7;UU;#3DE786M4iw3-N(GH5Vclc*$RWJ*g3~HQ!H}45_|qlb8imO z8ff#WiuI;^+L=~SQU8l8+JerJP|veU9=j& zzu7&sez>7loEV!3@5tj#f^QtX90vi^n|EMqQQ@;$ozuA@VE?L1VG8`48B5Ff}?KV3|tC!BxPc6fY}8U@^`dw?UQ{*NPT z)LUaNq>uLD7dN?UUWDV+Q$O|PA-hq9s#TVck%V=QA${7#39&S=+bx@U@9(!0aJP6r z3GnxeB9c(|A+}VOr6Q_l|hDMVw=IHdO`;5~M3pmC(Y!wC3 zyKo-6>``nBbl41Fsb8_NG2fbD-(_~HU$&_FVRTNvpLYRhoJKOnxerY1&H zUW{Hc8vWLuZqB=ZN*3r_DxfZF3Zd2G(-yI{d5PNYOsl2YOJCt$o#;Q*e?0&Kp-rNxa$Xg=2)DQ{p3AHQS zRt$A|yc@1qJU2oWsz5cI1L}Ga70Dt9$qQXCg)Wq&Jl(8Gi%;#2=!2IXwObq!67;qY z_V2h3QcOi#%5jmIjMUi`{#Q=~gaQfL_GYFt4GT9~GPF7_I2norwx(wnSz)#(%teLlkn*%g2#2+Yp8 zUDQw>fqXWQ2t4E|c!GUDxdY0hYCQDw*N37-w{EmQZ343v@yRwxDM>#uRsY*KoC{jt z4@>CKuh|~^a}}5tNT3q_#|N`j_+rO}e!)+TNOGt6)F>L|pweC+gJgRNME3hd+ymgY zgGX-Lnjty_9;g7`f;1=8s%A|;LFpl$%DOz2??Gq6MjHH{GIx!q!SHyr zFR))WrZxgJn1bpA72~<}W22}>Ry;1}8eBW$;{l!lR<%Z${ycM;=-X5Mq&x|}AoOuV z(up_#)arvXpRPpV!CHwDRf`}tIp_T1=xEBMn@RptxvIQ?WAc3nH`BOK;$q5o@;BN- zhGTvu3`%9t=f$-0B0rr8mU#V~QeiVseDOmP1Vj4R5qirQZSd(Z{{E9siLrqtGsW}> z5gm>S?;jUFJV1<*ZjQ+K&eu_LHLy%NEYYlDo&19nI7q!M9-t+VJ#=A%F9q)adO}TH z2!Y5|Z6|kQgYPDJx&E1s@4};k%G}xbSXiQ4k)t^yf&IyZQ}jv1&{&Q0x5p+uYyWFT zf?*7r+{|@&7rmfMWWrQDFv;xPHK=^h`cqt6IKXxg`o_Jhros*dClD!70;Qsd;`i^n zsW1L4FE7uYTCBTwjupG8tHY2kQ^hhLwo{%<1eAjfT?6uq*uaW2`S^qDujctmylj0S z9-x5qavZlho3m9|Av$QWbWCExMDfvBHSfYtovmMOrdXBK-z2*ZQ%^6FnM+L4BVOKu ze0eNh@)*Bi#X*Okib|aA)(%IXwxen7)o*35M06NnX`CdcLtpiXf4z({ej)ev1h3-#L%F;_Pdh} zTcHP#muKcvM}IR;7xV7krjP<10T=koKnF6ibVoFvV39)!0=QtwX7%Z}FI;~?wjZx? zhPb#4*Vg_fHc!mQ9P*p*?4lR3sLKOU+#CKUkuR1&0%uB6ep{WKhxeEA4P`f`dnVaG08k;UHyC@Jr`Ms?2x643B~3qw(f= z;ymq^O~Oz#xEk?kIZsse{l(b+e;dPAlE&%d6$ub_m(y^bePSUs<9Q-wZIKLdPL@?M z(B;`AhQQI_IsL%M^Kb3#WheA`R4v~Rd#W#}+Jj3hZy7&N6lLKyJg<)gs!u7=z3X}I>Dg+uc5z)W@w zub}3?eG9CWq$5a=!d+(L4qYNrU-LYzxUGihQ0%xt$?!bw8gUMFDwd`cW^ zW^cKFNXO7jS+ACzf@VGVpHuk8v9P~+_HC^)PtfK5O~xq**RDOPMXIN`hTH18cR_8v zZO#~d72DM5XVVFth2}hJ4o?<3$WQ(NJi66}4-n1n*cz-%Pm$SMkf$xX{pSNlA6o?V zbhXpf2l(pf6I#H!`JYz}M%5lP91IR+I*m zKQRK}eiftUXsBLCaRh_XLIK*EhsWac(~h7KSUcqoIXhQz{9`=F@jsR=OBg$ub7Ckh z?W07rQ3N%pLkYZdlZ#H?d2ZFsaR@(X-tTJUorgE^z$eG1+Y9J6)}z~5L-Pp zAJ@CT%3PptN&(kq2cPmi8gucjRyaIa!}xyRrLUs_riUF6u>-&4hCZ$PLd9n%qT}g# zT381rk-xM{IKnQYkgm>S$Tl)wH!7V(tt)o$^s(PtY4}{9L4+W>+?TkAv83idwCKB$ zB0z=#*o5rK#baCFv8El#gZk6GQOvo^>!a0zDE{}i|?;@Eb9T8{*wc?lN=XLWr70iVmG zae9i9)*Tx3DV|Wi zaZy|GdMJxfr$DwA#M>aDgf+6c2Z*pcieKpkeaG>p3+o2VLUxMrLW9=%!e7raMBCfH z@0+oh>Z&7&0O4}~Ra~sV_Fe$mwbXx}kx(0mVb|nZ2?e!G&eez_oSQ z6tS9`;HgFsxA7fkvvCq%cpJ%;4DiW^(>74ae4+wQ8fw_w6fw!1OqdeFR9((IA!L#}s2_uyI~G_lFC zbR%2-*eN*FIFahMva?`S$9=s+uB>+fC_+JKU4 z4WHO$GBPb=s6w*KlK{c!**#y5dJKIG#!N#IuqvM5M%pgTt=3PnK?B>@@ONA-|20a= z%u|u;)86_DpI-mz)o<|Wn>`ydd`I0wj{jt#eOs%p`{i95y3X~eL0UUy!Biv_uQ5-@ zZykV`9hvAoz`qq!?CIW+3t_aHP(4{;ug3(jVGV70bp&TJ^3AbFY{^bg2-0%sH;Qu& zFOq_oG?gB@qao-_bqflxlz|jgGvea68{Qh%l2bP^v1KHDv4)Ff%y!(8DU_p|o-6n~ zRZ|m3Y=7SJlpEt-UG?AlnB?`TDR`muAp^mJ$nPiZD%UhXzcPN%m;iEr2s8s5E}u** zzkA?uW6(FfPGY|1dUJ)oGSIgzM$zp@UNT8n2c)|?+v#;Yt^!7RKIywzc5U^@*X{VeVoz%qbnP2iSHe&dQ zA;S!9Ay0?&G^eQ8y`dvyEtwU{%0NHb9UtEL?+sc@MT-avvs46ZIi~o1@OMi8WBcbZ z5am7n2j;^TR!ao@bnL6zntE?xy#x+6iP>nqi4OSOC!~+<(bm_w6}rhy&*u4FIw^`I(wqbHVbwp^Lqi^?OoM#)ZAb;M0=K%nlIE15!CB$kfkVeYQb^pSM zl~j&A9uL{mMRvX1mW=cpXmI_rwvDR{?A6bms7s)l?=y$jSS+^sbrQG%a1?uIaczAq zNGww;=<2LX%FUB8RQcdy_*gE=_@16#7!53>6+6O5fX6Fy6#LAhrA@icB%*407@Z@QJm2%4YfApXH|-UhX>C#HPV&>sBD{zp7MI zKlm7U@kFBf@fBXO(*HgBQ>wRqoNImuiM_p{S}f7H0xsH#7V=FTa9+bobUA3zEO@pi z;JwzEpKe+8%;$5=5xCs~Gj)X%K8I>qucOQJ0Og+qkk*XkC*K8Fh1O0IlY+aX%_fAd z`3v+1k^pDa_|my^YY|t*y?nfviJbl#U%Gq|=s3B+)3OHJ`{|AdC%73rx%_pZ;$RpBR{mOd}zH!FS=4hw(n zLgsRA;0d@T84z2*@_VB^eYPoq)rsE^kUM=1x|&@*62y=fNQV&M?4RF3I!s0)N=l!q z_X$0T2WT4CfsWfyse!A|SY{?VmI=b$qS&TcS~yaS`QYR*Y1gWfW3h# zN2mOn5`N7`cg~3#vaWf1Qk|XIggHTniJCPVHV!$N`=9-`m5K0~#Ue#T5M=f&hepb2 ztu4Daw7!NQvzuuR&d@#9f5;v{tL-QB1^ybXqpGS70$ZhZP~P9+bBg}2-?!6qP-WD8 zJ&OF>oQR1wZ}n1W{bHfhWZ-m>{-;%^QdngF%5dtz>JAWDBKK*oV-)v0Kb)LC z--oS}jWDMyo%`fazGQl6AUsiiTwI5=*;x>m9**utfL$9|%Nivemt{$@W%)pG1nI!% zASUJ#j>SG%mqkd|)tnxC4xPB_1YRBGfxJO!{Pp!t4M642l1e$zE}<8iKZsvP}2~0@P#_}eO~#kh2HXAbCA32e4p}N zBq$gitrss>YfHID_2Ba)^$Ovnxa}Sq@&w_b&`@y9jd%j--q_pA!(yF$uQCA>^IHzf zhNHJl!5y08iY~XhvHeSHqs^4iXZPf|M=eJyJToX+UAM@BN1r9KvBd10Of0z=N__}X zJ;$IWm8XXWMG{%mOFkl*%9D1yUL1ZxvhvAa0d=f5KR}&kMvBD*iF`<-8Wbw^`u#;7 zj&&j-!rPV$(|BS?F?HcCEF>Ujn}kf5uyEmGx62=mKxP&8Uy=LQz6{ipdRaW@w0)Y+ z*3#)O8{jwiG*ynj^CziG}S^97SeLnkl*R&3_^xt&&`axk1w}w$kbK1hE-heWh*qL z3EPm28*GIA2Mh>DIx)@YiufY9dPIPq49#c%ce?uZ5Y+aIow>xX@9s(m z0i$qg%J@>N0ENz8;=N#hWA<(}>MA9zOTl8X!7FT-)VKScVflJOO6MtTXSm+2#S(_~ z%m{i_`6h4L$ND4ECUgw6-*<|0YYbC$j|h3Cd0(GNeuu?bckGVsxwO{Roj#)zsOcy+ z>1x-1n3D;dT1KRei}a_-Qtr5;+1<pg34h>G`9tK>+?#&k z&!;We^Q||_9mFnAir%ZS^@9r^x|iy&tKFsRm{wNB_{maFH|H!qG8T;{5Wz3BKyKF{ zi9D;S&oS!$Nw1vuM7d{qX8mTwimDD@(4Nm!Q`zGA9$Kw9vy?>35Oc9*u^5&)_i4(^ z*AXp_%bQwgJjCDhAkH^74F|$YaHM^T{*Qlc?(x> z%zAeQRk4bA-@QWY=Tse;|K}*fuD#ZBe`#qc6{|TAI-fF;ya=+0HSW(}Kutt4If+if zwq2H3(@Z7rO-N;O1@*Y}Ub8DYqND|ZzPvEC_v&T8cKaWM)dF5-w9aj9Gg^E zh21o{ac6IC-0ryY`ND?0x%|2o!j&89WU9sbW zyg(HQjk1^vx~WQ>=RL}bhK}nF zOky+@Vu^m4go2o2wxH^bcfiJZGO>!kA41AeT{UQGOHuAT?AXBI63eYo=yP4HSJUSd+=B)Y^H<_5K^jj(q9EsG{xq-*K zVhE4~gr*e|er0WKxY#`Im)QdFxn_D_x|?EpciAXDZgZoam${*WGJ9spkeSoxe05Jh zeSwpyRntrP++%Dmp5r#Quzep$V`ROI7v`&XFs)~dBLTyRt~+^bGcJYwI~Cm}{D7qH zvLpKRg1WI@fmM`i0Q+&(1$%kc@#Du2 zl&rgqkL zx6mZi_*2XXnn!a04<=acr%G+`kj}X+tI(NHtg}TPbXKK-p*2cP=11; zrrG?Ofso9_cJ*M!c(P(Xkid~t?~Wr1CR7KAT0M#Ysn=9-#{J zkY`O*X+f(x-GA$vH+!5UPpq?9Ja;;xi8VYPmOWor&P=79b#R)q$?EW=9B^n)RXh{l z^Ua{oc?66L%NoANa87UuWVU7g34@^OPhgGOz5GM}hRa9SWRxbfHP3kdc_`W=+|Brf z)hP|spgGiHK9{u>>&szmYaLIo?7tSklmr}?Z zJTf#+7XWF&4t-7#{)MX7))USQytiN`=KOzLB>`i*G2Z=<8G{Waib49%^EA6{qTn9~ngzE4Pp z74XaI1~!yzi=rD`>n`k7`VXmxM}|JB&rD%BO(L?>oV%REg2BBHsX zZ@P~fhStVEj34~tP<_)=peo z-)RDhoLeED=OwdGtl|3S;^KlC?$SY!hgaovmZ^>@)f5(t#-)z6vWC8uH)$)a( zuZ6uBfo<~`sW8%NuwNv!1BFPpdXRK-F_E%}E`G&j0U?kFOqYg&*==}L4tjRlJo4h0 z*hG@z#Jj$Vu$+4nuF$Ym(Rkh0Njf9uJHoK;#nkJ)Ke--DhRGLjsuH{qb`lq~&3l{9 z`b$OZ$)~#0t;{z0;~S$^gpJu=D-kAH!vyOOj{z9o&kAufAnv~L#vqB?6r5+9C4kNK z@4qc!R?7g-&$KF_FnR@unxwUbM}r~C1L0VD6nWR>yyv1{;R0l1l@0g6KyI0+ZM*dc zHJpG4qFKZzzw!^3GLAk;R?4xu*LAdYbFxkr10Om*IuZ&7>K=&2)@4D1vWM3_ceiSL zw8)3sL7goT(9jHL%98f_x}xZRA7gzm7I&m3PP0f8C2pE>b zNYJ*I@S_^C*w`!Jr6$@#Hx&uEtxTD^7ym1-IhN2&*(!*lGy$FS-wn`)>zt4ZdIur% zR>~n_e_CtyokL!6Vch#Nb9i;H`F{I_akXCL7Fhz_k$v{k^fg_uPEv^mYtq6*s`7(; zH6rD=3E8FXTEZU%1~+S*|J%ase6*E6lEoBc!sstA+>EtR)?$9o7qjifn+Xm+u#>Id zuKoFfU0#t7+?Y13S<4zk7CUUa`2efA;M_^c2WTFjAgYw0Tdx4{77ltSwFv$#9(e+# zthWbh-YXG&#hJt4Zmb;Z+M7X+Ku3=)z~n0H!&~xvRkw>m_4FmN1pJ_}A|;RHIr2D8 zfRIhU1oNMx8T_J^-}WoB`-z(3%n41Shfrk$c%~F88eXrDjZ<3ixp^`0<<4 zEUI&AKF!3kV1ntgXzE#h#I4ub5Vz*4iD}+d*`FO;O|g#xi5v{J{!Y!#gUxlC3Q?s1 zLCEvk#Nm5E(875Q(H8F2)zw5aVPRhh5sFxtT$`=)4nyJ7kB15Hsz&C(S$xFzyx-2j zV`2mi0LN6c%@Zn2EGY zW?I@Z`K{rcrrk!3-8LL{G@?|GKV+mNx6|hy3bc!+L`@rO*dcF*ieo{$>EJ^vvm}4I zbM$BU7U8v)_KDNRvRyP*_h@JZRYZ#v2L}c+lnx zc2j_8VKhLJ_o}Z497+T{_uo79kFl7QkD3Q@`xN?lRsi;FIA(eCR;)RS6Ae5(f00fI zFtmP}3x`>!q=Azan@dvBZHwcp2yI9W1+AL=;|ZM*xBVpr@5{g4WU+xn(|RMFVb@f$ zhMCHu3C@;Hd(t8s7tPi+pFT};g4Sq#oMQ14hkzRv58*BeedDX2 z482>L43nHxcS1noExXNpqb#}?wMEt-gJ2o&eoybbEqI5oTz}1;uY7Wpgq?+s({F`3 zJN9+9pZI}gy%f1s4CVM_o2m3w6|)`-I;z+#cyYowkc7_XhYsJi24Aj?Vj6Is#&{gq z3ASI(mSa9`&ow-U8`)C1NSs57v=AKJp-fisP?tD{rtJ}utCy4Vb z(C2VzIeX-{McI^Xq$+=Imu6acb-`=(BQe5*s@psVil5x2%;v{sB$mPs*=GZ<3l#}& z>UK_yh*2g*1Jwus)%&As|Ek)u+_dL2IhnHtW%uF=xW?^jri^cL$n8KW?Kq+b1Da;X zkr7G5ayLENGMi`Z9NLbJ+mvakmI}d)!rePlvB-XAMDyPai1TA3L1P=SiV&56%kF>y zdB<-G9+!Uqmcumk+Vxp(<|2SY=eim2?i~Nv;@O)d(%!X_6s0{RR^1@)xJ6A(Jwf7f zkCet)zE*EW_P9vP^jZY89yzPXfGXakl$dj_F%%lzBpC-vwa&vJ&$;7wN0QN9j;~za zV@9nsb6{4@kx1X2P{#^p+zfK;?&Ce@hui)nc7!ul-R4cHmh|qN&Uda9-atAAKHC1} z4zv`8zJo8iNy5AWV^1#~$6_z`MCY@l+y%BDm6E$;==idGPwF_qjMrWMy#RoJ*1?}Y z?>=MhVVB;W&Arc?JJIZRIS<;Bqxl*ssp&-DK_ma>9Uw@H*ZL_Jc=YG?wEPap?_e3t z1d@NTr)}n}KI-05%;77Kar5*<-wYl0R;{Ump1sOH$^Q49PyL+^ zk^0U%@u>7LFa^_ng&qo5FfLx?4s9` zBFyyv?JO$3YWXN{SF!+!ZCUB70UH|T0(#d8dguf6^h(qt4JUS@0n$=-;4gh40B7WV zPY*ll_BjC9o(tr!WS)?BKbKQx19t4q1%>r)E)6*vjwj3@$ODsyuiL_7I#oJ$4~_ed zEs5m)KSoCZ1KQieWLv}cYbU|#bEH)$G0q=s1;sFY?>%Ks_Sw<{lF zNdT+m`vh`-KxD17yj?GQ9q_f^zO1W%JdTaOr;uR`@>rVG_jQ`OWf}Co@-WkXpRIDZ9wfx zo0`x9aVI2u)shtEmn-xdS8hWx2RpcpM+SqFW`WY6LeRz^!x75DOoJN^Nva`Be&V`l zcwd?Zshfy2R`zng^WdOgv|q5iS;s|=-ZfAdgh`4H$_z6922{U4bA-h$Tr8ZDL;g89 z#Yd1PgyJuLaX#IhHwOcJW@@DD%;A+Sr<>jo4Dr~VV{9S~4N=3N{0iBEw`oz5{{>wH zEBe8e^pVxo)f9(B*NgkEJC!(DDNE##!{Qp7nvhgQxt!DHa~&Prko_*xfjA`Xe0^$A zbXSFA&LD7l(OXZ~BX3hokUT39cqAXiq#pxJ0Uhx-aVAcgT#nHv(|#w4F4opx$0tc3 z77o^<-LfZ@g$-&q14iP&42d5k)3RP`ilMcDjF!+{RA{4sLhu-;>bsoUI*NZ*kVZUuDE zLqM=~$?SI;z2xT~ezb7fY5+JDm%k%+ir0a2rD<;R1h#4!hzX0DH+#8g2VMmd4>|Lf zt9$*TZ9?VE2kQH~ve9jL#;`ohFx<+3{b*isUsg`(rI7_>#~Ft?1;`P_a3Xd|lL3VS z;4D5Vfj=DSc+>u52grKS^Hr~tf)S2>>$>SC03swZ@Q2Crka-k&g2&zhW*iulNunM` z%D7N5B_Hu^SpaC=(HDNQ@-ONk2#=Vof@)jR@EZ^RjO%!H)YPm2*}-0+15joI0X1SD z{|mJUSVHis=$mkMG8_4sO^3LkT7aL35YC1IC)P4a(cesUl z@o$&|**sH)>mSV$rDDU`gzIF1um@?%3thFz)SOUN7??%S?8?y$3JMzYAwGKHEDccj zDY}wrETGx%W#{O)Qw3q9o`P%QKX?Mj1Yom%{fjEyqJoGqGcf_BRh9Q>FpTdO12eS_onM(!DZ9r9*o;{ODS51T`SanB10?6&aFcZoK;KzcN!)7iW@M zPpK31Spr~@SmNjSkyGp0+UVh!spmYc$7wiyG-28@=BIwJ>WMTM#5W$iqNAk z@phYAF(Do0DTS1P9QY>+Cq5;DrAE+2=v!a6*(fg5yG49C@KIvn7Vu|3TnQfhl%~Ut zEqO?j{AUL-WrTr(+mf~p4o@^QrFeivLVIM(rcf@rjXm}Axx|1E2R2a`xPX0C04>mU zZZh4dqc#9bQP%W3*;xG^GdPkmX)H4>zMg-9(^#bWrj75K9D5#n2B9%9DWtsfi2rTJwfv4D{ihbcT$thXSNqi6v_1{ZE6o zb3KuDh;tU@B5JjKp2SvFwzaZzHE3x}(O98`g&m4A?FX=%e3 zWo9i>_Cvk|54d*?pV&(VmR9^J1_V|e2rK?R<1|pRri8_EfK5&+?J~i4{G6&1^vNg) zAx3ThlGC#qBgg7tu;D$FtWW<9deX$;E`%K~knJL7fuwbG-TXy%FFx?P#DdqC%=(2J zb50!bEl~O7790jRSy|lWVSCg0k=02w_=i<~s16D=<`+lOgW7jIZerI}HDck60s0H>q2 zU=7PduV?shQVYUfQfxAC#wdfmD@yhg9LB!y!om*ug+js<>{|XOuLJbK6-0^#Z^mwK zr2E{xaD-B!@K=Jgq>(w*S^lMO-6xLjg4`8dd#U*jEDZrLArG}nI{s3yqvFw@NYt;;(bGo}aC6%|<`Arb_ArA$LEQ0#7Y1h91ijW!wNmGD|;uHlIgzUd3Cs#Gu&w;^ z3C+ogtE1Rn4Q5(3Vg%h7jt6NJA+J!V9OP1qDl z3~?B=4^K_GpKCGtoQZd2CG_hYh`B%S{2ovy81CKnzP$o=inp3GG177U)`Cf1WPa%m zS^9M{i2rxQF(rRx-Q`!)^8Qnk>Rn8rK+EFx`8O43aKzZbLOX0m-i2#N< zr=)f^%sROAlRY0OguGe=*~7tL3%+0hWScbq3pLq!K;f7{e7L%##9pXAvqAOD^2&Tf zM8p=5*a(^YN|@t+QfC>Uf%;_pZD@EU)Mut*8ef{=x*_Z+?YT<>`8L^=aoT#?k}rM; zNlzIsp?7T+l2-?HV3mqwamqN}AT*oL`sY#FXTSOrv}7B07Ta(aZEPo;4Duf}Gae{J zF~d8I&&sdQv5q_@gRl(Jv4wjf-+V9GCDH+;?dTj)4}tT$sZojM?etvbZP><#|CWL* z9tAl{mI@WhVeh3(n zd-v|;^OVKOc~nq}q}y?ff=>P~5aQEAj(vxG7pQ9^DTJL0d%;TE(7Ox3XScm5Af&H_ z+fF|5csU8y)N?2UQhaUUyR77GV`Jt6e!H3R6f>V9H|9!ml0X3mv-m+fQr*+zO>{+8 zPZkZk;&?_!x&LuVz9c))hVfU99~7dfh>{%I=*q~2kO67FGMD3OSLNmK=st<6jgk^Eh&YFdPk)e!W`vAd(<^5Coy_Qj_3Bz&DA^qY zPWP~;XP1BE*yf%@F;jI{IR4TZ7jUFF+7@yV)xF9ldsANznnNP)CH<1NZj~T$Wc6=- zaOtSEAhc1)>+OCJ4;>&T@ivh%RT^FgW6=iEHz`$K$J9F(a-Jpf((c7Cmt_SK0!fUz z%n=C0^x_1|fL-8_$mVphBGm&{^oNe{-=&!r-C-{#GBIfQSLEG@K5K~W=PpVSAl9-w zJQ~WLo?@j)LX&*;dnj~H-fw%a5Bs(BH(wyNQQ{n^+jJj$J=AF(?C>pl{08IY(6(rS z9MO-n>i+22b}^ryL;+=|)E0IE;=>3q-QLhyU^E-3#D>Sp-bR1?NG6rSMiUaEh86GN z&XGNlpgwq+PHptBdo^w81Qd84QV-SAxy^V7g*7z&qoHg^g9Ox zm8bx7U2tJy2r$7sjt^b4FHDMP2{V!KR4Y-2$0{mCCWLlY0fKa#A(u~GIMK_w@B!0o za9@>w8}g&D(386$3-NHMq<1%3y*BvTgFE)=4MCdIP4qs)@8A*&dnZwb?Jh#sguvP9MRLBXTYp$$x`WWw(=VMaF7`XGr9@xDCpKM}6JNv* z&*X}20L5;f;cM12G0%i z4H%Izt}|9vL8Ved)U*lfF=zBfKH88x{#~Wyc628Hrk4Z4$f|7>_}~r^MFyDVWgm|L zdVah9q*u$o>PTM}Jy0CK+kB|$z300q2&w84AjfQWyaDl~TxYXl&GQt9gGQAf~9B1VBmyo*`o2+PNH6$u5{E=Mh@$}#6ERD8*U7~9% zUM4&B7MGS`#Ofk%hfX*Td-<{{V7}$)Sy`>D$Z?L~Mvu{Uiars?R&@i*OD zAeISpeiy1Lu5C;V*~++p+VooXI<)rus6futC(82O^JMhBK#%I=G`=h^&U$Z0!0fyL zfoo8mYuQ+Icdahp+2yKAQ%T}w2v_TVh-mOt(rZR%1%5pUXOJb0`0>h|n= zcCs{aP*9kgpZAlhpD+7x__D3j!6CBw_)#32*#x={g>~EmSGc#ZjgJYB&bPQliN%M50F=vCo{?wf@8-g$;yVfPBzc%FT(Yr}Xi78SlJTO9wU4BR2!7bBf= z+|iX(g5LGtoWbeIV$K4zU2}d_v-d``omlT((BHZ=mS$}R{r!9i-$j5hoV$=-JkuO( zkBfnj^+>GIa`FSaDU{yy*+5Q^ii^bIPikhf`P)aoF7uN~R4lebz7=R;VcwhlIQv1I z)>#x3Cs2(M`GJw0g%S9=ybvYd_QP$ysnsVVcwXOUHa$B$w6u3yPwk9W?<0*wkO+0@ zip@?gtLMTTd9>ZbUBjHCJhKm8f0Np77);;k6tD#q$hV-pR$EWJamPzBru`m44aj@% z3YIeU_Wj`^Y2_^0R>EE>k192@@%5tl8Yd4l3N-2fXkqe?WQ9@lSMZ;PH7&0jl{&=bz3vycGvGqhsK-==6GkbO`6 z*+~Pj_*2q1T*XZMt`qH8<)~T>T48(Si|&|$*2{cUnELPPltS6QuP;bnAj4fshzJz^ z&qzuxsBQ6+>2kT`43aU&Vp?9kCdj+3*1~@4wP;VNSbKIiQN4)u`X7IpIAQWP9P|^a zB~0wX`HQL;HV2LGt zMtz=W!TI{@etUZ_N5{Q^8GjMdt)eHS6PSq#f~AP43r@ob1#Ro-N$A-Bu0V{G^mJNZ zwKDaJ7z&j+)mG#Wh^SAaLo`x;xywX1qiX7Ru<3|qHK&H&1&{v4nL4cn%<5iJ`l4$( zdw}fG=WD%qP({ZWg>99I>xhP@zxfjp2uC^ zqI{ChYI*!DDjJ0jP%NMC#chWq%SrUI^*Jt&G%aGBG-={sU_p(oOX9XWx(>WsytEf1 zU;iAjjM`?t-tgHVmQ8yoJ#^AzJjgo)59YTbpHP)dbk}{We zm`=2aiOK8k)OW_jXMkdUGRIiTd*|fzm4&e5Zea*=>4!kEO_&q(ZigoPIATxLSz2b5 z(OQ^TL#_uAZnfRjcdD-xxw!ZVN92}T@yEtbgiVl!-0%Is?A+W#x;g6!Rn0I}dwOLv zjDIV@#L`=u`G2&05^d<~tNR=9vgq!H475Q7f?0x&Iloy%x7*Oss`Y%RpzySu$z z)w7k7_vKk;@KsvWu2;Hzp{hU1HLK|F%U@ZJv}v^}V=AHsW9m+A zlWEfP4l&se$}RlXrO0S+S|#~F+af`*^yyG>p+kV&S+7>#zkgoSm#j6=?iV$WVjzql zqL8?W(homy^pAOPm-iZ>X8J2Z;u!%2iTNBC*^tjYa=xvolF#qxrCRVYg=wRktywIi zhlL8l`aRlFjGr$kPm?UJ&-AV9*2xdk_uJLPk^p?5`7TJ0rxnDAYFQv|8@<>Iz+H6l z(6M9M*)HE*ehD~=`88hj9KBnO@jn_-c?IeR?@yK)K6vDClKng^j{6aWE_&R@9#A2U z(mZn1?|vNp=MnT(J?E!u=l7UlPSu~=G_PWJepyEQE$Z_Aj+6=eQCn%G`XWhI|L@^< zYOOr8&sOardMxw)7d!dtc@^!;s#n41GAU41y-->t|INJ3Zujb_=Q=ZiT~O^`F8w4c zM^b?FgwWj&ZiZ|co13%ITd66FMkaJy8fPNMVnEx&k+!0T(w7ypY0w5QUWw3Qy=Amz zs7w1?dU~pAxTk|Fy?@m}O}Wx}buAr1S7SbY=ikBaeib4chzzas0`LWtM?#5+>SS?n zg+nFns{@pdX36x6ar6Sm=p$6j+v<{ECj@*iENpEz0>62Ktc+z!a2j0Hn_+U)A!72& z#U1r{+T|5aHm+{(`Y(-Ig}L9`d*kne0fMB>4PCtdqxge^Gpe?5#u2Ia40Vl;Q_`q6 z)ly65Sa;f_SWJdwQ9vGG%a&>eGHN&%W_eofHVTEiJSzW@zjvK|Xj{1(bZbpVR)!}g z#2V}CABZ}CGMkn}{y&ngI}qys|DS9kiB4pOY(80q%#@LEoINVpduQ)TvS;=<>&&b( zPbp+`X4WNp?>+AK^8NkK`+e`%c+SV;@f80F-rrnl)|pmIX*9@62IY0>Yr`wKfhqR;loaY$zQa-4rd2PmkDCUx9Gq_U$v5Ab-L*VNSL8gM|XuPbf3K-?8lHxKJb2HIfXFt8M3w) zdsSO-!vAw3YF>3ql$whp4ARATKV*mps1uG|qq{g4su!nC} z*7D}cNqTHNb^@4p!l3wr6e&idlQx01$0%!j4f zC6-;qWdcQ$nE&by9@$v_d=v0Hf@LAOCA;zoqa3&fW;Y(1TO4XP zImvyZ*}UI5|LXNzWZBkH8}7<+L96x3wA*;Q!8q>iTx@G&y77s5N*$PmbX%g5%M7z%E5BxuXL)+|=*_E!Dc?xOnQAnUs7r)3f)FH{e0p zYu3@H+mbAvvsd#jxQdIo>5G2Wp!1@e&v`3D@zZO-RL7SU{tiQ z*aypci}3=NG}{}d3Kf0n-9RJMQdd*th!t$v-p8VF1FbEK9cc^OH&-{+#Je)+Sz3Iy zO&U%Ms`e8^DJle-d0fo4$4+~;7ziBpcth;-TU<=ch2*Gh z8ul>l;)J4&J0DnnZcKLzj5PRv>+d(SbNItsVZX@>UwgI78-jn){W=>|tRFwT!*Nj5 zzw{vIiA+6U7}j z8ZOU&u|2;zlm&8u(0MaK93I69)4nUQro@ET+bqY33iM z|7w&|X!&eb6%X|o!%~lgAJy4a4sO@xmz|n=HOg8B#nm?ZIey)5r_uj(u#y+8V>iAG zsQgG4pyg@z+k;bOC5ZhG+{}O~#_Tp+xs#x{%B+fD@}^_MViHD0roW+$e5i5Ae@mKN zuat~?V87Mw1E!=`T}A7c>)C@a`5g6Y^%$ovB`17^x}jYwIoW>2US;|vb}{z{KPc{= zh1JL*qIK5JoQ<5*fYw)~o4nE*s!1~=;WGeb#^+2~fhaNkwiq)?33 z<9BN*FJrW2nv~>fOVT8`pYbf~KD^Dz5e5F90Q^136og8zEf`%@hI5M}w1$MAq#p=% z1xp={Xzg9>50Ixq#q#7HhpR=7kLftSw6J*7PxZZ_P)mV5%MWYWk6BBqEXtYECVFr! zC<-hi+<7tn&e#{9DN4BrA>2=HLG(%PG^lpEob0^u0?FLsaMo2#_LC#u0p(K3Hli{K zzn5D}Zg-yyb+N_`@GO>&+J9|-9{08rTPd~=6r-4Lv)p)L@>nhe5&O69R=~;6;G65K zOABZ?NnHBgIj9ole`e1jRMzGc?R@q2Cy_s9HYwGvbZlu}FGQ;HZ|MAGG+zbyT&}ls z5ibZneUF6}?nKb^F#=z1uS z<>;!gG&y-w4rW%g;PSDyhV}j%*xR-x+7FC(AU|&t6tAzXblGeIkVib$3A4((x$$H;VEysM)>udIz|`yycy?oQx5&o@!YnF^b(?$6rejIAC#Lq?V@ z9Zh1!_$N6xo@5<~g#tnKVtRlGD_FJ{q}u2%G{#ema;hzRV&ClYWoaUUk)ek1-M|~2 zufJUNFE@5;I&0HSC^801a)g8HFlKwWpR>5mG51-o%;g6#j|KL+m7S02znqIkw2{Of zYKXG>|2!JIX*Q6F-hp)qMBS%(44k;i!Y1TxWd=)B!GCoeK8?_c`mwixg!D&ZN}D&x zC0D8z?0Z!t%@tn4|5$%7P702*;2BN)bVa*Pd~f*GTizt4zGGLfDBt&6uKiDs_cDD3 zqL0bz>lax0RgcHL`j=0ou`g)bYz63OQqDX^c<&p>3=CUxRvV)9*k&_!)Z}7jQCBRg};b`lC?OWZOLtlI1xR0OF9wSR11uI#DkY>Uj);uUE50um9;1v_SCj6 zX0VqTsJ-$SJ(Y|ZNBGctg*E4-L|#{|dR30938)vK z3-ZtT92M@ba}273mqg^KySAA|t7DYve%JIMwyRaEy$z5*9-)p-kfvNE1E!Vj`>9mL z8qjI|%4U9sjOZpRHf~u*S~kDACG`g4t&2FH6Hi=_&%!PLCgM`^gpTCcGPAvCyiZ* z8kn)e%{Vqdui9I-M9&=ynd?h5zaeCK9M62mUMwns8rnUJBhL(^UJ(=*gDU2V&h4ESG(+B zY#?CqRqKAa&;E>QgZSopy`XOgv)-yF7Ps@@xTC4$SpVQa_^fQ-19S5I4P&E*F|iE4 z;i$`C%B2UB^a2E^luLhOSw+m)M=<%U<|j8-pBB-DDam-HA7wT8u-)BB~W-+=@_ zM-uG3y`MW{XA{XKxQQJt;^W~VgjxF6jp#{uZuA-!|9}m&v8Vp5U~ZaR;(X)9a7UsZ zD40LhZTPBQqb}^^cvyE!{$5LiZ`V^C4wuF7&^3&DH)1U&m_4kr(ONksnC!r`&CVp` z#`kMU2JJTmJI0j`6P0h)01&#G(pXeXOw25L^WDKjZ|@h>u9tPXq1)E-w&XM4H$9E? zapxY|2@kKjwndT(65G#{1)9tXu`cSDr`US&fGgGQ?E{2LCQC68=E7$k!=&t&m|oiW zZZy2RBVqV;Flg{S{-w}uneSnaebWcZb=B&-YsI(?^i{x_ndkEMw$(w7Moaq(mN}=rYT_#Kmomd)lzssifD>@M^ueVH> zL>TZ=EquJ8WTtvRddh7hsKUQkn>B8_X0@?{x;goH(@uo&cxc(Rc}|9G4ch9mf_92~ zWU&I;+36pLfJe=&CJ#EF@1&+L$?YC_?~_nar0JOHQ+S(YwLqv?i!4dRwbVX}R+~%v zE%_OfmP}=j?(_rPC36GRg0e|)`k!FS`2HhN(Z!kk(k*J_n=Q{3WVYAZ`-7BJJKeXd zi_0&HwW*#GnJG9tZR?(REIKN{d+6@{?+R;u;ITK8a)2_rbgE zi*q%a_w6sWU$?ZE(n}$Vms)H0Im4oVd_VhefkrDKpLaJ38^%68Xlfmu;1+nb+=E!` zGXC$oxt{JtYR8$^8+l^umDteCs;>j2s#WUGqODJZ>-wHY7DtDj>5q(PeZ*yXoFC6- zS$7;Ti0QRnx+-jKVJE6A2Fs;yIbK{OM>5)*T&{*azoRKiW3Q;@R=e=oVqiUc$7&T? zf(K)GDv&92CDT-RWr{;<`i*ttOX4Iwk9Bc5Q-{O>e;v*?lH3JVm2>YMgC1o6HR>u@1HW0{^$kF9AWR_l%fyqkBi)4PjU7f)PXz=Fdi zaqH^ViQ^T+IH@re8uY*;$%=ph6xo``f5xcwTQ79h-2lw8&O&TeQ5CQ5mQGV5-@sqZ zw1GL+9*h4b$2<9@ziAR0EO~4M`1z>eJA|#1Rl7dXt=g#a5x7ic;eME^{YibrsL@Ti zXUc-nCh{};!AKInp?0Lm?WYAc?5Xy7ITqC&Z?rb+Gf>zy^m$|xH9n%0hW+VzvW&+7 zZl^H3HSfDD{z9&HHqBU^v7V7Hs-!{<&cXd`q~AAVu-(7@LbZBn_nvFm^F|U(OB)-Z zlK#0LjrqFgWedR7xUOBC?oTm}+5h$h093vB$Gpuxr;EwP9HWc(^Ek9n_NxZf&RELN z24n>vM&WtkDHXV;f=<@x|F{?XN)xgcBxfF>8 zvmYc7D(mK9mpxpbHy9fkv{+SCR(|vF4UeZ!mfky`u{S~T)dK3ejYQG zPFB?w3CbKXPoFcFZ#KnGAqs>0D0z);%MJ8Rv-8;IJ&IJjb{gi=_@;;Pm}l| zk$7_L42ZL{g@xN};Y8=dw=PhTL9MBIXI34`eVKj~a9d?)noasl$Qt41ir8hqDf5gi zE(+ojX^OW>N_$BoM|M8rDDzVO{#Em|cBPmlke zVW=nGf6{I@x7Re#Ve-5e-vwofV;u8%mMFM)~tGf855tN+A^t$}FKeJcAA|iP)jM8PWK2GuGvi^t?(w;nx z-m)f8^TI$h=Y}>mO3&wR1*~KOPp->;pxhn(H0t({{D z&4XgJFT8F&YN>mAxKO>xsPaBJM@;2uTc|?W&>}_4NR9Ek3Zhbal44yYd!pa)+#gqi zg}TX&@ORv8v$!c-&5}=;1lfCU#UP9=?x&3PfjWj#bf_bDZ=-rZ%)%7jZQ7LY;GJQ- zf&0uI_JSLh;opi{^^M!Cy#uG54C)tI$l1B*BWuj*8&g{>qqf3(?#5alf;a^Xa z(;E~=ao67*KR@$*ntAp5im!5mU{FVvfRFRT?S9tv;nHTTCC_hpGsgsOUa3m27rEQ%eV_ z^Mo{b@H3zcclgB{rA~RAeQC?!`?c7Ke^D7%_4KOovEF1RqJM{ry^boX+Y1!P}zat~aGDd#dmzsJlCMNYbkKhz>rXEE+#qYBZN>mq95h8|5!1LemX{ycukT`7W?!Y@tqY$4;QE{ zy~j{9jxFr=nS<)U+6AmW(u7{Kf@X6b0QK4_F+07cWtFlLj|xKJEE9@^B!A7`{fMi~ ztZxJV=4ScA_aaNSPBvmn8U?2>x(dwbZI||#hUkKjh{J|V)k!I9`4^}O*tk8QA1K)Q zYMM5$58eS#z$v>W*S>Ea>K*jGTq0zS8Rfd;rPL|{HxtCL<%L|%D|J`b!S!;WPLB)2H+(U?yT`_fX?8DWwpQ^I*H2=Rh<c;oM)&a0O9$Q z5g=jj-G+)1>**I7`uiL8s?|94&nqjsk5x9>XTILAk@ldixI&WG-!&6{@wbhZAKbw@gE+|Lhkp3pnW`N;G( z+X$YjNJ)Ij%UhCzA!e2+%s23&yb!I&X=^0;4;R8bd&377q;^IH8`2!OW*e^_Zs9UI z+U0$V(=5ztOQwVeq=qNvN)ER3qc{HTOy`zF3cWo0XQ>M1G?y zZGHax86U;%CU$5)P69|nMQO1T+4!Uisx7ZF`U6qhka0NA%s8Pxy|3-v;|`^0e2SxF zT^(W^8I_x6*?B3xn6Xmm;&9`*tSSE07eoG!s=|3S38h7tq{d?PP2q|*=CJrp3PM_cnjKetpGrp;RlsQp`p_9+5Mr2gdIH*ArjB^ZVsxk zy`V-hG5$9xh-K{Xifj7wtPE1I?|KT`!fr*`avDr)m;lag$eLvi~==s_cdWQ#}J(*o-`D~k7GjN1e4Ip4H|H5<{ zg7vsHeadmXrY^v)!*%$XX?SI?@dc9?`&5*R%<{h#dSgV9gsSjgLU0~@@6cLBGEM~^ z9UeT*bqcts?(b_=NJ$8_L1gC5iRFS%i(rxUma@Gg*7iW#IMKqRcACFb1D1`rQnvIz zDwn*h-sov}Xq@v#c@3{&q_G|tvZC20H?O{OXKNZB>lY1$nch54H83tgs(-F}Cp82q z4?XWS4$rzWUO6Yw!Un7ae?xiu08FSuTC?GBv$}aE_K%GyV{KlR$~=3zPrJ zo1`*44v%BUFQN{6XhNAntp9k+>6YN;QHgDX+7`JA2s$+W-xtxZDkt)Vbz&D+Mx;?4sYu zEm|6VY-r3WH|vdRz|>ON)72nU6Z723wMb&gexh8b_V)Jb6*fUoWS@FjOSKy6=k+_E zk5?n`0I*M*R#&Zwii=uK!gX6h2aU-ROirN3CWd5CQIK2DUUaE58fYlOf=c43>x!5? zfA@;Jrtj3}F8P|F#Lw!rECn;J{=kO6wP|B)r+mVAvZfbofXN&;M zKTs^f4G&{7D){4G!ba9#%8>YP-D{p`E&Iew2#VeTECiX;%6&7?-zB$oQmI>~e zauN?aq0*aMfYx(j(b-V>Fk`k%ErHQTDqQQ(8VZ%k$mvYZF?fwGi!;r4ZR4?dr{` zhowD~k1t(-S@~n%lWK-kktZiKnO!1{r)#dW?3;IAFGESNgct68?>AO0?>4#YIxR2@ zD_8m)L8M~%A%eMEN%RsPjf|8s(|?!f}94-!W1Juz3PJtmbsvr(61j0~vD>(hO?U?a`XC zp(5z+kYSmUt@1pwu78*0hja-pr-e6C=hAfWQ!Arkv1b(4q-j?skz_~)#_pfuc;Bk3 z+Pvy5Na89#oU!WXmQqW<8eD zS3WB~2~icW*o>()plCU6_ovF-5hjCh;$~=x2|UzPG)+)hsa3eSSIdhInNq)sP$1VW zucyA}xdNJg2I0iq$bzSruS84Y(8oWs#26#8P^yAA6VV5!qc$vu9I(Z>D9`G*+&7~R zEO|xFTr}>9Hh;~SQ0e!a1x^`%u}t)56CH(_nHliZtk?li0H zVn|YJv0$t>H}`^OzJ?w@qSD&UN|Xdf7%$I?Rg)Q3Yc0V&|B-M5xic`7HM1E*=9egh z_urNVZ*$%dNi3hD^xyNZJTcngzhGk7k_RTAb&sAp3TKPV^y@ay)t#}o;CkLVIGfTf zTGy#Le)s|m%UaoEBFqUVwdrLSp|0q_#UfUWvBX?vurjXbHswq+JpnfhjBBB3)iQE$ zb!im5o2IE8p1R#Q*e^IwSt}o-?P|@6c@QmySqUe|Zen`>H%Js~`}MnUz#)qA^iT6@ zV}Ti2e9XT`s#D00{$n*c>N|>7if>dd+;cx8kZOS^$kJ=7CUN#X$)aGE;tq^F0(827 zvU2vh+g{{HW%WzJ0EG&~{TVj}SFIuOftGmkFTmU#kMr4Ei3l&D!KQMysJ!@rBfl;@@P zhMR@lWBKX``^c!h9>!Aq0q~wZnnF7!LzQG5R|vo`U2UC!i(W&@7U$tYM>i$SK_)0) z``R?PT4}JyJk2p-JaNdz=04rXg_8S3JO5!D-a9$PuoOQB*M2F<(BU5HUe`VB07-1% zEXL0A;#u?YDel?)=KWuS)bI8SWj14Sjrw4nj8(5=qN7%f77dpUr%iDqi5V62;k!aH z{o4RwZ|Dzqd75ou_&x`sS>>if*XR19a^CA14CKu}%%KnHI;%}XMb-T{XbvGpu69=Ta zYy`@GzDQ;(NT{E3GWorEc~5FTIY3pEde9ggC%G7Hd^uAD{_Z^5q7~w;zTJK;L!{PO zXn|{MVkh5}ygV8h_iY7?xh9^ocg08tZ9T!Y_Ci3J0MocCc?Xm6R^7L2hWR$6^!D?sI@)&1!c4e8FFnWRGsXY z?}Udxi@Qk^kviK+yh0{R59D8`_yC4 z7YLOw2HK^3g2=(bIQXrH?a0?#gWjfQWZ08=k;Y3DcJeW?iy9O7EftJ1=+OG=8wNWQ zXFn8~(;P!0h8`cS(7YJ)vIoj^j=MTc zHRHF;Al^)PY_cz;jPr;5mdz^M?NYX<{&wtx>!R(OUB<9x@3U51NH3lRs!H6)ZLReKesjWEF7A09le?Kp)E}JI zC*S?eH6}6k9f!k|ODe9h%f|p!Sht_mYue)Nz6Ip9(m=};rQYzFfti`P5lA42or|v@ z(UOnVHH?1yky<6(A_D?#QR33^k_z-1`s%PkoN>F;YxVM$U{lx6fFZrNQ0UedBqU>B6G|jWHYZ~MP%Cxz&N@B0m!x03B(IVc(67R`Kh_s8Kc_X|pRo_;R*LM=GA&Z6{L!u&_~0mvGn`UeJ% zD~xg@HZEiwJnw+g+wnxJeVdnuy_(C`^b=w?13Z- zvW5vulWw4Iyy-Uymv29lwFgLnjErRMeBph7^~xNl5~C{kjp%++ zC15b4|HDt$$)Gvu2IbPk*6HC;dZ9u$#;-}|V zavYR2vEr9=Ub=DdL{dn|X$Q1P_sg?4q?pSdr#cw})LWxHU}Z-hXR9+UKRE0he)2=S znB~k@!<~l}7-2s95^->92TR`x-SLV<(oV$QDEe44tDJLP%Aoj%5+m9(&m%)vUadLK z>{nch{3lE1IcJ^s$lPy4(s~uTNSZ)%KXPVc+H!125Xr))t-pV{iNemVgwfJ|P#KUu zw=;WUbytx*fo*OGVQ85jJIjpze!6|NwLLbjaC1kAJicaRGgjtg(>*J~fKUlD%Fz!Q z`;-;ICK z)!)2#13ou=^aZOi-@1H`C0t3FhV60E zO_Sbp+&H>ZR961$?3kAi)8e2E?ML$f-JbpUiMemmeK)@OdX)y=WxT<*bRiOcFSNvpL(+|0KB_~vxOFy8N5aoSsoVG$yJt8TR43yZ_G* z6lC!h#+27#*HjiWkVgX&6Y3Ct4~X8>fJYLQtpBLVf$AIyJT|T zp9YtY3jEr69gG0qL9_RdOjbSYX^8d@=Z%SPFDX6&vFkKskbEllRd62_@4L_ach}GR z(#la46}G3A-S^Uz{Rb?P2QKWoT0B=D9Is)s9QB=PbXOqUh@I!Cfj{G|l|rk~R)aE~ z?rNCwTW0i z5Ii$krx?Aeknl$l!fO0({f4$U#a~(;Kfw3unGV!bC4I#IaKbHP>OS8Kjn@hJRb1qk zm!dtThN~7|X*X6l?`DcRF#DeDj%SdCJa{^bl){YDQby;_0tiBZ89I=?M98U+jw^$a zfkDz~t}QI2T;Z2vs{iEk(m5tH%zq5;<{t}OUNYRz9JG9AwTMu*TO-x7@)kw>fm$hL}Mac8L#?|{jsO!!K z*B4H9=Oh19#jGphnR7qPQ28xh90I~+o`%d31V~sSRsu~#l(Z7#wyycPy|k(` zXHB6{;>OXXV9#M2!sK&=!gKTwTX8}9gR`E>n-5*xVS~@koyT*DVR~MLuI~j)J{K#= zw?0d@UUM%>>zCZE)LX`fGulAq+W>^wcJ8ZXrnN8AzCN66|9|1D%Uw462xc>VhQL>$ zA!n7Etwc`rBkKjPm7kKdnPq4TuF5}YrmNqqo0={|%{E%ty0DOA+kbf{)p3v}&ms6d z$&vAOe=e`yyT_feTK=lqZv3V#9dw&2bA$bOp^*W(~ zO1W&sCM0nxz4C~HS@M>YL_$cm=|5VLOFe^P+%k|lSYYzm_=d=LplBq41!^PEjPwtm zg|O2X+*k?oxWQh#cJfo^;^*^E&-ORos-%RbcotCvBV5i^E+8!`5D0u}ioV&jW<(A8 zr19~dCjLaOXu#+^Z3P0B^6Laomg(R$uI}7iI~FM(8>LJFoZL-(IfyJgu9*P2BeGTZ?9JXH3QY(i`;Ajc)_3v)>U&Z5kH8%XL%rOC!y| z0|4L^z2PB`aM_b;Ro&m;2l*h_5>^qA?r4AAZbK+RStTwhMbe3nUocq=#^74dIi6H!ANlHuMgEf;$|1WL63BrU<@xWlI(4L|}#MHCDPFYE;<(R908_kr6o<$D2PefES~7IU+9YrA>B z|9KGsEk| zOF0tPGG_&5+U%pi=ZF7cs4FfX)V2Bac3AldIzHOt7!Tsx3uUKmVdF;RGooiOR|o32 z8o@iV=+4M+E{OHPy;7>KV|_ld=puO}G4}K=sCRruqynRhIz7R&py}yEH3!u$Kq`|Q ziAJFPG_lgdetjs>(>xBgmL;EQAZp02ncoaD#pO`x@q)rahMqrktz#kE?mJ*@v|_b| z36vMbm}O)YVGTthC&0d|b=K0{fPm5p-}|ou1|q7sO~-(y)p~(J-{qiYoB+C&81l=y zG_^)9P+GMnVKa!B$*Jg;y82UOtj#Bu;$J{KGA+)OC$9S^3ZdrqNT7#L5_0g_v9qqq zPsb+iJBqN5N6V+dHuXmR`;=$EIy?qx6+HfoPwUe!Se^H-y{}vV!kbNr=NstA4Rl8T zm#4>sW}FnM*A=9IeDCy~8cxm!c&((sU%bT!+w1Q0_NFAsBj*INmxwT>H!|!=RY1lx z%3j8$h%p#JZTLu_uDMTq;`uJu&i>~6oIAq8l3=AO3<^pU)6TveZ(1SL?s|J&9c9=S z*$}ZX&cQHTdA@x|9QrGjyuou6M(w5%l}TwgV}k^;FvKkjEL zlNpgzq^tv{?^i)qP{3Av{g|!m45fegVR*q*d)~65tWmC>ECyZ9P1%Jxu_SSrOldoX1Y9_Z+mW+9?^KlF|T zBu1I=YaTz1;&a4bQ~XgY*gV_d`jC}+YzJVoDoie8ub~6*??B>W$%v0)>F-QWJS8>R2z zrShdFhgLTRO>LT}pmoNdh^GS`-P?f~2SvxvEX8aBfJDjV|1!j}Zf}#>ZvyBqiZU(} zP(c2iAYG8!kuyI15zi*I66tlXz~bD`A$ZSv1~dgoz(INEnvh|DQlm_Xx~=t z*jh={rkcBpOwY#4c16o7sQy4cUnVde-ZbHJ+$VHxDf)0@P=-G1jAseXxJq6Z6iWJG zoY5wNrC46fZbFDrrr@5M#a$oO@D`rfxKvx$V4^@f-=OECNws0Vc>01jGAjHwZPhyI z#37?ghL{4`w53UC6 zaTjpd$_va|tbEHpvzt~sp0VSNB9tw#@0Je^MC_q4pPo;BdZ2`)^HE^_!mMR2C0T#i z0i53*%R%dMuo$b&w6}zj^lafsytC0036n&Gxw|$8-e`OcLM=W6e&ySKcFHLh%tkZ< zPFOoT@PvO~d;DjLyx0?cF&g%xxA$hX6qwqF1uo?Lf#Rvag12U(+qki(C}W;FwZ>CW zqDrdCp~WIPfJ0ciZxhK<9J~=(R@OG=HaM<4518{&3eqA?Kq_HCgmJt}U$SP%hygM= zPacPyWaeG~J@FpW#z<*p*~3+X@8WaCRN>A%n3I0u|386c-~U5*} zs0sSf8+qU-zgS4mFPHSCiR=2WhHYb)*Ud5ZXlZ41ei)>ix0pU$?e*a2?K5X5C#U|L zF_5fww;}L7#F=F>YtrZ>@~FDRoAU4)lBz+qLiXG@WK7~)y9xDbYd!AS%ngAcN_m5l% zIm^uLcFnUmHGu}UJhnU!mO!Qi*$n!8wPAOYuV7HW-Bx^hddl(90PI?21XR#Qwc~Pk zK6SVR+&M7~3iREZ+7DPGLP^86 zB}bURo@K}cT*d5f{3}qQ$0`%9(zF#*ccG+&Ho}2I93*3SOK5jMFO1h+pQqGB!?{Io zwjey_y5E}3;bm;h2hg7w@tuM#T%xnMmEMY@YH65?i#=eDpFcAD*K3B;FuUPH zB(#<}r*iyF|N6}{V67yU4C(Q+Pkcv}+>txK&VUxaE2R^Ga;+;c-umS61->%FvYJ1X zXDpcxx3kjI?f5MB32n)u0Gu+s0~A7^q9d5-+m4d9+2Vl#-y5P6D`q3LQXE&v93N;l zR_%WRfa9YuDo(HDhFXJexceJg7$*D4#z#<=g~%SC5S6V#wU5p1Tas#;BR`vG8^r;Q z!qwQY?Zdn)B}x!8k0xLivMn*b7nU|__r#LRRo&yH`Dz~+!f4~Hl5{R(CcaWY8VAI! z%I2xRu%&zY(UM=!ImFjsLUR=J*(&Iul@Ei0f}o64EK104*_LtQFRA(e({P6+GC13Q zFaQ3M6^7{-nS<5+`J->)3}JbAu%|S(js^{pP_W_Mi@jfk)jErIp6OgG0}X#$uW0q} z$HM~~HtgU4r0FYAQ^~{VtP%-jIZTG<>?-3zaw0P7U>ozOt|maYo|i%O4}?VqZEjDd zzBbYGQ#W>GUHK+_wyK$eORzjqTB>9{8W zR~P7P(&OfB&$jN2Dq@v0Ppi{Opi(#bJSIVei?SqUkmTDOs!4I+~#7yM*Xa@kkuiTbZ7D?Y5@>DPb+8VUF#l-S5)kCsge?c&P zZ?3SXF$c$EvXAX`;G-{OF6M3(%ijoOx>j0EiyC!DiD3y}%W#VL_K?LV1>$qv#rhBFp@dBl5YHTx*Hv^0ij8boEi76_*O3# zE%#*ge(}J+9i!s?y#9EvlfC5DjS{6v6-8LfvyvEUfAgr%>@_s{TOP85R>tQ(=1ce1_@twwu@Z8D1d#T>B}`@e{qkrur{|1&@_KS*+vBri z%xMZRdG)^Y^CsdOFFdV7vRC$z74DIvn?!384?&x!7_P)1#`kA{GAb&H2>=c)eti6m zH^Zl$T60dkNhmfqNkydE;xJJhL4D`~!{0P<%)1HM+2^+{*?&%~Xcxn9EAAOobaIqN zcK0Nh-NTMokWdossvub+RD~hCbt|cM^chSL@u>k^!Q#2OIWIp2AWl~FF>a=hsArbb zh&Tr}+qM}oN|63l3&M5Rc%!m|gO87$a%x7)8=hsaQDKty?v5i-; zrKX@5Vh|THGr7iVOA9-AEJr=YQ3OjJwfFV)T?x4*ol=Q#LA8*yzU{1QU?NNI<$m0q z_wWuR+vne0Xpu`zdC#OT{s~)pr<=neoTHc&l@tyRGrvrvS!dneUIy>}$%dL9{~lY< zBvK8o)YH>r^Q&G`l@o z0Dr3~6Kg9gg&M##sv5s1G&p_O|6b^Fhd|X;K!^Gs-$w=%bUCI)sdH28om{-hcd|6j z+}4t+E07x&-M+tS3kVI%p(K#SM3Kkkwj9=&xZqqVrZb-vsrxOm`Kw|YFTD=>O+{+zy;g&$vvP2`E6K$d_GDGya6IB!G>~_koR=+YmXm5bLUpO;)pT`o+ zpxy?71Kv2W_#=V%WN*73t1F0OKQchF(`izF0ij0v`^jOBV+~wAlk#X~MyxAo_IvHV zXcvH%)W%EgBv6nKK!i{x=2%RUlaoKZ21;@1k!3`h~j9!L7X67XYm6#HnNO2WfB7e$>M2Ag`jC?1{ zzp2;jk>C3^zkio+lkVQVKd$?I;(`rg01BqDpdY6t0IOp7A7~Po+YK8a~^3*BjpYB^`BVF4WUC#zcSI$O-M^fNGwYL zHW!=3eVM=KG5C;#2uCNQ8WY);1d(;45f|D;#ai6uQMoUsa&7;e;jB+^kLozcV<;(a z-SRXFqfv|rZ398MRw4m}Hg1HxsC=q({cVnJ_5bno-SJd^fBY9Agffea>`g_oSH`{e zxb{eqO&OQ0%oN!(!j)@XdvArZGOvBDO0xGwh<@jOzQ5n^Zx83b&w8ELc)p&`;RO`W z_x)hw7L6levn!(k9X(hf1z}QRnf8tM>2(wHV|J}6b?=6hQMC+oEE<4u3@_ z&9IO_1|pqN@#`-%oU5!Fdc%QL&T#*9aa{OR$W5>T$b)Wb_a4aVo(-#XkOk6dRx!^d zKE5^A@Mk3a8TP#XT{-hmodw{X%9~~Gn)A-We;+A2(Rf{1Wc^9ryLeJKRPpZmeQwqR z{{}yGB`zbtDLS~loonP2 z6msWi_`G$#&CszIZ1+sX*t^PuA!W~>XT`A{ZaYIDd}$~G&js1Dwfx0sNTh~+u&mM1 z_w#db$Z+W%FPY)cuj88}bNosQ->~GrL4UwPLsivt4mZgLYmujiV75O%vt~ zC9Xlfs!Cwcy{rL)0Ua&NaWVqcUq((mkOE|Dq96tjs4?}n25XybB4MEHfttndhzy-_ z)w}I=0CyICnx*llK1cL_qYB>w$M1d)M9&i1o;c9*u9TKMF;KJm=;q+yV3U9bIjNL7 z(ZwNN?P&d`lEDiuVW-yfomQ_bS1GUG*CsWH8F5O|0Y@8?L1dpWjsKf8u?;*)K4Kl* zp0CM96*fiAC~zQoUTZjiG%rxpCb%QhMDR5)nrkT_Ngn$3yNvqAX=Y9i;^lsJe6gAU z$&pgc^>tT}X$;8uz3r~6kY0TX4i8-tItiblOG)N{(7hq)g|3|WAia_h4u^6iEqZ#~ z29xL--0r@_qHzWZQUPCm^9D7tr&JdOibd0!*refaan7g?gOHRwYD_11fVUYq=-o}^ z!njh@3@W+MNf6}24a->6tCRMV#m(|B`fpG$t|K)x2QhS?{Hj1y=;heGl80w8$LaF4 z^}nQaMp8xJ?K0RD>q;2>YXj@C%)maxr|8)ql<^DgMsGYDh^Y3jYol2?-d@IbP~5?fy$9zi(WTn$y}ViqUlt!rT1|R@a2AY1CN`Y0 zmn0tBbyY?NB@_xNX8IDVB}5qHfzhJBP0QA8Olq_|9KCk<>lf;TQ2XCyx=nC1=8geK z2z`-aTBB}fTm~Z63i2oOUR3QTfRMzWm+4?JqRWIpRl;CbmC32-NGVgmoDC%oPAWZ| zTS_-f2M)Z$X@FE{pVZKmKSh-a3>~1!`T6Rb2Ms`2kLhEWKjXXm^4 z(qY`Sl%(%v+SW;o@(rt-C^&X)slv;kO}U6_17{A2*2;z30G6n%FiYb34p!KjJh3m@ zG?8%S_v5~Qe{37TUJpwDu>QpiZMKlieJzaz z0*Dl*%)y{Pcrv0r0l@A+!S~~h%!#{?Xlx#%rZtpvsUT8NG!0HzMeSt0?az=$aP4}N zGFRlwmoRBj8Njhbzwma5J4(Zgf!&P-yo@IQdv0NAsFB$P%fC$?EMg$4VJbcoffjD+U-iz z$ceZXYChQ13DW$XboBr0*hUHdniLr1cNK9+yEB?*RMHCYW9hs>!~T~`hlnhnH35o* z9%02yGi&Q99aq|pC4J6qF+~AQ&TS#kCTuVV&^|X>=UC(zb`lJyxq+yzAX71#ROLh3 zgVdm+2a&8TqZ@Xf%Kwhre1Gy2!O42V^Fi)K@+|Cm9Em^gdTpjP^t1_hPi@-|j93Z_ zMqa}FD<&C!{jYo7mH;yf{GVlp>)V=V+3bqarVTt;GXT3asFhp`ZndawFk(Xv`VlH# ziZ5q!Ih~p$Mt&pc_`4$NP@;%wc33oLi!I7sKZAk#UEReW-;P5TQlu}P z^Lf_=urM|GPtw=#9z|+kzuw=y4y6$o`c0PDqC$WHBEZWG9NF6Bi34r47BYa$U>&ao zgVovJU%NZxht^eHa0n-07bVvef2@-qTKlXO-eWUi=3(_n4qTmB@S&ygb(=#V@px3;825+04sbzp*cE z(jzphmy6ZRKOqUB;W((0|7Qx}IzM6HiN@ijo3{Px4zxmt3 zb2N-3GHUy_7oS^W7O1k?&+i!BIe(00D||HvbU_3rRrp=}=!)!FeU^Sut#P+2z*B$C zv4>uM%obFBgKA*ySSa0|uTRa!V%0+?0G*sb_p^ws28&-`u0j@4>rg`^M%XDtm(N@I zPd?)Y_Cc?sPM}>BoC}+U*0P&h7u9zE3fHx>9z7J@q0VRJ~!BWh2t`}-DldAwfh;pvI7n8kj(&hIq)@x46AD9A?*f+myD zSwQ`N;d3oP`3ki7+7QVcv$f{@RNK1QIVEHv5oAh%9EIP`O#l-+0|Em)BqSt=xz&9+ z5tR%BG4l90?1k<5$zZdOBd=bPqbNVzp8jo)hZSGRN6k)sUdk`w$QSd~*I3qMmg1s6 z!k`PInewDXg@tYKiuP-XTzMSMi}Jp{da)_7#2VlsDf#$NGsJt85cSf;3w#Zo&9a;A zTV_a^jlpTK!TR5|wY3KnsXSYnk+2qB<_|}jP2SsU;B4yD8wRDQwuUA}X#T}x+j2-X z$5ujsOf2<0&<(-7_?ie*pw5F$C+*|EM#wSn{`}NSBZ)joLKObGn91zojMV7RYoESj z08164Y`Cu%QrFg2=7qD1u>r(E!2KxTl2|M(;f0jx4m^er6;a#$;biJeqWscemQgiy z*;?uC#cyeR{4cvv2HyKuR-3wO}Q+mPlF_3aX0 zo$F~=&fl~CG3MV$NZrlZI&{W52Of*!+#fq@chi~(+S41Hx^OseVc*O_<<%~AwxEe~ zv)Rq-{3WZY`JD(&Mp5ezRfnmBsJ9HCU?C|$!zVRad>Ps-0`cZq346LoQfjdv&1YHD zIp5@dyzO^gti%(#m2hPX zsecnNZ8(0C_E;oiPiphYh_L2-$fBYB5TL4By|`s-P>bRY#eh%Q-cj!*R9NAg|G&CcFl|1A&I9-k|rsh}l9?Qm)+b60->iNJhjO>B#kt!m zg&iWL87Es@IQa=ko3aSM`u3J&{19>8AvatkR4jvyMGiNP3xH`}`uz`A*7Bu*z|z#A=b953DO{sF#t3F*(2f(Nzfih>k48n zBO&Uh-KeFLV~v2$m5N=R*zrvlvBG@v=yE@~qIuy-o!+$)a=fxh9t-N0xj!NU~q+=05kOp8fQ~LhRw?#|t1WRKIf+ z(7FHod)cbMnJFJ1d3<|C<)jR(v5hca+#KuhDjk?Wtv`^9Lw0`?gLcR@n59P8-n-2id^0kAhirc_>KXi z&t<}9ElR9^F!tTMt>=8iV17?Pxxel&&SL4z*#4Y{A2s$#u7oiOm(OOvHxes4P!`KF z^Ly)o+tY}As!d><2cCl4U&m|TV`$N$+Z?8s0dD4eo$SCMRgZ68v$g4iC$3yjV`&hu0KRu&$`0aCy{cqe0jg55{E)&TK_ zAf(>=$9KU^YGss^iK3EOfZ=te3?o)3t-PxO6;3w|#uA&?tym(YlPkXs3I)4(=>icT zC8$iFRD}{gKagN@JORCM&fzRiAnaBK9`EiA`iTe$X<-+%{ywxdcxjq}5_`@O{2>^s z_=;Zx(|o5~8XB6^h2k*bvtJLT!+cT^7K^TGI|k_;mk65|a2pJRh)u01+q>NfrlO$- zTRUQubphaD)jtWJ2tu5A|9yCo1ewV8f1(8_qgWa{XFpIHX^yq|zj&cvu%n&3k8u45 z1YVYyBRH)v5xUuk7{X}Soh0Rqx($@xO+NZN5MxKwp+)INe!}H2T3G<*XN8LuJwR@7 zwgS4A9T$Q#wj(kyo!%}kH;FcxU`rV>Ak;Qaptz7yE^5HU9Kw2aU}>30T}_QjhK}hX zDdfpFgKRp3Kcy$`qAJ{xs^~>0UfHCN#2QKWIt`0sMO*T+f6peaxS-`Teb*?$xF&Qk zRCjTjgnH^sc1xh1v=|e{aiR)KrsJ7~_>k77eNCSbeY;_ce267WQ6_itHney<=kcjs z@TvR7Ke&(g@4@$KAu0B==6`pM>L5_GcxQ6q!UpGgb zLpFX__p-WdnbMjZXFq0OUZ7`oL!PN&HmYM`;~775E7oV;nd z#2cvzaO>7jf2#b;t=cF~{^#HbdR$_kw-jGYC!KN3O1ZX1iWbO6fk%z*@u@ihNcA)% zopC#bfczW8$O`7Q4NDoa$Pjn&rS74k6@}Vj{BTVo5C3hAT$a7qCqq80yQY~; zvu%9k$iVbQfz?d3tdr&MSW!%!VbGTym#=^=%Dk51t=Ty;+Qv>CX6Zu|f`)ChKmP5} z^yBq5s1B#zY&z}YI-k#N4Bevg7o@W=KN1^i41GeLo{5*-U5w7OrtnRsP(gh(yjcXZ zjnBsezn5V=#qtI|RbBb}*Zz@Bs&fMc3UkekbS_uPYe=;CsEG}iE-i+e!mPo>N#@7n z>j4}cK4P{8Qe1HA@jzz24lyHX#Fpu@yvy?DkzRrRO(v&P^Y?_SDPCSBXB_Kot>9IT zjTL`;e{AEVL|y+@dMKCx-yb;&CV{jl(~eh#$nX0+ry&|EsEs@0Po0Wi8O#P zqHYJ)5I;7zFv3yLXE)L~Ol;k*tu9Bbsaz7%56(w`CG4X&%4)4x{}3=R{8iK-$S4@70DT7_J*mfRA=-1Q zhVs-%feBiWhLA0EL$-{-KSV;8mF&T$g8RMCEwRLa@FdcE25l{B3Cqb^dbygt4=PaB z-=^*z$zd?{K=QU5@hW8_e*=i-aU^AW;K$oaLWJHeaz(Z*OkM#-qCi;Hfmae@Zuw6-uSQJacV7M_%VMwLLDvLvQ&SAb$01ORVIDdQ=}Z!QrLOQ11|x>?>T7Nts)Ak z(U^rbrQI=jBUaLugz)6lBGhA0U67M7ZD?rC-s7A18S41(blp4%V7pkMD~I=FWHy0L z+d2ff2DB<)Hu~;bFU5KX*e+FUeM8r|BcTs&x@SIQ*AnPJb-b2V zD82Vqj7)J10D0QyPnYLf2crU;c^zH*+z@cA>xQ!c50{la*C| zZB#MCNJl9H62ud@D%=6f(eGZYR&g z5xM2%MjHMl{svE~A(rPBwHz)X0N;oN!6l#3qdoF^O8^bLZSUW|e_KXG!4`V1L<@+| znb+_{s5=4#U4p)WC`lI=M0FIs+Xb>AlcwhyTYuUpA2iY?1ilk1`2ae6K#M=>1yM-w z|GcGmpWPg^7a&tcp0zA`yqan#)0`soIzC_W-D-GE1$NeLrp7)-qzlm-4|LqtiWu%- zY?)Cy8=H}l#1b=c9?NQRX*K&g@TaVQZ@B zxKT~8iBTA)02&^GzHxtL)b)#`8cr>NqB^m+3-b`bdHJYjGwcEYvBqCMwTK$3W)8Df z_d@&&@@L5d6b>Oi>|iBau=0O^s*;t-8@hud%^HQ}&wG1&N>LKv+VBf&`F{^3oyXzo zh(g>oC=xHXKV3BL0R0}{>^Bekt*xzd04KG~N{BpA^qRuoiAugl*jC@l6Nl~B033}v z2J2~Po>2$zIBFR{p-oZ>vSuWyf^z&i29g{6!@YbQppZ%tCI-DwbNo!n&gc|wbUz5h z*+CUV9l_9NPGFg3E|N0wr_wybyO+gRe0Urzi*)tkb*Nli*!MMcquY5xU@9Wexgod1 z-$Oeims=xD;c$qf36_15B!_eaD9e2=C%Q<&j&o>XRUjr>;A)I|%U<5+34(Yl!@JN7 z$58S_?~1C=Yv;!=y*B`Tx)<)D@-2WDwn|~(c{{K=nM?qw(PQH}Qh_m{5S)+5%`#9U z6>rz>X5EpqV_Z6C$wWm!w}MR`3#pTiQkD;o*uJ4Fe}K7H53&_GA3f>eiV0VKJ$7|< zozl5UeHPr)&<&{>WrxeuIz=^H`JbWpd3(k^%YG8!u=xWY@^n*agXJ%QXNcdwuR?q` z?vWk{L`F7hYm{zMEQM3&M_`Je?gTmD#d{#d8|5f_u!#T+CV( zy0P4mTWxp^l51iUG>NAGeI_gC$%ew_27OjekjS-A;;YGi4_5%!&$<3~Hk1vbk5GMyo)HprBT>>4 zQ1%R<3;9B&YYns^bHmfaIEdBud$l{02zibSJ^8o-+<7LO7Efw{4)7G7I|xdD72>Rz zEX!2M8=+BjHBnC;2=mG%HEdUn#uF6tS|Od!9}gkUW*|p<__Wm!XdLr4Gc(|HqEJK3 ztj>N2H-U{G^wfN*uDt|k4%Hpv4zhms49Vj+JEWkW8SZw*eJKv|Jj!IsE7!Idk;pm{VQ z&SOHWKwy3YukVbqdEW?mqO0FP-Jod4S6pF6`)1}O3Xo)-$Z)~@x4y(}H}>u(sN5m8 zO-joT2(Y-jPGpn}s#fZ}+aJtRz|c$hp|QYX!=B{ooJrN@DAwDV4jA_ZQ=ltA{#1TF zGyPo8Y&@)sD`2Y)UVI$L_TO8jXNt%D=Er>Wv-+spIB6N9!3ze@ji)iV$UAy>gp<6kZ`-Jn_(2Fbq1N7avrB>Y`H(j zig-pO58B1)6tD9*_4s$4c!6;a44&gazIvQRAy{_Z1ZF6ZiwO)kACg=etdI~kz3Kaz zwX%?VC<6L*+Ie--sudxDu(8LG@-YIwwZtTW$RQkF0XZalHl!X*mk_#{#WmEVz<$MfDS;9Ajba=~w?5nws-)gP9SpN6Sb?)>=hHpdzf0zy4Ku?KqD z*Sb(ImPVZR$mnv>3E~g6*X!8}KCE10(#U=1;ih}Iy=5SftV4P2+7_~4%SeV2B!G4`H(;64KrVwvHbz4 zITW4u{`#|9LBBAt+J7aos)>xXR)7v1QY9CG8LNedQ%P z99jbL#>w&wc{X=6XG0zUL(Gjd(`_ABJnBR(c_b zL$DV64zA%9SKz8R2ey(1-?=N_I$pM#u)Au+OVFNd``WPZ-^!=I=~0)HpUzD7gOxRb z$+0Nas|R1bqd}G&v1rIGW(ROX-h6JDs70uvhuuT<&A$N&RU&*48P z^1#=0^xmofd){e7ZRZ&x?U_BJksrSnmf;`Bz`e9HORDJ4dH?>FFL+2FLN?Ru4Q71f zNJd$~#($|X;;o*iG*zI~p=V#0S9Pw{v6DL@p5EZcn@ZMPv+jedn(5iLyRa1RMZ?kWhhy_;=g~vBKe#%mtIziagS5(ML2j zkVl%c$7=DjpH7Xg^V}txCt2E54eRxy{$VL)SFZB}L+ZHXFAz!K4bnr!Z-j{8V~X(B zo_XLoP=(kuF=x3ug&S@k*R-{@eae=nh>3}DSeAQ^ZutA{GL~0Pv$hzut_aI`RAAC? z_a0~4%$%St`TqvXC?kSDlPCCu#|XfFS>R09{2J3Etd+eOYPO!!Cr*fGvMOXl9jCyt zR>sb5gTlfW0>)BoOn&suy>CtLguM#H{@Yf1`%p|MyB-WhfJXR^i+Ct5*ne0^@k#GH zPkRM-i>jqy6mhLhPzf~)RDm?q?1r$8fIK;84$A^g`a?}Gu2{wCiX7F^QDYTlqB%kY5z(eNrS8$w`VxTlG2Ewg!FfF^~(H=~cDE zCb`K{#WjyTmO6FZw>8l?8g%=>TY)D?2{_xjY_P;6ihxcg`~3Gc`$mIXBbCMte-XncHp2hg1(6zS{qL&YmQ!Wcy?k&?GZdBHQQeSq~8^(&3K3$-!8#0T+^z<1a>M3S0*e$ z%HMr3_~SL{s|jbuth*LJpY8UuFz^gDlS8rn<<2rwN9Lm*nwkqgWqwuXTl5pJuJwPv zEthlLD)driw(ez)yhg|sI$**>NLt^FPhoWD9{YTp+`V`HmSY?vu`qC*Ju*;pJ}f$E z8@}n2T$P+FWARQluDY!zi;2pCoGykkv%e zQ5n+3DsC1e6cNU|W^n!nJ3*=v=zLoU%Uy_k4uj%u}zGXdkm4f}_MEZjR z{ENe&!246okvvqT*vM_nzz@z|Ps`ejg@t?m|Uh)C?q_8z? zLo||1J9h}DjgHb0cpIr#A}@U{-udnr5hzV54)^r-MuC5IY4WZ_VW4 z3&+2zD>^h%4Sokl-8s_7G)u{<=g)UtM%WfSZ>5!s+u%eRgPGt4{YlDbSn4)h5Fvtv zcs0noSLM8ZYw?DUy~bBg7C>%z$2WM@_xSJP?uB)EfWX9yw-%UTpNgN8rtJ(7Q0!Rw z`eBAZ)!AE|K}&CaHBP(B`N@$HMs9W0Q_udn>C1`EN4ib@Uw_~WvIS>Xiy6Tg4$9N( zCn-JO(i9G2_GKA;Ea9&Lx(B0;Z?B*0I4ZWh`giH}>QB_Ghu+JvlCPu4s4V5uo4DbF z#pXRCekU^CltIbYr`Hc~3B&!FdwJ$1rXN=~b$I^*K6`=8`r4!-(ajj4wZi@DH^JI>C(MKYKCE{|cK6f@|=F*8c*KAW$(}zEw$hgCU z-p+v}DQ*@CaT|&h3JPoyb()s_v0ov1{&&G`w|gP`HH;=3m-n&Bw?%5~sqa$b+!nqE zg(UE z6%(yn3Xsv{x=Kc5pc@*gd{dD@%8Ai_oF_P z<|^drUDi}gP59F4wZ{t^Wd(rLTh;dYMC54koqNgeof;){y__PsEt*5zWFMe4q-kU< zPgh>?Pu>gITC(0`HD6F$;}hOXdgt-%1bMFx$7`e#7B(HCZ<+ouZO^}cbtCyn+x^ee}+2nB6{L?rYXzn_8uOl`j&ZM!BtngD3(YQqf7(U(jh3eO^JVsc%a^{(C5k`Gl$O zwT+-F`KKL5N^$z@kv|eTrtSu@44c31_52v~XSZ}ozwz1K<+)FSADCEK7dQ0gxZQu2 z80W)y)Q|mivDpHhnerA+{&C_jb91{7J^d(nUv=$%*si-(E% zUOwKQynoHSF}x1IN`-Mk7VxxTn2DR>YezTIuqWb{0=18S8-$NeOI)pw2{CMUZE_0j zY<1rjLI{T?l?6^p`Ir(GN0+hBm>ybFDaUT(FdOo6ztcr+;f5Sv4s9p8qxp{XI+B z74`y;B-V4H?(S|%O>PVKPQ>y0^P4|+Y;s$)z(I9faJ+)pN?{Vcv{ONRx=k^DF`q?pd<&e zLw0WARkvRdYrFegm#IkTC-KW#=xt~13IATgn3vqJPDzA3{mv7c&_}X>HFH|L?9jAl z_4ZHmC@H}i3+TwFRedREh)tm9F`Mzjd|mVs4;wbwYZ__NQ2DD@HR1Lt@})XU0;g`D zxDnv2{NDc2>$HSL2H^@=AotkZ&w--$uKY>!h}6Oouq6S0Yy^Bj9Szb4tzxNp7Z5DF zgBgmlnIX>VN>+Pb0|2cB(y5ia;tLglY)Ax`@QYGSFHd;ij4H~S(?x{I%sEvdttD^G zo3+7fErBPL?xEBAhlQ?@Xi<`r9zQ9*p)h=<^+UePAtt|xE`$553{7f@Md^H55 z5XI0ELVkp!29ZaUOV~jE`r2Kq8JlL$l@}&NE#GS>stXHTk8j?tN?2ElnqR9gs_N_b z5c5x~Qt7CgJWL+KdOUmdcb)g^a!y2c15;#|Hy0+eUY3}V4QsH zF9jII>k8aAEb3n@_k`mliR2k@%reX}S`suNUHKej5IsMJ1g%~wZN1dGG?CHm7tYvn zt#5I3_nMjXc7=3_vu+!Raz|Mb$N!6aoZcs~H6u@)-SQj(VG?>2OnDSQNKAqWYk&^eys6fUMs{g%0o4tjj6ghqdjxk)}|)Y z818h%Om?H}cP#QS-MbKbkTQkv$-?JmR2^k+&se3yvJme89$*}SsqRkp*`P=De%|@1 zIa}v7ena5NK@o**0M#*+5d_1QyB;rbf;z=`KsBfLIww z4WiNpaHQiVQ{r?fqaq65R(2QiiLT!xS8lfFZHNW1Hk_rkyk8LisW!A77o-3f=t62h zfeezOJc{xi^1LPNkuOm4UPNuH+6lYu{_^F^p2-MnQS&FN851`C!q~&A1qo3}Z>6Vy z-h=3U>e)IsJGtI6GpAnc6{Hq^bqao%Ek1YzPg>gBs}gAX{UmA<)%GCXgi9+ko|RW? zLs*Ww)3dt-r8bX!pSVio^J8}5c^v3=vscEiNG$R_&JveFOkBTyF)otzV@kw*pJye0 zdOfUNHN!h1x?yoF?RSU3-Mkx;8(k)`DHFQaAkhG21JM)MCuk)3g+~BAccdA7z8lZN zina)$5vn|hgwBNxaCMw7}9 zii&V{k%sX1geih16%!kGLA@cZPHkSv2h%St-2F(-5u=g|oB%)w!p8}&q2l^XdocDHW289U)W<|3##{7BT0{_X^R@M8FU+Z2nx$td& zG@R4@eo6G*qtq9QZ;3~J?HH6R23ErE2)AKcdW2TCi-HAvFl%w8c=Wuw+TJ6G2& zI$MM4h}b5%$=4!|TCyrWGy{Y3F4I~FzK1NgZ-4ND=mC@)V39b0bI&Sf=Lql{o&K1; z>*C82Fvh1<6Y?Z}RXqY4^wIflfHe;bSi20sPu=`j1?(3Keg*i6ztvm6V+l?u>|X~b zk1j`Cn4;`yfx2FS#LDRy*WBodu4%TN4QuSbcN<(OiP@#cJ@J%OR5gG`&k4jG5ZE`u zgxQ(AJh0wCf>Zha_v5CoqK{v1DX+dM%|nj48D`MAs_14+@cL(Cy=r@{OV6ziX&!+@ znSG7S@r4k6{-6~Ng8Z_jd=Rmq{fJ7~$>IfENQ$clB+SP$W0)y)sQ?6f$5(>x3XQfV zR;ceGK(T18D6*9t@U5*#B<=~;qIbS`-3F~Iu4Kddhg1+)bTpV)$xB4LJ{*Hm5ok?t z+E97AwA+f01559~d59-PUG&~hyaLC93hbv^4vXqKxB#h{2gIa>a#Ito2khV&$`nYnu zy?S>we8dXEN3VU)IiKSEwo6?KQXLpgnys#VSJ&UZI*cEvglC`nVpv<FI-J$2Bk&YZ1b+_i$ld&C)%vOW-PH?Z|GV#Dg3bbtd(@BuB!qwF zrDtv|WhK1KF!2hc_=-%w_J3wICDzJy_eJ#vRU{wCWz76`jvhe$G*lD^N>xra501{0 zkS!JZ*+$bVnQMR%lI9Lg$UQtd+-=!K`vL14P*DRWpg01AGQ-lkH-C;MiS%t(J3E`p zKYZwi8fN?+0bN1Hc)?j`AD>k1uKIU~UoX5R>Ce43PQOH-3{!&?Tt8N-5sm+H-u5>3 z7T_O-KvI#v#}GBZo~bz7x%#?@e39^L@?3$H6VZawPM<;cFXOBiBctz_Us`}#-{CBB z8by5QMFk<(TJ3dYLq*tRHS}kd^wPaSB+h2k=`nTYK_RT7XMThuET2-KlWnO{r8`b83vpyzqBG z!jqH!Q-M0`MeRK0!bh_p5bb`U8lmAJ;A4^i4ydgBcZ$RbQs`6GZ1v?kNqA!sU?qbr*&)DP{FFrrTQh9!LyFX*@V+wFSf z+O~-~l4-4Xs5`eV{eE4SJA=@L5uzT?-%C7j@nQ(pwU;8L!G9EghTHo$x6R4VjD)1@ zXs$`?e4!c_Z8#CGH}-&5?pYp&YANYDWG(fvCw#TFKgGskTtkK; ztpSYEU;Ec_Z6H+;2XhW5q3h@!BjLJFB~twSkXS8QsoRP!B}az%+p)cYkJZy==Jv`2 zxNL@z4>LHb!TR>i!?sG1m#AG|oK(SQi`3-pU~7H+1rS0fwR9go6eyOl3y-^?T7+Gx zCcu_Feot4~NcgR0UON;`S?9p=JyEY+>Z=QhVkL1o*>MXlduBq{h+ZSgGO8GV!WiJt zg}kE!k%rAwO6BAApZ%T0j4|4&P-ndgGy_vRV^)sBHf-7DqlnUH@$MaWjVHNL0Z*4k zXgNS3zlyNGheg=;2?djr?`;l=OCjKOMe3(cu;4VEg^RT?!)`7l9smTw%3Ac(P|y4H zV1?*7P%h)Z1*h93(J{nw8`w~PR4h5_KLGm1)Sq;``76MW(1|@=pRuL1j(aG+)-}dT zy**m@EPU4Kqnusq@ZCm!R2EL{atW*_3vRk4by8rPnDKG5QW^fFHs~P zB%#+JZIOBl!@F+>KjBcOH5dLEmASR_2`mV^8)lK(@rG^p-j=Uu_K>WT#~fo6$mGc; zHMX!<*-a4c2E+#Deq7DQBL?`jZu5gDiI9ieYd9vKaeoz*m=Nt}9a*5lvr!iry{CKi z0oRg$uf$U5xjbrbB&V^vBRA?I*;VhA0l`O=@-L-{j~LylAfCwT(rgc-P;Wl&N0}6e zCs_ktKmW7%!dG|~_xA$ONz&?{P_A@0`5#7`)atzlc$1^I{*MbH3vs&Ww@A_c{+aVE z;IvpxJNu4>5Te4B1jFY4I|{_$D5!_i+3fNkX+DXniRahSd;F_Zt41-5JS}PL(CJPm z>-5K0Noms6-4;i)hXClOuMRvgcd`KX*=?(H1P}Kka=`=yPT&!Z05D+nH!5NGyrlP~ zy(5C5vs`eV=E=f&?817n%DUbAI}N!mq)pvMd~oVjTFA@38VUQ2B*8cN?nS z&$BnDVu5a?e{nZ<;l}^(vw3yDwgD(*!}iOCom0zZHJNq3N2s%Eb6j8#(U-bu0!SUE zN5nO#L^1k5hnZO#3X12CbcY8yDgnfxhT_>R`1=Wfeoqwyp_=8Jv=sEPh)qY?Mm}jW zsKzd$!*c>4Ed@gV|0BX;xn;<7d#Mal>!_GUXH<;>`RDYiWNuv~ap2Aj?URCL>xeR* zu}RRb>%qvidwY6#gcTPuZKE1%jrda2hZ%vFWm;JSXaD-&;?kp&4s02F)GZI)jn%_{ ze|b~gyP2@C9m`g9uY@U#W$~G1S(GUqJ#2+;mlD6Kh%2j6z1y`#4yF4q{6G}L2BI2j*>2t(b-l>!OeA@v%6jc6nNo<_akcW#3mF9L+jqJ^=@jc8 zmc?Q{fhP7}+1EZzdlkOkY||y+BJ+oC$SREeG;uI7HZBD7)oG@|JKqyYyM#HPpI(62 z2JUp|jWc~-T_65?F1Vvt%WobXrC8Dyw!-J|G|FN7Wu_f!2_oaH8{oJfuYHl9)wrz z7ea+;*jWTzR5?MEi);Wh8v(_J{y#z=f?oBp&RtMhLp6!$`ApZEi-cBo`r;HzZX3m- zviY8l&;oNTF*S^-fk3IPY5^ZB6e}aSZ77C z2!L74ghh8sXz!J%5ss?;%a>|pslcpC;(vPz{I;&xXXzUr4I|a@C(TVb{OW= zF{Go;51@%}JQ83TMnP^yd_%zFWaZKuhzAVg{UpfXu)~9J5taXoPOdmu1&k69vL~Kd{xgWshfLp2# zH|Pkvx0k#hV+%^;%M{Y8GC%NT?K`r?9qf8oi&cBNi-am!-)MQbV!bNDCTsBeuaVi+ zFEI%DoN~hg3GmyHI5esaU>XtP3Bz36T9|vCED_M7(VoGFvCXZY31uTSXn&l5>o8e3 zV){SCaHu)y3GgN$579~fogxDhAC5J)_AsDEg$PTS3T!uj#MpR@T~Hx9Jkt{kz?*zT z_2FaeE^%ygJa$+6kkv)^dT?$mOCb|kaYoh4hDhZTnY2oKkV=9mhM-Y@Pg$RWx%dLw zOgDx$mi<)hT=m7Vx!f;3W?Q6P<=gZ7gE=D`gl|6IWu|ba$LhRZI+P+d(rlen_p$6q z1IiDO5C|OFZp-M1hDCd14=sPtuksosv&r}p=YB+IG@@naIwwMMayorKIm3j1zD?g13sD#$%rWIG2-?yGdOf({=Xb>>3e4r3r@vfxw9bv!yZV#DM9A&~g5U%%FZTPRv^ z`hY-#dFu#$Yr05I!5}_LZm>$ZMkIh@5WCA#G3Ua2IqLyVa(t&dDY6Rd^nnE@FRgks zc7}JtzW>Ndr6OS=z1=dvx@p!Z9pI;3CGJ9cKVr6Edrghj{ZW1GmUd^w!Vq?swZgwC zYp;1^I9ri`cvV(n$lYNb3(&wDR3JQq(0p+y95Sf-5=x-KNE~!c=L@s?xOhttm=U6` zc`j?PnSA%p?~+Gy_Pg)5sTA+j)ox}MpY_QvQ_WrI6aLr%MJ))sQq{L!00;xuI}s4* zu}cU3Hb@Z;dgKRyQSTfh*}URcxoKG_-B#^dA|b0yd{Px+ zpqDyfL8hHgzv#yM$I-?h!%n=qD5{*>MqMv9j9XY>E z-N-<`>Z>KJLtI)IIL-LcNPg(Q#0RLOgy1BvlI_~L+t~pO(i|r9d6rr0%#yA~ln~Cm zCGhlsAYf=dxmu}l5WD@A!}#+lI?)kF{fifSSe(7r7rdL&V~x+xd_6_~!=KGQ_B?^N zE5=e}B$>R+jA+a6J|Y>X@RhGSe@51_B6K1&m+FXpnHj@lVFe!}VEfKoWCA*?EFl$s ztJ%Iim7gxp7PiF)sk%8ETPz#)Qe%BU)fb{jLKcg3*nZ+TaEB2Th~q(Jj$u)|+6c0} zfx7GdJn`Y`S<_$PJcIZV9{@HD~){JLEU7M?lpB8Bb+9)(XGZPnlj$=@iom zHM<%K`W$*qnZ$l^kV+6S_Ni-)eSPkNu*kPXoc_0WuaS&z&`BR_-Ho`9LOggSnGJHeZIU!@E8pfd?AVd+Qk7JS3V+{x<`zUX!>XWk= z^7yMxr2r5M33+fT((=B4*aoBH9=F10rL@F_>z4KxkmzDN*UIp6GZy5=S!dOZB$|;< zSlTsXO}2t+WHvqP-}GQ609p0gnuEgm?8_(^{$?SyJJ4LuY6Cc(cqPN)b{xkfo$%P( z=mPgX&8G9Lb`svLdc>y_?ClMz9|u0D#C{100|TOZHXw}7p7v9V!K z39|Gu@aPN@r-m6_ord7w6c!Cr@-OcY)BuHKn?-4=+XH%*rT~cGPLvN>m$ZcB63f3^ z;2LP6Z`Yh0?Eon)kJV3& zGd{jojG{qYNI5F?5Jb&$Ut!UF@OV+@tCsJKOJJj1k1p?^=?L~|doBezP6cJYo`XPy z#!#FIZS#hx(MPnRXHjX3j2pxa<=x*kVz=x2yG}%HI%L%=Ilthyx{4|%`wN|&>&nj- z$*uidpHh1A>8iizR?HdCzG?ndj!D=FDdX^R#eFZdIluv*b_ne!OF6}6Q)1p)LW$#fNhZ3Ab>2qh~<8)KUqD#X7IR$fa>qA_T@|{N_72T|2 zZYFjbO}BLzr#FbZlUQ+vGjkewiP=j$TcdzmZ*Dp)-*{-90CfICAXEJKYY&%8GPryH zKakO=gp!AVFt2NiU-fd-mhw3}BbF}{sr1RZv2{V3AJs5@Ci{ct;3HYmf1-HcP)jOv zk}2yGt1Eo`d36DwLjEj$icT8Tr?`9k5cjC6PF1G4{FdW5UCX)Y#_M}E*Kw{}5;6{r z1wJB?E?2v`TQB;`MdB?!b`hCffve4JNr-nWt~li(u^R24g6(p9i(H><^4apX#NrRR zNY$-fGv=!D^~r9kC++k`EPSJ!S_5WcPlNd`GpVa4C?k`q?snCWtg^pZe8#v)dz^V0 zz4_RrscG+twdDDIn`McL5Aw5OG&1AG84{L|;PHNJv?q@WRa@>@+!4e=27Za3_% znA6Bt+o%fiv=+JSy{8Btl{_LZ|FC%)6!7F2RR9nS5gX_P@1ZcPMQ6Pd^bW463@ zW0XwAcUp|B=;+JEImDI6@Xy#NHCfLw5{~h#)Q9h=|01NJ`#)=6m1w{tfr}?U8fN z?7dg)wLYuFJcSK5L_yls)kFqhySbq0icIC(>XZttG9}D=B`w~((G}q%INF(BwB{mB z^Q!K#Dk4K8?TbEcceyE>7kiAhUOfH710vge(rPyel{!XiloCD={3)1{zh;dU>?FSv z_d*IAp6aNUJlbCtz2hk);sYC3EGdQ_ks0xMSFfC69$XaSdDvK1o#Z`^SUf^M5M~mb z&d#=&Y?xDszT>Yx=kS{{m}w(`fWN#axTia~d>NoEd#$oQpw)_+eP*TQ*Oi>5yN5o_ zhe1C*t*cQHVdXtJZS@PLLlZn(&#W?Elsire6DawoU5(aahc6GWk0~`Xa(^H0-$JOI zf1q#}zAF8wqNJ_yI)0g*Q zp1XZzx0QiHmwiBTUsn~>h*^%tE4b7|Wzc1Qe?{LyU9J>c6--zhtHSc6;vv6DiU^bL z&*+?nG&M|VM8R??cacbeQ(wVS{HF}rE3ZrcVMzxr6MrRT zd@Ilt8j{oCZBCBad1{R}!H6xzluoL+CN8h^&Y;0xZd`>(HvfiLlTv~kNKBPhQvBUB zmid+GUG{BXohebu2u+j=uz7wh>GyP9a%5|2m$Bk#9bcxCw>BfCwty?VLV4)&w21aS zId=82f;<`s9p-kmSg5rcYav&@!O|rhb9=%R6ug|Jz6iO0n?cL;|DY5uP&Q50WoAMx&%RTE|k>v zjT&1Ob^Z%$!(vFwq8{1Sc;~OUt!zfYtM4WGEWejIAFi65*>()--sPqAox`CmB2T%S zY}LyRdC`J~>(G9v>5^Sf#p|yu2zF=Ne;Of%TJHB+Q*X_V9+?@jxnz)AQC+5uJKFmC z`WE9sUR3{kr<~g!4s&IfQ)*3h|4PYoLfL64*zu`({eC?%GJ)25sFEr z>9~I7%GhN@UgV$33irpug-61Y5U!jIYz+SX0E&9HL+4BFE1X%rd?yRagV+k;VA*7> z7+PHV$M6%~yV<7l4dS%YzlgSRAX6+^-f>n`kSv2(YB&{$dFAdRGs7)oqX>d*fQkK_ z{gt$mH^mHaS7WhYIwZ%B{UsK3g9K%HjaHmxqSirXXH-H0m3gwTy(M$Pv!JYvJ0vkk zt@M|705OHsJdqF>jR8yu>D&w{Bu=v|(icWeO-O0lr>2tWTs6Dj8NaW5*ZxU%%-jj| z976|5U`{2*RSs;Hceal!J38c3obcq{ii$(fu;;Ebs+<2+-4%Jllm3iM6||$#s0~On zU0$~2*ExMSu+FEOqAd~&ApPL2weQ4Ej1KZFPqz*Pv3Dc{HD-g@ZKzDeFsE4p7$Jr` z;-cAlAtir6lAsb4n|Hr=!T2uU)j5ooA93JH_NO4tIA`eV|G43`DL!Krx}~x!fcLZb zhSpVZQc_t=dKt`*XZAi5I5O9wH11U!{dicOwgDJ%VEWV}5)b+b1A)-VXMPXz27_m| zBzzh9yGVEZfs7!vX&b()p!nHUH$A?tI`tzq0h{;9!)|b4kuLK?`&gSR=#vaTHA89R z)rs(t1RniE2}-3X!Po4%LHTPv52j01B+=9J(bAei!YgHIBJyP!A$%&{Q0QD`%{8#e zJ&}cp@c-02-gIXa7}B79Mbtp3SJE!ljFe05hGGYXB_%H@Gdye-E|UL3rU|SbIae(J zi^^sGLz!-|+J6Dv*;u_tMFi4m>2BQ;cZa=qvLG70yVFUY8O-Da1EWJa>AEh5{~Ge^ zVbFKj7#7+6kNzzsB_*Hn-Vll0L~_R8R!@my+3;_6XA@HekYkjIXzjk3 zKA^@GR^8Cx z8@pBt_g}S14E6x)6`m!8vgEwv9ck(v6O~7N(aE}eHXS3q$J9TGc`1c=)2K9~Z07v8 z$)-HTN-Ku5-M04JGg}rj-ZHK1-GKbx!Nev09QOKr_Ze7kzOwab;J$c*N;j40$|3J# zCoLhD@E5#3JS8BM24JdC85o_*k{k1#-Ohg&d3UQ5S+B7Cn>A6e$Yek58acVE4|td= zJYaW2_B4Y7=)fzy&L9+NQDdcwR-I0oq;tOVEw8TnXMQrJIH&m2kH(F^{m_B`suHVV z_`MqB++Xn{JN*{cQeAm8X;LA{3}yAeNM)9V4xOv)wwAIkC=Hv6mNeKF&HUrIwLM`A z#8W{8G-U#v#UY3?-c%~R0u5Yqbk@8JO)DtNc-@!l@x&tAW-ep+ll)MQblZ~-3f+C> zvudrOF5uVbU4ed+z|b-$CpuaBmaL8C=kH|Uiq<-fpdUg1#y`)2pgy{NsTt4Wja=G{ zZwn1@#50Ouqjg9LlG}mxotBnLOg*O4AydH;)UAQA1WEj%ePe1+fbjAp$9Rtu+(9p6 zC~*4pcgNI`f3OWBl;vswf>mEAAvZ|@5W}VgTBC|{;xI%9Qdi9*<_D?jFk=F zRH_FQ+WHArFBBCdPfVpsiy|tb3`NcuJvfEA<4_7itDv?dH!*Q$?_*8C4;O;l!avQJ z16+l}y$>6{ThT+rAdeHgJ4{eQK<-HA%JT|D7l~u))j|b}0Hh9@2fEe^6#N*n$G_G% zX?qXEsu5<87AclP?&tCOfneTNg*>Jl(5bYP4AQ!VUD7>ygBox*ykd7* zePFFqyZ#UEh_$Rk=dsyXmzl2R>j9#MMEaf`&jfEs=P&4h_c*cd>FjcfGEBeAO; zSq8uKg$!H4>ITs)FX%YkJVz`8|KK zHcSZo_8r(*Q{}h&(wv{9T=CgCQ(@gszt!SnYI*PB7BDmX%y>9R<9Vf~U9$U@%Eojxc|+fOsH%Q7uN_Jr85e1|V! zZc%Pk#3PXUrBZTa2u(Mt}VMpG-ZJH3jSW!m9{QgLXGoe{}>OwYZZH zCm(t^qZXYn_gt{(1dRCtnN)HrG0mpEpcF78hG5C_aA#^6@K!g>TimRLs;>mxXTRta zdQ?YksL%wMb!W`tA#V(l1ySX%n!+(eAeP2dl;+4 zZg4WzE`Hoz({Pnn9G{-DghyK7>uRf3JA;$sEL43h;7wc~_ox@#HN{mW_FSnCUM3_Y z+-%bFDWnP)lvf;LLJ#DfZMg7pZQ4F_Ic0dGKgRVl!xIYa)CtX9K;DnUng`+2M3=AT zF>jfky*BoS(I7AdI`pLfA=8%?L2M51HLWNDz zW0(`UquE`?3) z_U|`qGyW%q8~OwA;QYMn8X1AI!fxhGSVOymB0FrhaX-Hc)u#$MwHb<&74;dl<-23NIOOS1_%&2Y57|C`W|QD&DDOH z1M|8Sf7Zu=$0s;O@ zl~>>Ut6TIqg!luDk6c_c9PV(l;bdu6=`j=h+S)s{@Fl}q031Mw$>r?|QSat=GpBF= zd4CP}Nb)x8R*qH2U+L1s76OFomD+Dck}NglbiG@ZLz(b>bQ7b?JiXT9&!+*Z8+NrS z=>PP{dO}c^DEJujp~J z+-Zy7IR*MoZNIbm$bj994XEFteus>QfdyY1D zyyab4Ix?`=mRZgvq{X^?;k{p2Y;n{%vx?V``@cS(8$ym~je@;+_3V8xD4iLE{d ze&|Q9-m-R9&ITJ6 zR&(f^tiNNhU6ZcVKb{M<<9@!`8Wj?-O^z)&l%t<1(uvK|v6n^Hg0T*rrjuKqUY*tk zP1M`XHf!UY&mp#*r9MzV=T0IoH4sqF|bu6dCguJxVTW^nfePwVc;pRY0`_15bF@sp#q ziI?W^X${qX9$(-_xu)J74V22Rn_P#X>rjrw(ZWe-s zr@|LiG#43=dngvwE~|?7K>eCz-Z~Un^Pz>$JEHRc?$A8#Jo*;W@+o|Idn(t#Zx{Eq&=%7mWDpd1#P;w_44B0p!pZVw6Y3-WKC+(Or&joW` zMTBYpIxD!1`U&Mv`5ub@)bg{JY+C;`PgoaF2wI+;o(((N-aC^irQ=eunvS&D_OU(R z>6~BO`bIPDO(7@Csa}sh#h5grWAe~C$A38Cgkfz4J zI@@>~^oJDe4eRDY$y{G8R@$)ned}7n)=-9BPC;zH#0fXfZ(Ic9wC$`khTo8+Ge>FM zY8uZ4-rABiO}J5CotS^LjKo1)RSx~*2KUDgL`Jtj##H<_m8mC11M`b?YQmVdR8QXL zc<<#45RX3n`Fb%6I^EQ$X2RKJP&?0*<aO^KnTbudwmwm$|n-zRlZM8;^tARq?$9tcN(2yXPb(--jWdNPn=ln{CzDV zh|I~cena{EZQmGgKnu(ga@=USIa|Ekf@hIyTdeMp3YgDE47F&YQ?a>~nXOXF|7dji0yCM^;m zbc4;?BUdai{NC_ENLs7NxZ))No-ZPI>+NN9tIBx(Qg_D6&C0GP2!okhX30ic_bcC8 zyTvH&%fHLpna%WmL#$zO&iZYvl|q+322J)bWs~PvTGcI{5$4;k>NNVu|Ge(W*yWg0 z80|mSX>c3M*5NhK3l*mgHvi?2s~2YNHoLW_d7jW(_12KIx5mbA99ux2NXrM0o}O>sfNEbf=T3mQRKzox$P9!ngbu<{a=kFeZZQPIcnHN`@G?}@3`jLUd)x^(S7AGHaK8SI1CdbcA zbPpG5V{CUim`7GPL$HdC4arjUW!CZJ$JFg@Y*lTf5eZ6!Qg~FXD2xL2;dcV{%`K;f z1=F0e*St|A#Yw-`8()4RK}Azi#zba+U}W0qJk73DAJ`7D%6#P6OyiFwyVso}^H^|q z^>vZeCQXR-nu%RmFGgs9_-*C%W$DWo>tV#vyDkX6@K57vCn=#s=hv@AZ;2c$2E=GK z@Z&2{ye#xQTx->+)&385R6^gfZ_PYxyJHeJA@-WM?yt(#_eDw@S_ZYb!D^eBT1p!7 z>>dW>EtdkOO~Sb{N{1zs%jdgk3Patfq%V7<$qzor z-EIz%z8bbhlzO5W^oH=tl}O6_GxbLP`KV61#;wMcCbESVCcP_2FT0_ZH-?nlF z`llpb&4>*jR{&Z__jc}@E2l5H3t5{!Li?w$=I?*r#Rx}yB@yiLH+|7}{&YbvPtL$k z?e=(BZ2eXFWfZ^87OpS3B!Gq=Xi15HZUdWI#3Ku0{)^Llw3wjp)$e=nQmDUNBMK^i zQ$NWj>oc-A(h{U0+{$lsJFhS6k0atLLH0Y62h8)uZ}fXQ<&?|975UftvUxvQY8&|F zv)k*Y%VWh)e?))?V^L%*F?D(ZjDIrVM;96WxR%J4piV6G<&@E(hR5J36}jXmc|<{o z%dP#-2hzv(`d+jt1P^x%mqeKLsI|EwuqKO~KA=Mi11+t@R_LvR1?}Yc3gMMP)1@uj zBIC>dG!%wsGZK!d%KDu@Z>y_$@wPzR`&y40BUB0bQh&v|hTvwkKrzsO=8; z-3??X4M*izX&dkFCoeF)e*&bLJ3Pkyt~|6;(VrT+J9UGXH0{TE;MMdQO8y;^P8L3) z08NCVrJU8`rl-ei8O7fO*vlyjWZDFYE(P89=WT%I2h}-a4eZ>%ZXbd!L^|~gAh9R~ z*i0Wi+V8)Vj8;6o65jHHQh2)YX*CIM;h(jR5Tm}^ec#vCXa1sn1q$70qzDvr$+KT! z56|UC5CYfaB`cz0p!tFFVmGVWyg#Q|%$>ty*VWxeH(sJs);H}qsZ`hP$A%pWs@)12 zBpSy+e+zoRA~LV&@xo z7KiqYV0=U#DQ?u}D-!#l*`kOA%K8N(c93*-)amo*IOc7+LBO;nzkeH#gpqt|p2^%h z)=bTTtW;^9jIp}HCCuRtBu4?$Z&+yK*)y8l04dq~pSxwN=m3ME@>~vAM~;WW*yC*N zKhHP%b|*cM6L&n90<(60_zX5 zq$y~X5kniszC;$?w}-jW8uw-o>tFlv zR&ZURZ7~G#Cp@sFd(x)b-Cy$+zFZvkSw?2&wzpkTCygacy-0Rvk-3T9-rjFl-$g_y ztsc~}bN(wyV>{@Mz6SyYq%gnvx}|HZSz8{9-mGhCI`JR7z5Vy(#ExUqcACtfjoCcY z9L$Cz1r^bdP1y3RxgaKmU7u4F9haHUDFI|64-()7tNY%(Hjn!92V<~wIfX%DxVt~|8hekv0z)x0 z3aXz#?Nl&8D#SyBxj`rGnDI{WyqJN)r2Qp$NT-^Pt9I5|p$?}Z5tysV_VX_FG$g0x zNRKGb2v;F@31&{7cZ4rG(3bB6ItN|;HTG=U0)o40+sfkz8UpdL_*Gg~Wl zU@PR6;;t&rO<@;rox#nNo=pLnqk%KYu^|xmurPt2=Kq<9asNF&wE9%MD;GCEt3;#E z6nBv{KuZ;laM$4J$nt$&Y~C=nA*;t7hP0l?~MJ*MGO`E}9M)D*zjx{7vvXxJcuD4C+@aNUwqPMN|?9^*m1Y;Gui*?peo$#{?l$^)dWLk|_b zWKW-8l=#V9Vd@Dx_rp2bYO zz1yf-bWk2?z=B5AJYQEai)YsY732n{dPAY%lu^H1!~;i06A{df)rtQErE*E%3=R$= zSBw~@J!mH#WG*<$#=rkaOc_^hL|eWzT>QUpj=9vE|LSZE=BF8NOx5H4sZZ>Co7SqV zwA8t0Ae~e!Qu?lKKzHqlqaGDi{qW9_Sfnmn3m3HX7N!RY4_=N*H2Fuh@0(utjS2cg zuLdF6wES;~MG~gf7v)Al8ww1fo6Ky+o5WB98%&SRzPc=vQ1YUaseVIY@WLk2=iBXl z;IFJS?8Wu1If=?dq|ZRfSFz7Z0v>!N$HgPuT6p|bLKqwf^;CZ}>GP^{kFjl+^`?I+ z*2+z$0U*c+ae$faP{kiCK`dc{tPQ87rJd=vM}Ix6y|>>uewJHBw;r{(J9mx?alqL- zyx}hOvr%0ddx89Y&*U$fLup6Xy$*qb^~o_B?R%3~-za1Z(kqMiXlQ8633A>s%Zggj zhrS*G!8zUpkBVLWD2zd}Yl7v?{`iu6_a!C2A?u29htB5`_%inDMyTs)vc~`|Z^FiN z;@~U6buYnNPS*4$I6|Dbc`*%2i{>J-MMd*2vW>kiu8K5t3L${MAD3l0J@>ARVUZBads<%mhuxFjTKVOI$x73RklI1F2W0*q((+=2 zXT=;vt5IAEMSq{+wkrcY9hy=#uvV;BcQqLquF@ZSHKetP2{h_&7TaokMyNWA%MO+0 z)Lw%uUR{kbQ}W$M`N>c(qbqM;HiljiewsgNu-E)IXlQ7t-nevP&(Ub%a0YWYWV8Sk z!dQJYkPjcvR`tC=-qw&UB)Xi$_x5U;qwD4x>%N~y-0-6;{B~#9U@jBnRww@79ZtUV z^XJcJY-f!J+hMev>0lm#O_tZd69QEz8R#Up1ebbrs&Ov4pWV*!@o`hQn7WA`n+VUV z9~Y$x1Edjv-|MPB6-kt!vs1G7S#T!?a6|K8^xKLOa9q~Wdt>Vv=g zH5aOz9xsUV%pyuwL;az|m%y|M{qOUGb8ChOgk!Tbtn`w*2= zNX{eXdsE!DqBs@!u+-1V@g^Vr7%mwTk0ChH<837;PX6S?6V+woEgP2BH9kImx0^CX zGG0O@mf1-p_Ry4%vMDpB0FOcEdSq z4dKO|z_vI~4;P9PE}<)8@M zq`wH0&0$`W>A&e=}!6pP@~B z3v31x2mIxS@AoU+O$V$*Mg{*hi35|Vgshha=7c?$Oz$9{34x$@^zA;QWuTfa~Tw?PcvAlFR}_nX$%gFPpW4EN_TNS`S7CyoK9W(gGS zPB8K_hi6H#;-JTtclA*QZ-BVY_TeF`nF9SYsTTMf$;9BEZkK*7XrD(KmqN1q{m0%} z>Umh6XbBzHR(EaX6Stzp9oH|8cIa^j>9*}gR6N9iWs`ShSK~&J_=KKqEt2&<9v*-1 zmBr{Ec%nW;pnKi6DTgFqt+l)^w;h_ixpWBA>_|QNGv|#vK~67OQ6Ijz#c}72nn_)n z*3V+7pgGWP2U@~0q2iGC-?#Je_C8n(%Zn8X{qd(Rn7`~DpJmUn148xLfX?U%p_?bX z=_{1W9cB!zs8829_%d0>m0<=%xbZxC^oaIO3xjP>eeS_E3n}cAKtc>ebawrqQ?rir z1oOqf80WYD_*;AfXtGCxU#b=mKq0V^)*ou{yA`-Ezx*smJznixMt-99u=<7wT+F0Y z*R5;4M);z@eXm;_p_-o;`X`^wis!J>+Q$AQKHi~QD3kk zWnPHSD+CiInwE-VRd7kKxYpX{+Ye43+EW8rn&rY5*NK!4i$9iMpNd>@-LXsmD9|-Sy>fb*uH_ayFRI(uXuI38 z(>-9`kHEDtJmasTxO{$~3pADBUBH4K1?9}oZk#k(@RI^OQ}I>9 z`65yMb#qigsNwr%?X&GlM%mQhSDD)M?k3p1eYmIYFYDw^*S3TyD=WJ&5&W)1sy&3N zY6%N}g1Mj;%j}u8aTjXDn``@9XhhT?&~|xodIC={FQ8?jQV4c|srgIY(CDJ|*5S8w zLHl2d@AtC{gLbNj*;{QjiK)%_bo(x+7yB=#nU8`HJUHLSfOWX?N1=G!-`_N#4Dh^# zhT`oHrA`77QdV|*(S7D)Y}b#6#X-Tl%Id*&lDf3w{qt*)-XZ@PukWt(T)DxUP7OUn z{bKAM*jZWY{qFt(GZ(G{pG417mL~ePvGXyh%&7ij^KM%dwT;@#km=Dn(|P@vhj}T* z!l(o*(w2sd;MZ>Ja`X&>>JgK>XnDfP)b4A+gI2zjJ8`N0R*FAc;YAIa-r!*TA`ord znLDjjClXYWDRd{@vQPE-z}4`Kg(@R1nxHGRyxl$P)7P#iK5?EHIjXlSkdDb@-ucn6 zSmI9!*o-&y1|FEN034y1c-A|R!))d6l*EoS`Eu?F6yUyJ3faD@jGNNjeSOIGH~Js*Gg|=w7RED)hz7)^&{U z_aPh7o_j3Kyqu0JbqM+Q&0N%#o&m7r-jBuUD&*{SLvW-O7B5w*?}p{Ri`j{ExRd{h zG|{7V^p|N^o>Nq57(2%8kD!Bd>Ec2a0#htM9XGYC4ZjoG=W&$QyXW<_>vZ?q&$pP) zot}z+h)TiBLC9)bVft?h##Q!nn8ykkm0FQLNfMc9m!QPIokK8(T&cF$=$}7hfH*Qw z<|ZcO@^tY+_G0&4*V9f|q)2!lM9|_NeQT@wBA6kuP*Ji;M&L;w)g@f z4!v-VGTtKAziSdEfXLR?BTTH05xskTN#-|yG<=}!Zm?<(-VGG_$s*bG7=U1nQ47G1 z=|aQTBd~(!sLD+CgJqv18$fx&aYNhck0Y2@XB-WMYY%H;sz&Wr__MXS*AlGV_IIuZ z1yo<}Ck6yhd?-<~*8;Y~MzgZ(eGW7I>&7RuwA)n<>vh$}DJkP>?ow2O{|RhJ7Iq(? zWqOB2d=OESMgkWD+XFu~L4!u9;$hj$2;X1})^%0?yu0gOSL3XIgkY;w?qgZlj3Z0Z z2`nK^(m)9A4p(*lZo53+QKh|QZLmLt?iX@M>2}C*hfU2yb%~p+LH}7%tMV^BT|Vre z%SChXh|7p-6e~T=ek^37il|kZ>tAUrZddvG*)(BXaXPH-UN3P@lfRwt&T=6vFe+b zmnEH_et7x{SGEf!u&i~kzEtYMu4oF}5P~mHCzN#AFx`_L1rUFN&}Y4TM9Y{iftn(% zaCfF(3dvcC-5a-MEyeGF#hBI|W8*N6PX*+{FVVI)3^J#r>8E~*b(W-tive^M&o#%U@1zr9PQETg8uD~@{c>cYs zkoz?gu>Q^Tu9wD-Km4~dzS{Vdm`d?+7jBMqD-+C}w>qWe;fp@}4fIOyuK&)&F)b?-r+_mbfyLoIyjy*+q?T%}hkL7XBuWb?vZ!hCqnf zMGI-9p2PC6zx%r?>3d|6s~TZ#t4Ds*csW??)r8x)s_LG7^}pmsl@$pYnFW3seV=m` z5ejw9mOL)JSI4wov$rS-(%jIjA1TdI4+k@w0}w6dVMB^uV6sHQo8L z&Ww#F?w^wqdS|jW0CJ~P>d0U;TDa56-s$vTr=%q@+ z9~L0j&>=wlpoqS1#M!5dg}K~aaDiqQ))M@>j4m%Lli>;t|8u(2R<7@C7BjrQs{3lWb;|31Gvq>oTERE+1d{iQyDO>|;26?rKcJT3c*U`%K)nLI^L{22p>~oDYce-phH6mo_slgI{_W&dUc; zaDixhEkHM`6EHw0HL7HGPu?}2sVhE3eF(Mh+bGn2H~ykn8)acTwt~xC20Qo`yg3nW z1>$Y=9lb-{6x=@O(4Y2jX4cK zR^nNFum>5}OB!YN-jcuv8lwKsgsZr0VPXh4*I*hFMEwz7$M&5UacNPMVgUm^tR^#G z-@`UCHHpnvGNd#M1r((LA5gk=D8NhVSjA5qWVH0S6@UM={9rU+MQ5)}Fp&y2lm@(X zh=cncd&g;k!Mv#+%tw<%Z6P`JwrZKAfLW#W3tr3Tmfo*vcBu-!q~8 z`{%opH;nvn{?e^mA6Li~E}GaJ-6(|cdeoi=BA zAPXWpurmu(-~51C_iW$$$N&s7z}C!Y-XpRJpcU_8zDt7rC5FTM%jZ@|yc*R?KeACL zp^0u%e6_Dgg}6xnu`>a_1E#@4P;wJ$y!2L8^>p*rM&h`#`&3zG)KH7q&WkRbB@S3C z%%y4+?A;@i>_Ww{#!myV_8IL|e;Tm!KKTKiLtk~RT?aP8+{&xQoyW>t4AWs-(NRL* zenQH54z8E}pIz&=Q#LbRdMp)x>ikm0RW|6PMyEfNwC>(dR3`k)y($15Lgd_6oa>EB z>j$zptjBfq1f;!IHS6X?WZKLCcXLA)y>L8fN!}YhZx*5G?%DRwV0z}aU!DJpu-u#b$1U9V{%%;NEjE;aZzZC$O+6{zLent;AT~91 zcz2$bz^?*FaNzvEGpDhhk@e{j;bO5-_0tX`+rO{VcGpl!Z725H*7|jI7&~9r3v+5*p?OpIy z6%=wvQ#=fg9;04YeJZ+f=K#o^9u5T4!lLzY)qQ&dkUx4lEOjFr?E`@?$E9b!>V1hkl@)#Dl z2kQVh0~<31B-q-Wn|2nwb(YeheIEEyQ-JP>oGp2V2MU#i#oNNt+JG{9bk-(Md%&!g z%~+2|glQIVV|x<3L*C|1Br+;%UC_m4Vrbs0UJNoIglUu5S)iFYapgrvBo8WX;}Gly zoNZu7H2kLrdNiwKPAjCtWi#_os6Y|C$8{6^^jv_HMZm~cMU94rhM7`we07{&gf!Fh zQE))6tXlW^AZ(e>H@@OHfJWaF^q1M%^|*C3gmlBh{=o!zi3@}2^JTZbv?Nu>)u3@I z`r$=t^=;$=sZ9btY2lg`dUj(2WjP1&vIB<-5{?#%mSj4u{Jy>gsP+(kT(Hx-tAPaL z0`bzqPnawN6n=Rh;8x|e;ESZ#MOeo1g!&D|UkuxFi*~;rHFtz}5{!;gf2}aq{`Lkz0(L2bPFsNdl<$Ske#{eJ73#l<{laweUYtMBWRKAFP_;VX1L!C>Epz|ko$SR_!w-8x4?MwY z?DB@nQZsca01gYL-a^nHxi44aHCA>fk-m5q*sO#l9BcF}FvaVh&q4jz#FYL}cR~-6 zc*xfd-c&LRfIe-oIbt?i;81vH=S3e$!-!=NOyG7>dfP~`=M>NpHd^m#F(+p)cx%fG z=o}pA34HN{O5D@PqVzY+z5}*7iaqj3un#G?R}45Fp?Ii46aFCs2>j4CTyOzVzmg*g zwTIz}c3gPr*RX%L5F-MhAaW{r>$Lccby0UQbh<^U~cH;eItXt{qz9Y84I=7f-o)trPXyR$1q0!JU9 zxxUB0ILDCzn?(Sng26;+X|PX1Re+Uu=GMx30AQN|7~;lh$2#-S&F^3vZ0a*i1sJ}t zf)~EqsbG(nU|3BWHPkN!AF-hO^dT}h_(B*rkLFoTOZpejLj)I2zYO+oh?VWbL;y0` zfPc9S|BIUdYJP$B&dEg+cU~;xB4mS2>1*&211(#kyHWuvQ?5$1gvD(`uXsjr=K;JA zSR(-h&=Gw7L;Wx5^|(yQeop|%stqP~yE>Oo;H(R@aH4K^9wZ#oHt@k4YRHDqb6Jaud*13w z!#9UK-%N%vQv$UDf_BorOz%=Xo{&LjYHM^k{J|x%?NMC)zUT9oeL14yODyMLpu}d# z`KEd1m!aGpw*A5Oz`TD9*mdD2jH}LyIX_pkrk|5%2zl*>s;{dZ zzH@(Yh!^{C{8({<()rE>0`*VT{Vd6XsG*qj?cmT*HKSMv$Ey1t$4;hf9QFW?e)3l* z=tvuY=*$(P<IwH zj<%I&TZ{X)VOTPd>2{2o@+9i$@g-1Qy%cpMviZK#cG-H7)OMCf9OE*cujjeBRHygKTiUt5vHTt`@m_xZ_b#|9nC4 zVFVgzrOkhIX<7j@dB?XRSLdojE_XsMXD@qXFWe30VAK(6k1a$c_Jw1Pd}x2n>k_Ds zYwa`yaa%k!9Q#FU9X=wvBYyQ%bG+h=)MdrRl^TUh05hpt$q2;}=4Dsjx5U&2KUCOj zzsW+u6g!Lfjp~>mL*lPwi+vWyl~!P&75Wp-lAyL3XBSn>!;{QUGq}5UmjC9pq4t1a z-=<92w~$27Xb$R%d_STXthR>SO8pyv4g3K?yQuMW;hW&VdoJT&Rk2*uN0AXhp89@# zmq%@vrXP_8xb5hOHzO3`Cej%a;*8rs3-q~P!pNJHf}UL;V3_% zN~pN|r%$-^lP;l>q4mD89<6o4JZnTItY09GvKxXy^aT?Ih>kH`{@>(L9->bI$qEot zUcte^;zQ%Nzwyrwg6qaie^Zf`c(vrykG%s#dg_Kq9q;oIPV6VPACI%@>%Flmo|7#w z?G{$o6Q#07Ji#1i`u~o11(!iVSiySHOh3R8(&V)WADf`gbA;&iN>KU}k2-BjcxRqbE^TIY@Pvs*;#J>{p|@n4 zk00-gOsTSm$5lFdqM_+;|GVmGKSRknd`m+W!l=G{51R4=rPFbLnMc3( z1p%J1TFG>M^}OUx-x|?tCaeVoDHS4#cwQ8o2>wPfI^_lO*jOJ9DWGb%5v4v!M?#c+ z5>E~cb;bofoo)88mo4-}3!|J^Uo1aXM4XG7HN1h4)~j1N5BeHuEWoi-0icdR)aihs r`nX8}H-6u2*ln=b|KI$VTz84LdgXe2R0snbyl53gjfZ6qEW-XDcE=;= diff --git a/Projects/Shared/Util/Sources/RelativeTimeFormatter.swift b/Projects/Shared/Util/Sources/RelativeTimeFormatter.swift index f3df8be3..caa09f7c 100644 --- a/Projects/Shared/Util/Sources/RelativeTimeFormatter.swift +++ b/Projects/Shared/Util/Sources/RelativeTimeFormatter.swift @@ -22,23 +22,55 @@ import Foundation /// let text = formatter.displayText(from: "2026-02-09T11:41:48Z") /// ``` public struct RelativeTimeFormatter { + /// 상대 시간 포매터를 생성합니다. + /// + /// ## 사용 예시 + /// ```swift + /// let formatter = RelativeTimeFormatter() + /// ``` public init() { } - + + /// ISO8601 형식의 업로드 시간 문자열을 상대 시간 텍스트로 변환합니다. + /// + /// 소수점 초가 포함된 ISO8601 문자열과 일반 ISO8601 문자열을 모두 지원합니다. + /// 값이 없거나 빈 문자열이면 빈 문자열을 반환하고, 날짜 변환에 실패하면 원본 문자열을 반환합니다. + /// + /// - Parameter raw: 변환할 ISO8601 형식의 날짜 문자열입니다. + /// - Returns: 현재 시간을 기준으로 계산한 상대 시간 텍스트입니다. + /// + /// ## 사용 예시 + /// ```swift + /// let formatter = RelativeTimeFormatter() + /// let text = formatter.displayText(from: "2026-02-09T11:41:48Z") + /// ``` public func displayText(from raw: String?) -> String { guard let raw, !raw.isEmpty else { return "" } - + let isoWithFractional = ISO8601DateFormatter() isoWithFractional.formatOptions = [.withInternetDateTime, .withFractionalSeconds] - + let iso = ISO8601DateFormatter() iso.formatOptions = [.withInternetDateTime] - + let date = isoWithFractional.date(from: raw) ?? iso.date(from: raw) - + guard let date else { return raw } - + return displayText(from: date) + } + + /// 날짜를 현재 시간 기준의 상대 시간 텍스트로 변환합니다. + /// + /// - Parameter date: 변환할 날짜입니다. + /// - Returns: `방금 전`, `N분 전`, `N시간 전`, `N일 전` 형식의 상대 시간 텍스트입니다. + /// + /// ## 사용 예시 + /// ```swift + /// let formatter = RelativeTimeFormatter() + /// let text = formatter.displayText(from: Date()) + /// ``` + public func displayText(from date: Date) -> String { let now = Date() let seconds = max(0, now.timeIntervalSince(date)) let minutes = Int(seconds / 60) From 607098a6b68d31fca08d416db5dd1c41c4926b34 Mon Sep 17 00:00:00 2001 From: jihun Date: Sat, 9 May 2026 11:29:39 +0900 Subject: [PATCH 17/44] =?UTF-8?q?refactor:=20CoreAnalytics=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EA=B0=9C=EC=84=A0=20-=20#281=20-=20CoreAnalytics?= =?UTF-8?q?=EC=97=90=EC=84=9C=20Feature=EC=B1=85=EC=9E=84=EC=9D=84=20?= =?UTF-8?q?=EA=B0=96=EA=B3=A0=EC=9E=88=EB=8D=98=20=EA=B8=B0=EC=A1=B4=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=EC=97=90=EC=84=9C=20Evetn=EB=A5=BC=20protoco?= =?UTF-8?q?l=EB=A1=9C=20=EC=B6=94=EC=83=81=ED=99=94=20-=20logEvent=20?= =?UTF-8?q?=ED=81=B4=EB=A1=9C=EC=A0=80=EB=A1=9C=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Analytics/Interface/Sources/AnalyticsEvent.swift | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/Projects/Core/Analytics/Interface/Sources/AnalyticsEvent.swift b/Projects/Core/Analytics/Interface/Sources/AnalyticsEvent.swift index b799de9c..a459ffa4 100644 --- a/Projects/Core/Analytics/Interface/Sources/AnalyticsEvent.swift +++ b/Projects/Core/Analytics/Interface/Sources/AnalyticsEvent.swift @@ -8,16 +8,6 @@ import Foundation /// 분석 도구로 전송할 이벤트가 따라야 하는 공통 인터페이스입니다. -/// -/// ## 사용 예시 -/// ```swift -/// enum HomeAnalyticsEvent: AnalyticsEvent { -/// case homeViewed -/// -/// var name: String { "home_viewed" } -/// var parameters: [String: Any]? { nil } -/// } -/// ``` public protocol AnalyticsEvent { var name: String { get } var parameters: [String: Any]? { get } From f0017e9e64aff86b52ce7530baa85b83e808f223 Mon Sep 17 00:00:00 2001 From: jihun Date: Sun, 10 May 2026 22:41:21 +0900 Subject: [PATCH 18/44] =?UTF-8?q?fix:=20=ED=99=88=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=EC=83=81=EB=8B=A8=ED=83=AD=20->=20?= =?UTF-8?q?=ED=86=B5=EA=B3=84=EC=83=81=EC=84=B8=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=ED=95=98=EA=B8=B0=20=EC=95=88=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EC=9D=B4=EC=8A=88=20=EC=88=98=EC=A0=95=20-=20#285?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Feature/Home/Sources/Root/HomeCoordinator+Impl.swift | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Projects/Feature/Home/Sources/Root/HomeCoordinator+Impl.swift b/Projects/Feature/Home/Sources/Root/HomeCoordinator+Impl.swift index 1c7d97fd..1647b454 100644 --- a/Projects/Feature/Home/Sources/Root/HomeCoordinator+Impl.swift +++ b/Projects/Feature/Home/Sources/Root/HomeCoordinator+Impl.swift @@ -66,6 +66,15 @@ extension HomeCoordinator { case .statsDetail(.delegate(.navigateBack)): popLastRoute(&state.routes) return .none + + case let .statsDetail(.delegate(.goToGoalEdit(goalId))): + state.routes.append(.makeGoal) + state.makeGoal = .init( + category: .custom, + mode: .edit, + editingGoalId: goalId + ) + return .none case .statsDetail(.onDisappear): state.statsDetail = nil From 9b2ee06a7889d2d22aed3d03166dad93d20c389f Mon Sep 17 00:00:00 2001 From: jihun Date: Mon, 11 May 2026 12:25:16 +0900 Subject: [PATCH 19/44] =?UTF-8?q?chore:=20stroke=20=EC=96=87=EC=9D=80=20ic?= =?UTF-8?q?on=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=97=90=EC=85=8B=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20-=20#285?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../icon_book_thin.imageset/Contents.json | 15 +++++++++++ .../icon_book_thin.svg | 11 ++++++++ .../icon_clean_thin.imageset/Contents.json | 15 +++++++++++ .../icon_clean_thin.svg | 19 ++++++++++++++ .../icon_default_thin.imageset/Contents.json | 15 +++++++++++ .../icon_default_thin.svg | 16 ++++++++++++ .../icon_exercise_thin.imageset/Contents.json | 15 +++++++++++ .../icon_exercise_thin.svg | 7 ++++++ .../icon_health_thin.imageset/Contents.json | 15 +++++++++++ .../icon_health_thin.svg | 21 ++++++++++++++++ .../Contents.json | 15 +++++++++++ .../icon_heart_thin.svg | 4 +++ .../icon_laptop_thin.imageset/Contents.json | 15 +++++++++++ .../icon_laptop_thin.svg | 25 +++++++++++++++++++ .../icon_pencil_thin.imageset/Contents.json | 15 +++++++++++ .../icon_pencil_thin.svg | 15 +++++++++++ .../Sources/Resources/Image/Images.swift | 8 ++++++ 17 files changed, 246 insertions(+) create mode 100644 Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_book_thin.imageset/Contents.json create mode 100644 Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_book_thin.imageset/icon_book_thin.svg create mode 100644 Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_clean_thin.imageset/Contents.json create mode 100644 Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_clean_thin.imageset/icon_clean_thin.svg create mode 100644 Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_default_thin.imageset/Contents.json create mode 100644 Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_default_thin.imageset/icon_default_thin.svg create mode 100644 Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_exercise_thin.imageset/Contents.json create mode 100644 Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_exercise_thin.imageset/icon_exercise_thin.svg create mode 100644 Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_health_thin.imageset/Contents.json create mode 100644 Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_health_thin.imageset/icon_health_thin.svg create mode 100644 Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_heartDouble_thin.imageset/Contents.json create mode 100644 Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_heartDouble_thin.imageset/icon_heart_thin.svg create mode 100644 Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_laptop_thin.imageset/Contents.json create mode 100644 Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_laptop_thin.imageset/icon_laptop_thin.svg create mode 100644 Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_pencil_thin.imageset/Contents.json create mode 100644 Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_pencil_thin.imageset/icon_pencil_thin.svg diff --git a/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_book_thin.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_book_thin.imageset/Contents.json new file mode 100644 index 00000000..abee8f7f --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_book_thin.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icon_book_thin.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_book_thin.imageset/icon_book_thin.svg b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_book_thin.imageset/icon_book_thin.svg new file mode 100644 index 00000000..d2343c47 --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_book_thin.imageset/icon_book_thin.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_clean_thin.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_clean_thin.imageset/Contents.json new file mode 100644 index 00000000..bc9626de --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_clean_thin.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icon_clean_thin.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_clean_thin.imageset/icon_clean_thin.svg b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_clean_thin.imageset/icon_clean_thin.svg new file mode 100644 index 00000000..906382cf --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_clean_thin.imageset/icon_clean_thin.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_default_thin.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_default_thin.imageset/Contents.json new file mode 100644 index 00000000..4f40b08d --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_default_thin.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icon_default_thin.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_default_thin.imageset/icon_default_thin.svg b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_default_thin.imageset/icon_default_thin.svg new file mode 100644 index 00000000..3a9215d1 --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_default_thin.imageset/icon_default_thin.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_exercise_thin.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_exercise_thin.imageset/Contents.json new file mode 100644 index 00000000..2fc144a9 --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_exercise_thin.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icon_exercise_thin.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_exercise_thin.imageset/icon_exercise_thin.svg b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_exercise_thin.imageset/icon_exercise_thin.svg new file mode 100644 index 00000000..cb41f5d4 --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_exercise_thin.imageset/icon_exercise_thin.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_health_thin.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_health_thin.imageset/Contents.json new file mode 100644 index 00000000..446fccf5 --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_health_thin.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icon_health_thin.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_health_thin.imageset/icon_health_thin.svg b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_health_thin.imageset/icon_health_thin.svg new file mode 100644 index 00000000..5d206578 --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_health_thin.imageset/icon_health_thin.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_heartDouble_thin.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_heartDouble_thin.imageset/Contents.json new file mode 100644 index 00000000..737371af --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_heartDouble_thin.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icon_heart_thin.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_heartDouble_thin.imageset/icon_heart_thin.svg b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_heartDouble_thin.imageset/icon_heart_thin.svg new file mode 100644 index 00000000..af4928bd --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_heartDouble_thin.imageset/icon_heart_thin.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_laptop_thin.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_laptop_thin.imageset/Contents.json new file mode 100644 index 00000000..456036f0 --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_laptop_thin.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icon_laptop_thin.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_laptop_thin.imageset/icon_laptop_thin.svg b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_laptop_thin.imageset/icon_laptop_thin.svg new file mode 100644 index 00000000..336bc3be --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_laptop_thin.imageset/icon_laptop_thin.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_pencil_thin.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_pencil_thin.imageset/Contents.json new file mode 100644 index 00000000..bd624508 --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_pencil_thin.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icon_pencil_thin.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_pencil_thin.imageset/icon_pencil_thin.svg b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_pencil_thin.imageset/icon_pencil_thin.svg new file mode 100644 index 00000000..ccaa8ff6 --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Illustration/icon_pencil_thin.imageset/icon_pencil_thin.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/Projects/Shared/DesignSystem/Sources/Resources/Image/Images.swift b/Projects/Shared/DesignSystem/Sources/Resources/Image/Images.swift index f0e81d7b..2c927ac1 100644 --- a/Projects/Shared/DesignSystem/Sources/Resources/Image/Images.swift +++ b/Projects/Shared/DesignSystem/Sources/Resources/Image/Images.swift @@ -50,6 +50,14 @@ public extension Image.Icon.Illustration { static let health = IllustrationAsset.iconHealth.swiftUIImage static let heartDouble = IllustrationAsset.iconHeartDouble.swiftUIImage static let laptop = IllustrationAsset.iconLaptop.swiftUIImage + static let defaultThin = IllustrationAsset.iconDefaultThin.swiftUIImage + static let cleanThin = IllustrationAsset.iconCleanThin.swiftUIImage + static let bookThin = IllustrationAsset.iconBookThin.swiftUIImage + static let pencilThin = IllustrationAsset.iconPencilThin.swiftUIImage + static let healthThin = IllustrationAsset.iconHealthThin.swiftUIImage + static let heartDoubleThin = IllustrationAsset.iconHeartDoubleThin.swiftUIImage + static let laptopThin = IllustrationAsset.iconLaptopThin.swiftUIImage + static let exerciseThin = IllustrationAsset.iconExerciseThin.swiftUIImage static let profile = IllustrationAsset.iconProfile.swiftUIImage static let modalWarning = IllustrationAsset.iconModalWarning.swiftUIImage } From f9a82c51af97ba613cc583036334c132c2c46df3 Mon Sep 17 00:00:00 2001 From: jihun Date: Mon, 11 May 2026 12:25:44 +0900 Subject: [PATCH 20/44] =?UTF-8?q?fix:=20=EB=AA=A9=ED=91=9C=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=96=87=EC=9D=80=20=EC=95=84=EC=9D=B4=EC=BD=98=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20-?= =?UTF-8?q?=20#285?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Feature/MakeGoal/Sources/MakeGoalView.swift | 2 +- .../Sources/Resources/Image/GoalIcon.swift | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Projects/Feature/MakeGoal/Sources/MakeGoalView.swift b/Projects/Feature/MakeGoal/Sources/MakeGoalView.swift index 2a8ea549..46709366 100644 --- a/Projects/Feature/MakeGoal/Sources/MakeGoalView.swift +++ b/Projects/Feature/MakeGoal/Sources/MakeGoalView.swift @@ -98,7 +98,7 @@ private extension MakeGoalView { } var emojiCircle: some View { - store.selectedEmoji.image + store.selectedEmoji.thinImage .resizable() .frame(width: 64, height: 64) .padding(22) diff --git a/Projects/Shared/DesignSystem/Sources/Resources/Image/GoalIcon.swift b/Projects/Shared/DesignSystem/Sources/Resources/Image/GoalIcon.swift index 51a7bf05..d254f728 100644 --- a/Projects/Shared/DesignSystem/Sources/Resources/Image/GoalIcon.swift +++ b/Projects/Shared/DesignSystem/Sources/Resources/Image/GoalIcon.swift @@ -35,4 +35,17 @@ public extension GoalIcon { case .laptop: .Icon.Illustration.laptop } } + + var thinImage: Image { + switch self { + case .default: .Icon.Illustration.defaultThin + case .clean: .Icon.Illustration.cleanThin + case .exercise: .Icon.Illustration.exerciseThin + case .book: .Icon.Illustration.bookThin + case .pencil: .Icon.Illustration.pencilThin + case .health: .Icon.Illustration.healthThin + case .heartDouble: .Icon.Illustration.heartDoubleThin + case .laptop: .Icon.Illustration.laptopThin + } + } } From 500360510aadc8601ee07de1c6341e7d41e257de Mon Sep 17 00:00:00 2001 From: jihun Date: Mon, 11 May 2026 12:40:22 +0900 Subject: [PATCH 21/44] =?UTF-8?q?fix:=20modal=20padding=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20=EC=95=84=EC=9D=B4=EC=BD=98=20storke=20?= =?UTF-8?q?=EB=8C=80=EC=9D=91=20-=20#285?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Feature/Home/Sources/Goal/EditGoalListReducer+Impl.swift | 2 +- .../Components/Modal/Content/TXInfoModalContent.swift | 2 +- .../DesignSystem/Sources/Components/Modal/TXModalView.swift | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Projects/Feature/Home/Sources/Goal/EditGoalListReducer+Impl.swift b/Projects/Feature/Home/Sources/Goal/EditGoalListReducer+Impl.swift index acb49c4d..4cea0869 100644 --- a/Projects/Feature/Home/Sources/Goal/EditGoalListReducer+Impl.swift +++ b/Projects/Feature/Home/Sources/Goal/EditGoalListReducer+Impl.swift @@ -175,7 +175,7 @@ extension EditGoalListReducer { GoalEditCardItem( id: $0.id, goalName: $0.title, - iconImage: GoalIcon(from: $0.goalIcon).image, + iconImage: GoalIcon(from: $0.goalIcon).thinImage, repeatCycle: $0.repeatCycle?.text ?? "", startDate: $0.startDate?.dateDisplayString ?? "", endDate: $0.endDate?.dateDisplayString ?? "미설정" diff --git a/Projects/Shared/DesignSystem/Sources/Components/Modal/Content/TXInfoModalContent.swift b/Projects/Shared/DesignSystem/Sources/Components/Modal/Content/TXInfoModalContent.swift index e27004f3..b615b7f2 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/Modal/Content/TXInfoModalContent.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/Modal/Content/TXInfoModalContent.swift @@ -49,7 +49,7 @@ private extension TXInfoModalContent { enum Constants { static let vStackSpacing: CGFloat = 0 static let imageSize = CGSize(width: 60, height: 60) - static let imageTopPadding = Spacing.spacing8 + static let imageTopPadding = Spacing.spacing10 static let titleTopPadding = Spacing.spacing7 static let subtitleTopPadding = Spacing.spacing5 static let titleTypography = TypographyToken.t1_18eb diff --git a/Projects/Shared/DesignSystem/Sources/Components/Modal/TXModalView.swift b/Projects/Shared/DesignSystem/Sources/Components/Modal/TXModalView.swift index 9b5b9ddb..746a916d 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/Modal/TXModalView.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/Modal/TXModalView.swift @@ -51,9 +51,10 @@ struct TXModalView: View { modalContent actionButtons } - .frame(width: Constants.width) + .frame(maxWidth: .infinity) .background(Constants.backgroundColor) .clipShape(RoundedRectangle(cornerRadius: Constants.radius)) + .padding(.horizontal, Constants.rootVStackHorizontalPadding) } } } @@ -61,7 +62,7 @@ struct TXModalView: View { // MARK: - Constants private enum Constants { static let rootVStackSpacing: CGFloat = 0 - static let width: CGFloat = 350 + static let rootVStackHorizontalPadding: CGFloat = Spacing.spacing8 static let backgroundColor: Color = Color.Common.white static let radius: CGFloat = 20 } From e5931d46fc40fad07f3db62be13055ecf316ced4 Mon Sep 17 00:00:00 2001 From: jihun Date: Mon, 11 May 2026 13:00:54 +0900 Subject: [PATCH 22/44] =?UTF-8?q?fix:=20=EC=9D=B8=EC=A6=9D=EC=83=B7=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20reacotionBar=20=EA=B7=B8=EB=A6=BC=EC=9E=90?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20-=20#285?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Detail/ReactionBarView.swift | 76 ++++++++++++------- 1 file changed, 47 insertions(+), 29 deletions(-) diff --git a/Projects/Feature/GoalDetail/Sources/Detail/ReactionBarView.swift b/Projects/Feature/GoalDetail/Sources/Detail/ReactionBarView.swift index c10e23de..fa9095fd 100644 --- a/Projects/Feature/GoalDetail/Sources/Detail/ReactionBarView.swift +++ b/Projects/Feature/GoalDetail/Sources/Detail/ReactionBarView.swift @@ -25,37 +25,14 @@ struct ReactionBarView: View { var body: some View { GeometryReader { proxy in - HStack(spacing: 0) { - ForEach(ReactionEmoji.allCases, id: \.self) { emoji in - Button { - onSelect(emoji) - flyingReactionEmitter.emit( - emoji: emoji, - config: .reactionBar(width: proxy.size.width) - ) - } label: { - emoji.image - .padding(.horizontal, 8) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(selectedEmoji == emoji ? Color.Gray.gray300 : Color.clear) - - if emoji != ReactionEmoji.allCases.last { - Rectangle() - .frame(width: 1) - } - } + ZStack(alignment: .top) { + shadowView(proxy: proxy) + .offset(y: 10) + + reactionBar(proxy: proxy) } - .frame(width: proxy.size.width, height: proxy.size.height) } - .frame(maxWidth: .infinity) - .frame(height: 67) - .background(Color.Gray.gray100) - .clipShape(.capsule) - .overlay( - Capsule() - .stroke(Color.black, lineWidth: 1) - ) + .frame(height: 77) .overlay(alignment: .bottomLeading) { FlyingReactionOverlay( reactions: flyingReactionEmitter.reactions, @@ -65,6 +42,47 @@ struct ReactionBarView: View { } } +// MARK: - SubViews + +private extension ReactionBarView { + func shadowView(proxy: GeometryProxy) -> some View { + Color.Gray.gray200 + .frame(width: proxy.size.width, height: 67) + .clipShape(.capsule) + } + + func reactionBar(proxy: GeometryProxy) -> some View { + HStack(spacing: 0) { + ForEach(ReactionEmoji.allCases, id: \.self) { emoji in + Button { + onSelect(emoji) + flyingReactionEmitter.emit( + emoji: emoji, + config: .reactionBar(width: proxy.size.width) + ) + } label: { + emoji.image + .padding(.horizontal, 8) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(selectedEmoji == emoji ? Color.Gray.gray300 : Color.clear) + + if emoji != ReactionEmoji.allCases.last { + Rectangle() + .frame(width: 1) + } + } + } + .background(Color.Gray.gray100) + .frame(width: proxy.size.width, height: 68) + .clipShape(.capsule) + .overlay( + Capsule() + .stroke(Color.Gray.gray500, lineWidth: 1) + ) + } +} + private extension ReactionBarView { static func reactionBarConfig(width: CGFloat) -> FlyingReactionConfig { let minX: CGFloat = 8 From 6715095869b7071497b430f3df641f778768fc26 Mon Sep 17 00:00:00 2001 From: jihun Date: Mon, 11 May 2026 13:06:49 +0900 Subject: [PATCH 23/44] =?UTF-8?q?fix:=20=EC=B0=8C=EB=A5=B4=EA=B8=B0=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=20UI=20=EC=88=98=EC=A0=95=20-=20#285?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Components/Button/Round/TXRoundButton.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Projects/Shared/DesignSystem/Sources/Components/Button/Round/TXRoundButton.swift b/Projects/Shared/DesignSystem/Sources/Components/Button/Round/TXRoundButton.swift index 2f3814e5..86a115ee 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/Button/Round/TXRoundButton.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/Button/Round/TXRoundButton.swift @@ -110,8 +110,7 @@ private extension TXButtonShape.TXRoundSize { var yOffset: CGFloat { switch self { - case .l, .m: 4 - case .s: 1 + case .s, .l, .m: 4 } } From ba448194a11ea4be608e06020bc2705153179fa3 Mon Sep 17 00:00:00 2001 From: jihun Date: Mon, 11 May 2026 15:31:14 +0900 Subject: [PATCH 24/44] =?UTF-8?q?feat:=20TXButton=20Round=20illust=20disab?= =?UTF-8?q?led=20=EA=B5=AC=ED=98=84=20-=20#286?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Button/Round/TXRoundButton.swift | 89 +++++++++++++------ .../Components/Button/TXButtonShape.swift | 1 + 2 files changed, 61 insertions(+), 29 deletions(-) diff --git a/Projects/Shared/DesignSystem/Sources/Components/Button/Round/TXRoundButton.swift b/Projects/Shared/DesignSystem/Sources/Components/Button/Round/TXRoundButton.swift index 86a115ee..cbcebbb3 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/Button/Round/TXRoundButton.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/Button/Round/TXRoundButton.swift @@ -12,27 +12,32 @@ struct TXRoundButton: View { let onTap: () -> Void public var body: some View { - if case let .round(style, size, _) = shape { - Button(action: onTap) { + if case let .round(style, size, state) = shape { + Button { + if state != .disabled { + onTap() + } + } label: { ZStack { Capsule() - .fill(style.backgroundColor) + .fill(style.backgroundColor(state: state)) .frame(maxWidth: size.frameWidth) - .frame(height: size.backgroundHeight) - .padding(.top, size.yOffset) + .frame(height: size.backgroundHeight(state: state)) + .padding(.top, size.bottomYOffset(state: state)) Text(style.text) .typography(size.typography) - .foregroundStyle(style.fontColor) + .foregroundStyle(style.fontColor(state: state)) .frame(maxWidth: size.frameWidth) .frame(height: size.foregroundHeight) .insideBorder( - style.borderColor, + style.borderColor(state: state), shape: .capsule, lineWidth: size.borderWidth ) - .background(style.foregroundColor, in: .capsule) + .background(style.foregroundColor(state: state), in: .capsule) } + .padding(.top, size.topYOffset(state: state)) } .buttonStyle(.plain) } else { @@ -49,31 +54,39 @@ private extension TXButtonShape.TXRoundStyle { } } - var foregroundColor: Color { - switch self { - case .illustLight: Color.Common.white - case .lillustDark: Color.Gray.gray500 + func foregroundColor(state: TXButtonShape.TXRoundState) -> Color { + switch (self, state) { + case (.illustLight, .standard): Color.Common.white + case (.lillustDark, .standard): Color.Gray.gray500 + case (.illustLight, .disabled): Color.Gray.gray50 + case (.lillustDark, .disabled): .clear } } - var backgroundColor: Color { - switch self { - case .illustLight: Color.Gray.gray500 - case .lillustDark: Color.Common.white + func backgroundColor(state: TXButtonShape.TXRoundState) -> Color { + switch (self, state) { + case (.illustLight, .standard): Color.Gray.gray500 + case (.lillustDark, .standard): Color.Common.white + case (.illustLight, .disabled): Color.Gray.gray200 + case (.lillustDark, .disabled): .clear } } - var fontColor: Color { - switch self { - case .illustLight: Color.Gray.gray500 - case .lillustDark: Color.Common.white + func fontColor(state: TXButtonShape.TXRoundState) -> Color { + switch (self, state) { + case (.illustLight, .standard): Color.Gray.gray500 + case (.lillustDark, .standard): Color.Common.white + case (.illustLight, .disabled): Color.Gray.gray200 + case (.lillustDark, .disabled): .clear } } - var borderColor: Color { - switch self { - case .illustLight: Color.Gray.gray500 - case .lillustDark: Color.Common.white + func borderColor(state: TXButtonShape.TXRoundState) -> Color { + switch (self, state) { + case (.illustLight, .standard): Color.Gray.gray500 + case (.lillustDark, .standard): Color.Common.white + case (.illustLight, .disabled): Color.Gray.gray200 + case (.lillustDark, .disabled): .clear } } } @@ -101,16 +114,35 @@ private extension TXButtonShape.TXRoundSize { } } - var backgroundHeight: CGFloat { + func backgroundHeight(state: TXButtonShape.TXRoundState) -> CGFloat { switch self { case .l, .m: 70 - case .s: 31 + + case .s: + switch state { + case .standard: 31 + case .disabled: 28 + } } } - var yOffset: CGFloat { + func bottomYOffset(state: TXButtonShape.TXRoundState) -> CGFloat { switch self { - case .s, .l, .m: 4 + case .s, .l, .m: + switch state { + case .standard: 4 + case .disabled: 1 + } + } + } + + func topYOffset(state: TXButtonShape.TXRoundState) -> CGFloat { + switch self { + case .s, .l, .m: + switch state { + case .standard: 0 + case .disabled: 3 + } } } @@ -122,7 +154,6 @@ private extension TXButtonShape.TXRoundSize { } } - #Preview { VStack(alignment: .leading, spacing: Spacing.spacing7) { Text("Round") diff --git a/Projects/Shared/DesignSystem/Sources/Components/Button/TXButtonShape.swift b/Projects/Shared/DesignSystem/Sources/Components/Button/TXButtonShape.swift index cc05705d..ed274687 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/Button/TXButtonShape.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/Button/TXButtonShape.swift @@ -104,6 +104,7 @@ public enum TXButtonShape { /// 캡슐 형태의 라운드 버튼 상태를 정의하는 타입입니다. public enum TXRoundState { case standard + case disabled } // MARK: - Circle From 554b572c0e89f40bd5b7408b5cd5702421eaa4bb Mon Sep 17 00:00:00 2001 From: jihun Date: Mon, 11 May 2026 16:04:20 +0900 Subject: [PATCH 25/44] =?UTF-8?q?fix:=20=EC=A3=BC=EA=B0=84=20=EC=BA=98?= =?UTF-8?q?=EB=A6=B0=EB=8D=94=20=EC=9D=B8=ED=84=B0=EB=9E=99=EC=85=98=20?= =?UTF-8?q?=EC=9A=94=EA=B5=AC=EC=82=AC=ED=95=AD=EC=97=90=20=EB=A7=9E?= =?UTF-8?q?=EA=B2=8C=20=EC=88=98=EC=A0=95=20-=20#285?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Home/Sources/Goal/EditGoalListView.swift | 3 + .../Feature/Home/Sources/Home/HomeView.swift | 3 + .../Components/Calendar/Core/TXCalendar.swift | 268 +++++++----------- 3 files changed, 115 insertions(+), 159 deletions(-) diff --git a/Projects/Feature/Home/Sources/Goal/EditGoalListView.swift b/Projects/Feature/Home/Sources/Goal/EditGoalListView.swift index f610dd33..b3ae0a4e 100644 --- a/Projects/Feature/Home/Sources/Goal/EditGoalListView.swift +++ b/Projects/Feature/Home/Sources/Goal/EditGoalListView.swift @@ -75,6 +75,9 @@ private extension EditGoalListView { weeks: store.calendarWeeks, onSelect: { item in store.send(.calendarDateSelected(item)) + }, + onSwipe: { swipe in + store.send(.weekCalendarSwipe(swipe)) } ) .frame(maxWidth: .infinity, maxHeight: 76) diff --git a/Projects/Feature/Home/Sources/Home/HomeView.swift b/Projects/Feature/Home/Sources/Home/HomeView.swift index 6c87a999..932e9e26 100644 --- a/Projects/Feature/Home/Sources/Home/HomeView.swift +++ b/Projects/Feature/Home/Sources/Home/HomeView.swift @@ -130,6 +130,9 @@ private extension HomeView { ), onSelect: { item in store.send(.calendarDateSelected(item)) + }, + onSwipe: { swipe in + store.send(.weekCalendarSwipe(swipe)) } ) .frame(maxWidth: .infinity, maxHeight: 76) diff --git a/Projects/Shared/DesignSystem/Sources/Components/Calendar/Core/TXCalendar.swift b/Projects/Shared/DesignSystem/Sources/Components/Calendar/Core/TXCalendar.swift index fbc529ef..50ecb32e 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/Calendar/Core/TXCalendar.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/Calendar/Core/TXCalendar.swift @@ -90,8 +90,9 @@ public struct TXCalendar: View { private let config: Configuration private let onSelect: (TXCalendarDateItem) -> Void private let onSwipe: ((SwipeGesture) -> Void)? - @State private var weeklyScrollPosition: String? - @State private var weeklyBaseDate: TXCalendarDate? + @GestureState private var weeklyDragTranslation: CGFloat = 0 + @State private var weeklyPagingOffset: CGFloat = 0 + @State private var isWeeklyPaging = false /// 캘린더 컴포넌트를 생성합니다. public init( @@ -147,21 +148,13 @@ public struct TXCalendar: View { cellSize: config.dateStyle.size, columns: TXCalendarLayout.daysInWeek ) + let pageWidth = max(0, proxy.size.width - (horizontalPadding * 2)) - Group { - if mode == .weekly && weeklyReferenceDate != nil { - weeklyScrollableContent( - width: proxy.size.width, - spacing: spacing - ) - } else { - staticCalendarContent( - width: proxy.size.width, - spacing: spacing - ) - .gesture(calendarSwipeGesture) - } - } + staticCalendarContent( + width: proxy.size.width, + spacing: spacing + ) + .highPriorityGesture(calendarSwipeGesture(pageWidth: pageWidth)) } .frame(height: contentHeight) } @@ -173,11 +166,10 @@ private extension TXCalendar { VStack(spacing: headerSpacing) { switch mode { case .weekly: - weekdayRow( - items: weekDateItems, + weeklyPageContent( + width: max(0, width - (horizontalPadding * 2)), spacing: spacing ) - weekRow(items: weekDateItems, spacing: spacing) case .monthly: monthlyWeekdayRow(spacing: spacing) @@ -190,53 +182,29 @@ private extension TXCalendar { .background(config.backgroundColor) } - func weeklyScrollableContent(width: CGFloat, spacing: CGFloat) -> some View { - return ScrollView(.horizontal, showsIndicators: false) { - LazyHStack(spacing: spacing) { - ForEach(weeklyDayOffsets, id: \.self) { dayOffset in - weeklyDayButton( - item: weeklyDayItem(for: dayOffset) - ) - } - } - .padding(.horizontal, horizontalPadding) - .padding(.vertical, config.verticalPadding) - } - .scrollPosition(id: $weeklyScrollPosition, anchor: .center) - .background(config.backgroundColor) - .onAppear { - if weeklyBaseDate == nil { - weeklyBaseDate = weeklyReferenceDate - } - syncWeeklyScrollPosition(animated: false) - } - .onChange(of: weeklyReferenceDate) { _, _ in - syncWeeklyScrollPosition(animated: true) - } - .frame(width: width, height: contentHeight) + func weeklyPageContent(width: CGFloat, spacing: CGFloat) -> some View { + HStack(spacing: 0) { + weeklyPage(items: weeklyPageItems(weekOffset: -1), spacing: spacing) + .frame(width: width) + weeklyPage(items: weeklyPageItems(weekOffset: 0), spacing: spacing) + .frame(width: width) + weeklyPage(items: weeklyPageItems(weekOffset: 1), spacing: spacing) + .frame(width: width) + } + .offset(x: -width + weeklyPagingOffset + weeklyDragTranslation) + .frame( + width: width, + height: config.weekdayHeight + headerSpacing + config.dateStyle.size + config.weeklyBottomPadding, + alignment: .leading + ) + .clipped() } - func weeklyDayButton(item: TXCalendarDateItem) -> some View { - Button { - onSelect(item) - } label: { - VStack(spacing: headerSpacing) { - Text(weeklyHeaderTitle(for: item)) - .typography(config.weekdayTypography) - .foregroundStyle(config.weekdayColor) - .frame(width: config.dateStyle.size, height: config.weekdayHeight) - - TXCalendarDateCell( - item: item, - style: config.dateStyle, - customBackground: config.dateCellBackground?(item) - ) - } - .padding(.bottom, config.weeklyBottomPadding) - .frame(width: config.dateStyle.size) + func weeklyPage(items: [TXCalendarDateItem], spacing: CGFloat) -> some View { + VStack(spacing: headerSpacing) { + weekdayRow(items: items, spacing: spacing) + weekRow(items: items, spacing: spacing) } - .buttonStyle(.plain) - .id(dateItemScrollID(for: item)) } func weekdayRow(items: [TXCalendarDateItem], spacing: CGFloat) -> some View { @@ -342,10 +310,7 @@ private extension TXCalendar { if let currentDate, currentDate.wrappedValue.day != nil { return currentDate.wrappedValue } - return weeklyReferenceDateFromWeeks ?? currentDate?.wrappedValue - } - - var weeklyReferenceDateFromWeeks: TXCalendarDate? { + let selectedItem = weekDateItems.first { item in switch item.status { case .selectedFilled, .selectedLine: @@ -366,112 +331,118 @@ private extension TXCalendar { return TXCalendarDate(components: components) } - var weeklyDayOffsets: [Int] { - Array(-365...365) - } - - func dateItemScrollID(for item: TXCalendarDateItem) -> String? { - guard let components = item.dateComponents, - let year = components.year, - let month = components.month, - let day = components.day else { - return nil - } - - return "\(year)-\(month)-\(day)" - } } // MARK: - Private Methods private extension TXCalendar { - var calendarSwipeGesture: some Gesture { + func calendarSwipeGesture(pageWidth: CGFloat) -> some Gesture { DragGesture(minimumDistance: 16) + .updating($weeklyDragTranslation) { value, state, _ in + guard mode == .weekly else { return } + + let horizontalDistance = value.translation.width + let verticalDistance = value.translation.height + guard abs(horizontalDistance) > abs(verticalDistance) else { return } + + state = boundedWeeklyDragTranslation(horizontalDistance, pageWidth: pageWidth) + } .onEnded { value in let horizontalDistance = value.translation.width let verticalDistance = value.translation.height guard abs(horizontalDistance) > abs(verticalDistance) else { return } let swipe: SwipeGesture = horizontalDistance > 0 ? .previous : .next - handleSwipe(swipe) + handleSwipe(swipe, pageWidth: pageWidth) } } - func handleSwipe(_ swipe: SwipeGesture) { + func handleSwipe(_ swipe: SwipeGesture, pageWidth: CGFloat) { switch swipe { case .previous: - guard canMovePrevious else { return } + guard canMovePrevious else { + resetWeeklyPagingOffset() + return + } case .next: - guard canMoveNext else { return } + guard canMoveNext else { + resetWeeklyPagingOffset() + return + } } - applySwipeToCurrentDate(swipe) - onSwipe?(swipe) - } - - func weeklyDayItem(for dayOffset: Int) -> TXCalendarDateItem { - guard let baseDate = (weeklyBaseDate ?? weeklyReferenceDate)?.date, - let targetDate = Calendar(identifier: .gregorian).date(byAdding: .day, value: dayOffset, to: baseDate) else { - return .init(text: "") + guard mode == .weekly else { + applySwipe(swipe) + return } + guard !isWeeklyPaging else { return } - let calendar = Calendar(identifier: .gregorian) - let components = calendar.dateComponents([.year, .month, .day], from: targetDate) + let targetOffset: CGFloat + switch swipe { + case .previous: targetOffset = pageWidth + case .next: targetOffset = -pageWidth + } + isWeeklyPaging = true + withAnimation(.easeInOut(duration: 0.22)) { + weeklyPagingOffset = targetOffset + } - let itemID = "\(components.year ?? 0)-\(components.month ?? 0)-\(components.day ?? 0)" - let isSelected = itemID == selectedDateScrollID + DispatchQueue.main.asyncAfter(deadline: .now() + 0.22) { + applySwipe(swipe) + resetWeeklyPagingOffset() + } + } - let status: TXCalendarDateStatus - if isSelected { - status = .selectedLine - } else if let refDate = weeklyReferenceDate, components.month != refDate.month { - status = .lastDate + func applySwipe(_ swipe: SwipeGesture) { + if let onSwipe { + withAnimation(.easeInOut(duration: 0.2)) { + onSwipe(swipe) + } } else { - status = .default + applySwipeToCurrentDate(swipe) } - - return TXCalendarDateItem( - text: components.day.map(String.init) ?? "", - status: status, - dateComponents: components - ) } - func syncWeeklyScrollPosition( - animated: Bool - ) { - guard let targetID = selectedDateScrollID else { - return + func resetWeeklyPagingOffset() { + withTransaction(Transaction(animation: nil)) { + weeklyPagingOffset = 0 + isWeeklyPaging = false } + } - let action = { - weeklyScrollPosition = targetID + func boundedWeeklyDragTranslation(_ translation: CGFloat, pageWidth: CGFloat) -> CGFloat { + if translation > 0, !canMovePrevious { + return 0 } - - DispatchQueue.main.async { - if animated { - withAnimation(.easeInOut(duration: 0.2)) { - action() - } - } else { - action() - } + if translation < 0, !canMoveNext { + return 0 } + return min(max(translation, -pageWidth), pageWidth) } - var selectedDateScrollID: String? { + func weeklyPageItems(weekOffset: Int) -> [TXCalendarDateItem] { + guard weekOffset != 0 else { + return weekDateItems + } guard let referenceDate = weeklyReferenceDate else { - return nil + return weekDateItems + } + let items = TXCalendarDataGenerator.generateWeekData( + for: referenceDate, + weekOffset: weekOffset + ).first ?? [] + return items.map { item in + switch item.status { + case .selectedLine, .selectedFilled: + return TXCalendarDateItem( + id: item.id, + text: item.text, + status: .default, + dateComponents: item.dateComponents + ) + case .completed, .default, .lastDate: + return item + } } - - let components = referenceDate.dateComponents - - return dateItemScrollID( - for: TXCalendarDateItem( - text: components.day.map(String.init) ?? "", - status: .selectedLine, - dateComponents: components - ) - ) } func applySwipeToCurrentDate(_ swipe: SwipeGesture) { @@ -510,25 +481,4 @@ private extension TXCalendar { return isToday ? "오늘" : weekdays[index] } - - func weeklyHeaderTitle(for item: TXCalendarDateItem) -> String { - guard let components = item.dateComponents, - let year = components.year, - let month = components.month, - let day = components.day, - let date = Calendar(identifier: .gregorian).date(from: components) else { - return "" - } - - let today = Calendar(identifier: .gregorian).dateComponents([.year, .month, .day], from: Date()) - if today.year == year && today.month == month && today.day == day { - return "오늘" - } - - let weekdayIndex = Calendar(identifier: .gregorian).component(.weekday, from: date) - 1 - guard weekdays.indices.contains(weekdayIndex) else { - return "" - } - return weekdays[weekdayIndex] - } } From bbfc66c4a3256de142d33991334dfc1e41a8e2d0 Mon Sep 17 00:00:00 2001 From: jihun Date: Mon, 11 May 2026 16:38:06 +0900 Subject: [PATCH 26/44] =?UTF-8?q?fix:=20navigation=20pop=EC=8B=9C=20?= =?UTF-8?q?=ED=9D=B0=EC=83=89=20=ED=99=94=EB=A9=B4=20=EB=B3=B4=EC=9D=B4?= =?UTF-8?q?=EB=8A=94=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95=20-=20#285?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Root/HomeCoordinator+Impl.swift | 22 +++++++++++-------- .../Coordinator/StatsCoordinator+Impl.swift | 6 ----- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/Projects/Feature/Home/Sources/Root/HomeCoordinator+Impl.swift b/Projects/Feature/Home/Sources/Root/HomeCoordinator+Impl.swift index 1647b454..48611ebb 100644 --- a/Projects/Feature/Home/Sources/Root/HomeCoordinator+Impl.swift +++ b/Projects/Feature/Home/Sources/Root/HomeCoordinator+Impl.swift @@ -57,12 +57,12 @@ extension HomeCoordinator { state.routes.append(.notification) state.notification = .init() return .none - + case let .home(.delegate(.goToStatsDetail(id))): state.routes.append(.statsDetail) state.statsDetail = .init(goalId: id) return .none - + case .statsDetail(.delegate(.navigateBack)): popLastRoute(&state.routes) return .none @@ -75,11 +75,13 @@ extension HomeCoordinator { editingGoalId: goalId ) return .none - + case .statsDetail(.onDisappear): - state.statsDetail = nil + if !state.routes.contains(.statsDetail) { + state.statsDetail = nil + } return .none - + case .statsDetail: return .none @@ -95,6 +97,12 @@ extension HomeCoordinator { popLastRoute(&state.routes) return .none + case .editGoalList(.onDisappear): + if !state.routes.contains(.editGoalList) { + state.editGoalList = nil + } + return .none + case .makeGoal(.onDisappear): state.makeGoal = nil return .none @@ -103,10 +111,6 @@ extension HomeCoordinator { popLastRoute(&state.routes) return .none - case .editGoalList(.onDisappear): - state.editGoalList = nil - return .none - case let .editGoalList(.delegate(.goToGoalEdit(goalId))): state.routes.append(.makeGoal) state.makeGoal = .init(category: .custom, mode: .edit, editingGoalId: goalId) diff --git a/Projects/Feature/Stats/Sources/Coordinator/StatsCoordinator+Impl.swift b/Projects/Feature/Stats/Sources/Coordinator/StatsCoordinator+Impl.swift index f8fe03e7..fd5238fe 100644 --- a/Projects/Feature/Stats/Sources/Coordinator/StatsCoordinator+Impl.swift +++ b/Projects/Feature/Stats/Sources/Coordinator/StatsCoordinator+Impl.swift @@ -64,12 +64,6 @@ extension StatsCoordinator { state.routes.removeLast() return .none - case .statsDetail(.onDisappear): - if !state.routes.contains(.statsDetail) { - state.statsDetail = nil - } - return .none - case .goalDetail(.onDisappear): if !state.routes.contains(.goalDetail) { state.goalDetail = nil From 3b1fc5b1e7df7bbcd7e9f30f784181e816b06ee5 Mon Sep 17 00:00:00 2001 From: jihun Date: Mon, 11 May 2026 16:54:16 +0900 Subject: [PATCH 27/44] =?UTF-8?q?fix:=20AddButton=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EB=B3=80=EA=B2=BD=20-=20#285?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MainTab/Sources/View/MainTabView.swift | 2 +- .../Icons/Symbol/ic_plus_l.imageset/Contents.json | 15 +++++++++++++++ .../Icons/Symbol/ic_plus_l.imageset/ic_plus_l.svg | 3 +++ .../Sources/Resources/Image/Images.swift | 1 + 4 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Symbol/ic_plus_l.imageset/Contents.json create mode 100644 Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Symbol/ic_plus_l.imageset/ic_plus_l.svg diff --git a/Projects/Feature/MainTab/Sources/View/MainTabView.swift b/Projects/Feature/MainTab/Sources/View/MainTabView.swift index f14d6edb..c319edb9 100644 --- a/Projects/Feature/MainTab/Sources/View/MainTabView.swift +++ b/Projects/Feature/MainTab/Sources/View/MainTabView.swift @@ -81,7 +81,7 @@ private extension MainTabView { var homeFloatingButton: some View { TXButton( shape: .circle( - style: .basic(icon: Image.Icon.Symbol.plus), + style: .basic(icon: Image.Icon.Symbol.plusL), size: .m, state: .standard ), diff --git a/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Symbol/ic_plus_l.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Symbol/ic_plus_l.imageset/Contents.json new file mode 100644 index 00000000..67d1a1e5 --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Symbol/ic_plus_l.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "ic_plus_l.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Symbol/ic_plus_l.imageset/ic_plus_l.svg b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Symbol/ic_plus_l.imageset/ic_plus_l.svg new file mode 100644 index 00000000..df9a86bd --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/Image/ImageAssets.xcassets/Icons/Symbol/ic_plus_l.imageset/ic_plus_l.svg @@ -0,0 +1,3 @@ + + + diff --git a/Projects/Shared/DesignSystem/Sources/Resources/Image/Images.swift b/Projects/Shared/DesignSystem/Sources/Resources/Image/Images.swift index 2c927ac1..d026ac22 100644 --- a/Projects/Shared/DesignSystem/Sources/Resources/Image/Images.swift +++ b/Projects/Shared/DesignSystem/Sources/Resources/Image/Images.swift @@ -89,6 +89,7 @@ public extension Image.Icon.Symbol { static let meatball = SymbolAsset.icMeatball.swiftUIImage static let minus = SymbolAsset.icMinus.swiftUIImage static let plus = SymbolAsset.icPlus.swiftUIImage + static let plusL = SymbolAsset.icPlusL.swiftUIImage static let icReturn = SymbolAsset.icReturn.swiftUIImage static let setting = SymbolAsset.icSetting.swiftUIImage static let turn = SymbolAsset.icTurn.swiftUIImage From 7e4668fc55a6603716de8411f1b15e4c007344ee Mon Sep 17 00:00:00 2001 From: jihun Date: Mon, 11 May 2026 17:02:59 +0900 Subject: [PATCH 28/44] =?UTF-8?q?fix:=20=EC=9D=B8=EC=A6=9D=EC=83=B7=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EB=B7=B0=20=EB=84=A4=EB=B9=84=EA=B2=8C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EB=B2=84=ED=8A=BC=20=ED=84=B0=EC=B9=98=20?= =?UTF-8?q?=EA=B0=84=ED=97=90=EC=A0=81=EC=9C=BC=EB=A1=9C=20=EC=95=88?= =?UTF-8?q?=EB=90=98=EB=8A=94=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?-=20#285?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CaptureSession/Sources/CaptureSessionManager.swift | 7 +++++++ .../Feature/GoalDetail/Sources/Detail/GoalDetailView.swift | 1 + 2 files changed, 8 insertions(+) diff --git a/Projects/Core/CaptureSession/Sources/CaptureSessionManager.swift b/Projects/Core/CaptureSession/Sources/CaptureSessionManager.swift index 6c13f40e..4fff75c4 100644 --- a/Projects/Core/CaptureSession/Sources/CaptureSessionManager.swift +++ b/Projects/Core/CaptureSession/Sources/CaptureSessionManager.swift @@ -17,6 +17,7 @@ final class CaptureSessionManager: NSObject, @unchecked Sendable { private let photoOutput = AVCapturePhotoOutput() private var continuation: CheckedContinuation? private var flashMode: AVCaptureDevice.FlashMode = .off + private var currentPosition: AVCaptureDevice.Position = .back func requestAuthorization() async -> Bool { switch AVCaptureDevice.authorizationStatus(for: .video) { @@ -40,6 +41,7 @@ final class CaptureSessionManager: NSObject, @unchecked Sendable { await performOnSessionQueue { [weak self] in guard let self else { return } + self.currentPosition = position self.session.beginConfiguration() self.session.sessionPreset = .photo self.session.inputs.forEach { self.session.removeInput($0) } @@ -86,6 +88,11 @@ final class CaptureSessionManager: NSObject, @unchecked Sendable { } self.continuation = continuation + + if let connection = self.photoOutput.connection(with: .video), connection.isVideoMirroringSupported { + connection.isVideoMirrored = self.currentPosition == .front + } + let settings = AVCapturePhotoSettings() if self.photoOutput.supportedFlashModes.contains(self.flashMode) { settings.flashMode = self.flashMode diff --git a/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift b/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift index c1d56be6..5a4bf592 100644 --- a/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift +++ b/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift @@ -63,6 +63,7 @@ public struct GoalDetailView: View { public var body: some View { VStack(spacing: 0) { navigationBar + .zIndex(1) if store.item != nil { cardView From 37780fa4d5a3b36d5a062a4da399ff4b590ab9c1 Mon Sep 17 00:00:00 2001 From: jihun Date: Tue, 12 May 2026 21:04:05 +0900 Subject: [PATCH 29/44] =?UTF-8?q?fix:=20=ED=99=88=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=B0=8C=EB=A5=B4=EA=B8=B0=20=EC=BF=A8=ED=83=80=EC=9E=84?= =?UTF-8?q?=EC=9D=BC=20=EB=95=8C=20disable=20=EC=B2=98=EB=A6=AC=20-=20#285?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Interface/Sources/Home/HomeReducer.swift | 1 + .../Home/Sources/Home/HomeReducer+Impl.swift | 54 +++++++++++++++++-- .../Components/Card/Goal/GoalCardItem.swift | 3 ++ .../Components/Card/Goal/GoalCardView.swift | 4 +- 4 files changed, 57 insertions(+), 5 deletions(-) diff --git a/Projects/Feature/Home/Interface/Sources/Home/HomeReducer.swift b/Projects/Feature/Home/Interface/Sources/Home/HomeReducer.swift index e514c954..1bd6c0af 100644 --- a/Projects/Feature/Home/Interface/Sources/Home/HomeReducer.swift +++ b/Projects/Feature/Home/Interface/Sources/Home/HomeReducer.swift @@ -117,6 +117,7 @@ public struct HomeReducer { case setCalendarDate(TXCalendarDate) case setCalendarSheetPresented(Bool) case showToast(TXToastType) + case setPokeButtonDisabled(goalId: Int64, Bool) case authorizationCompleted(id: Int64, isAuthorized: Bool) case proofPhotoDismissed case addGoalButtonTapped(GoalCategory) diff --git a/Projects/Feature/Home/Sources/Home/HomeReducer+Impl.swift b/Projects/Feature/Home/Sources/Home/HomeReducer+Impl.swift index 940113a0..d975680f 100644 --- a/Projects/Feature/Home/Sources/Home/HomeReducer+Impl.swift +++ b/Projects/Feature/Home/Sources/Home/HomeReducer+Impl.swift @@ -234,6 +234,7 @@ extension HomeReducer { } // 상대방 미인증 시 찌르기 API 호출 let goalId = card.id + setPokeButtonDisabled(state: &state, goalId: goalId, isDisabled: true) return .run { send in PokeCooldownManager.recordPoke(goalId: goalId) do { @@ -241,6 +242,7 @@ extension HomeReducer { await send(.showToast(.poke(message: "상대방을 찔렀어요!"))) } catch { PokeCooldownManager.removePoke(goalId: goalId) + await send(.setPokeButtonDisabled(goalId: goalId, false)) await send(.showToast(.warning(message: "찌르기에 실패했어요"))) } } @@ -301,7 +303,9 @@ extension HomeReducer { // MARK: - Update State case let .fetchGoalsCompleted(goalList, date): let cacheKey = TXCalendarUtil.apiDateString(for: date) - let items = goalList.goals.map(HomeGoalItem.init(goal:)) + let items = goalList.goals + .map(HomeGoalItem.init(goal:)) + .map { $0.applyingPokeCooldownState() } state.goalsCache[cacheKey] = items state.hadFirstGoal = goalList.hasEverRegisteredGoal @@ -352,7 +356,9 @@ extension HomeReducer { let date = state.calendarDate let cacheKey = TXCalendarUtil.apiDateString(for: date) if let cachedItems = state.goalsCache[cacheKey] { - state.items = cachedItems + let items = cachedItems.map { $0.applyingPokeCooldownState() } + state.items = items + state.goalsCache[cacheKey] = items state.isLoading = false } else { state.isLoading = true @@ -374,6 +380,10 @@ extension HomeReducer { case let .showToast(toast): state.toast = toast return .none + + case let .setPokeButtonDisabled(goalId, isDisabled): + setPokeButtonDisabled(state: &state, goalId: goalId, isDisabled: isDisabled) + return .none case let .authorizationCompleted(id, isAuthorized): if !isAuthorized { @@ -418,7 +428,7 @@ extension HomeReducer { status: goal.status ) state.items[index].updateGoal(updatedGoal) - state.goalsCache[TXCalendarUtil.apiDateString(for: state.calendarDate)] = state.items + refreshPokeCooldownStates(state: &state) return .none case .proofPhotoDismissed: @@ -451,7 +461,7 @@ extension HomeReducer { status: goal.status ) state.items[index].updateGoal(updatedGoal) - state.goalsCache[TXCalendarUtil.apiDateString(for: state.calendarDate)] = state.items + refreshPokeCooldownStates(state: &state) return .send(.showToast(.delete(message: "인증이 해제되었어요"))) case .deletePhotoLogFailed: @@ -476,3 +486,39 @@ extension HomeReducer { } } + +private extension HomeGoalItem { + func applyingPokeCooldownState() -> Self { + var item = self + item.card.yourCard.isButtonDisabled = !item.card.yourCard.isSelected + && PokeCooldownManager.remainingCooldown(goalId: item.id) != nil + return item + } +} + +private func refreshPokeCooldownStates(state: inout HomeReducer.State) { + state.items = state.items.map { $0.applyingPokeCooldownState() } + state.goalsCache[TXCalendarUtil.apiDateString(for: state.calendarDate)] = state.items +} + +private func setPokeButtonDisabled( + state: inout HomeReducer.State, + goalId: Int64, + isDisabled: Bool +) { + updatePokeButtonDisabled(in: &state.items, goalId: goalId, isDisabled: isDisabled) + + let cacheKey = TXCalendarUtil.apiDateString(for: state.calendarDate) + guard var cachedItems = state.goalsCache[cacheKey] else { return } + updatePokeButtonDisabled(in: &cachedItems, goalId: goalId, isDisabled: isDisabled) + state.goalsCache[cacheKey] = cachedItems +} + +private func updatePokeButtonDisabled( + in items: inout [HomeGoalItem], + goalId: Int64, + isDisabled: Bool +) { + guard let index = items.firstIndex(where: { $0.id == goalId }) else { return } + items[index].card.yourCard.isButtonDisabled = isDisabled +} diff --git a/Projects/Shared/DesignSystem/Sources/Components/Card/Goal/GoalCardItem.swift b/Projects/Shared/DesignSystem/Sources/Components/Card/Goal/GoalCardItem.swift index 0d8ec31e..3fa4a5aa 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/Card/Goal/GoalCardItem.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/Card/Goal/GoalCardItem.swift @@ -40,6 +40,7 @@ public struct GoalCardItem: Identifiable, Equatable { public let photologId: Int64? let imageURL: URL? public var isSelected: Bool + public var isButtonDisabled: Bool public let emoji: Image? /// 이미지/이모지로 GoalCardItem.Card를 생성합니다. @@ -56,11 +57,13 @@ public struct GoalCardItem: Identifiable, Equatable { photologId: Int64? = nil, imageURL: URL? = nil, isSelected: Bool, + isButtonDisabled: Bool = false, emoji: Image? = nil ) { self.photologId = photologId self.imageURL = imageURL self.isSelected = isSelected + self.isButtonDisabled = isButtonDisabled self.emoji = emoji } } diff --git a/Projects/Shared/DesignSystem/Sources/Components/Card/Goal/GoalCardView.swift b/Projects/Shared/DesignSystem/Sources/Components/Card/Goal/GoalCardView.swift index 3fc749af..524cb235 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/Card/Goal/GoalCardView.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/Card/Goal/GoalCardView.swift @@ -138,6 +138,7 @@ private extension GoalCardView { } else { unCompletedView( placeholder: placeholder, + isButtonDisabled: item.isButtonDisabled, buttonAction: buttonAction ) .frame(minWidth: 0, maxWidth: .infinity) @@ -155,6 +156,7 @@ private extension GoalCardView { func unCompletedView( placeholder: Placeholder, + isButtonDisabled: Bool, buttonAction: (() -> Void)? = nil ) -> some View { VStack(spacing: 0) { @@ -165,7 +167,7 @@ private extension GoalCardView { shape: .round( style: .illustLight(text: "찌르기!"), size: .s, - state: .standard + state: isButtonDisabled ? .disabled : .standard ), onTap: { buttonAction?() } ) From facc813d3ae1cb4987b01063ba8a86f18acf809d Mon Sep 17 00:00:00 2001 From: jihun Date: Tue, 12 May 2026 22:26:52 +0900 Subject: [PATCH 30/44] =?UTF-8?q?fix:=20=EB=AA=A9=ED=91=9C=20=ED=8E=B8?= =?UTF-8?q?=EC=A7=91=20=EB=B7=B0=20=EB=A1=9C=EB=94=A9=20ux=20=EC=A0=80?= =?UTF-8?q?=ED=95=98=EB=90=98=EB=8A=94=20=EC=9D=B4=EC=8A=88=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20-=20#285?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Home/Sources/Goal/EditGoalListView.swift | 3 +++ .../Loading/TXLoadingPresenter.swift | 23 +++++++++++++------ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/Projects/Feature/Home/Sources/Goal/EditGoalListView.swift b/Projects/Feature/Home/Sources/Goal/EditGoalListView.swift index b3ae0a4e..c819aa08 100644 --- a/Projects/Feature/Home/Sources/Goal/EditGoalListView.swift +++ b/Projects/Feature/Home/Sources/Goal/EditGoalListView.swift @@ -46,6 +46,9 @@ struct EditGoalListView: View { guard store.selectedCardMenu != nil else { return } store.send(.backgroundTapped) } + .transaction { transaction in + transaction.animation = nil + } .txModal( item: $store.modal, onAction: { action in diff --git a/Projects/Shared/DesignSystem/Sources/Components/Indicator/Loading/TXLoadingPresenter.swift b/Projects/Shared/DesignSystem/Sources/Components/Indicator/Loading/TXLoadingPresenter.swift index 546873fc..2b4e9912 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/Indicator/Loading/TXLoadingPresenter.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/Indicator/Loading/TXLoadingPresenter.swift @@ -24,18 +24,27 @@ struct TXLoadingModifier: ViewModifier { ZStack { content .disabled(isPresented) + + dimColor + .ignoresSafeArea() + .opacity(isPresented ? 1 : 0) + .animation(.easeInOut(duration: 0.2), value: isPresented) + if isPresented { - dimColor - .ignoresSafeArea() - if let message { - TXLoadingStatusView(message: message) - } else { - TXLoadingIndicator() - } + loadingContent } } .animation(.easeInOut(duration: 0.2), value: isPresented) } + + @ViewBuilder + private var loadingContent: some View { + if let message { + TXLoadingStatusView(message: message) + } else { + TXLoadingIndicator() + } + } } // MARK: - View Extension From 434069a8a8d916cb6406113ae76f389a765bf90d Mon Sep 17 00:00:00 2001 From: jihun Date: Wed, 13 May 2026 09:19:22 +0900 Subject: [PATCH 31/44] =?UTF-8?q?refactor:=20=EB=AA=A9=ED=91=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EC=8B=9C=20=EA=B8=B0=EC=A1=B4=20=EB=AA=A9?= =?UTF-8?q?=ED=91=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A3=BC=EC=9E=85?= =?UTF-8?q?=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20-=20#285?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DTO/StatsDetailCalendarResponseDTO.swift | 1 + .../Sources/Entity/StatsDetail.swift | 1 + .../Stats/Interface/Sources/StatsClient.swift | 1 + .../Sources/Goal/EditGoalListReducer.swift | 3 +- .../Goal/EditGoalListReducer+Impl.swift | 26 +++- .../Sources/Root/HomeCoordinator+Impl.swift | 14 +-- .../MakeGoal/Interface/Sources/MakeGoal.swift | 69 +++++++++++ .../Interface/Sources/MakeGoalReducer.swift | 99 ++++++--------- .../Sources/MakeGoalReducer+Impl.swift | 117 ++++++------------ .../MakeGoal/Sources/MakeGoalView.swift | 14 +-- .../Sources/Detail/StatsDetailReducer.swift | 3 +- .../Coordinator/StatsCoordinator+Impl.swift | 8 +- .../Detail/StatsDetailReducer+Impl.swift | 15 ++- .../Sources/Detail/StatsDetailView.swift | 5 + .../Card/GoalEdit/GoalEditCardItem.swift | 3 + .../Sources/Extension/String+Extensions.swift | 8 ++ 16 files changed, 213 insertions(+), 174 deletions(-) create mode 100644 Projects/Feature/MakeGoal/Interface/Sources/MakeGoal.swift diff --git a/Projects/Domain/Stats/Interface/Sources/DTO/StatsDetailCalendarResponseDTO.swift b/Projects/Domain/Stats/Interface/Sources/DTO/StatsDetailCalendarResponseDTO.swift index de84e838..d9eea198 100644 --- a/Projects/Domain/Stats/Interface/Sources/DTO/StatsDetailCalendarResponseDTO.swift +++ b/Projects/Domain/Stats/Interface/Sources/DTO/StatsDetailCalendarResponseDTO.swift @@ -27,6 +27,7 @@ extension StatsDetailCalendarResponseDTO { StatsDetail( goalId: goalId, goalName: goalName, + goalIcon: goalIcon, isCompleted: isCompleted, yearMonth: yearMonth, completedDate: completedDates.map { diff --git a/Projects/Domain/Stats/Interface/Sources/Entity/StatsDetail.swift b/Projects/Domain/Stats/Interface/Sources/Entity/StatsDetail.swift index 584f32c1..67db01fb 100644 --- a/Projects/Domain/Stats/Interface/Sources/Entity/StatsDetail.swift +++ b/Projects/Domain/Stats/Interface/Sources/Entity/StatsDetail.swift @@ -14,6 +14,7 @@ import DomainCommonInterface public struct StatsDetail: Equatable { public let goalId: Int64 public let goalName: String + public let goalIcon: String public var isCompleted: Bool public let yearMonth: String public let completedDate: [CompletedDate] diff --git a/Projects/Domain/Stats/Interface/Sources/StatsClient.swift b/Projects/Domain/Stats/Interface/Sources/StatsClient.swift index afa73f8b..d7a426ef 100644 --- a/Projects/Domain/Stats/Interface/Sources/StatsClient.swift +++ b/Projects/Domain/Stats/Interface/Sources/StatsClient.swift @@ -177,6 +177,7 @@ extension StatsClient: TestDependencyKey { return .init( goalId: 1, goalName: "밥 잘 챙겨먹기", + goalIcon: "ICON_DEFAULT", isCompleted: false, yearMonth: "2026-02", completedDate: [ diff --git a/Projects/Feature/Home/Interface/Sources/Goal/EditGoalListReducer.swift b/Projects/Feature/Home/Interface/Sources/Goal/EditGoalListReducer.swift index abbe7eb5..939e0d29 100644 --- a/Projects/Feature/Home/Interface/Sources/Goal/EditGoalListReducer.swift +++ b/Projects/Feature/Home/Interface/Sources/Goal/EditGoalListReducer.swift @@ -9,6 +9,7 @@ import Foundation import ComposableArchitecture import FeatureCommonInterface +import FeatureMakeGoalInterface import SharedDesignSystem import SharedUtil import SwiftUI @@ -95,7 +96,7 @@ public struct EditGoalListReducer { public enum Delegate { case navigateBack - case goToGoalEdit(goalId: Int64) + case goToGoalEdit(MakeGoalReducer.State.MakeGoal) case goToCompletedStats } } diff --git a/Projects/Feature/Home/Sources/Goal/EditGoalListReducer+Impl.swift b/Projects/Feature/Home/Sources/Goal/EditGoalListReducer+Impl.swift index 4cea0869..3f948a1f 100644 --- a/Projects/Feature/Home/Sources/Goal/EditGoalListReducer+Impl.swift +++ b/Projects/Feature/Home/Sources/Goal/EditGoalListReducer+Impl.swift @@ -9,9 +9,11 @@ import Foundation import SwiftUI import ComposableArchitecture +import DomainCommonInterface import DomainGoalInterface import FeatureCommonInterface import FeatureHomeInterface +import FeatureMakeGoalInterface import SharedDesignSystem import SharedUtil @@ -87,7 +89,16 @@ extension EditGoalListReducer { if isPast { state.toast = .warning(message: "이미 완료한 목표입니다!") } else { - return .send(.delegate(.goToGoalEdit(goalId: card.id))) + let goalData = MakeGoalReducer.State.MakeGoal( + goalId: card.id, + category: .custom, + icon: card.goalIcon, + title: card.goalName, + repeatCycle: RepeatCycle(displayText: card.repeatCycle), + startDate: card.startDate.displayTextToAPIDateString, + endDate: card.endDate.displayTextToAPIDateString + ) + return .send(.delegate(.goToGoalEdit(goalData))) } case .finish: @@ -175,6 +186,7 @@ extension EditGoalListReducer { GoalEditCardItem( id: $0.id, goalName: $0.title, + goalIcon: GoalIcon(from: $0.goalIcon), iconImage: GoalIcon(from: $0.goalIcon).thinImage, repeatCycle: $0.repeatCycle?.text ?? "", startDate: $0.startDate?.dateDisplayString ?? "", @@ -232,3 +244,15 @@ extension EditGoalListReducer { self.init(reducer: reducer) } } + +/// 재사용될 시 RepeatCycle 내부로 이동 +private extension RepeatCycle { + init?(displayText: String) { + switch displayText { + case RepeatCycle.daily.text: self = .daily + case RepeatCycle.weekly.text: self = .weekly + case RepeatCycle.monthly.text: self = .monthly + default: return nil + } + } +} diff --git a/Projects/Feature/Home/Sources/Root/HomeCoordinator+Impl.swift b/Projects/Feature/Home/Sources/Root/HomeCoordinator+Impl.swift index 48611ebb..7f5ac325 100644 --- a/Projects/Feature/Home/Sources/Root/HomeCoordinator+Impl.swift +++ b/Projects/Feature/Home/Sources/Root/HomeCoordinator+Impl.swift @@ -40,7 +40,7 @@ extension HomeCoordinator { case let .home(.delegate(.goToMakeGoal(category))): state.routes.append(.makeGoal) - state.makeGoal = .init(category: category, mode: .add) + state.makeGoal = .init(mode: .add(category)) return .none case let .home(.delegate(.goToEditGoalList(date))): @@ -67,13 +67,9 @@ extension HomeCoordinator { popLastRoute(&state.routes) return .none - case let .statsDetail(.delegate(.goToGoalEdit(goalId))): + case let .statsDetail(.delegate(.goToGoalEdit(goalData))): state.routes.append(.makeGoal) - state.makeGoal = .init( - category: .custom, - mode: .edit, - editingGoalId: goalId - ) + state.makeGoal = .init(mode: .edit(goalData)) return .none case .statsDetail(.onDisappear): @@ -111,9 +107,9 @@ extension HomeCoordinator { popLastRoute(&state.routes) return .none - case let .editGoalList(.delegate(.goToGoalEdit(goalId))): + case let .editGoalList(.delegate(.goToGoalEdit(goalData))): state.routes.append(.makeGoal) - state.makeGoal = .init(category: .custom, mode: .edit, editingGoalId: goalId) + state.makeGoal = .init(mode: .edit(goalData)) return .none case .editGoalList(.delegate(.goToCompletedStats)): diff --git a/Projects/Feature/MakeGoal/Interface/Sources/MakeGoal.swift b/Projects/Feature/MakeGoal/Interface/Sources/MakeGoal.swift new file mode 100644 index 00000000..4065b085 --- /dev/null +++ b/Projects/Feature/MakeGoal/Interface/Sources/MakeGoal.swift @@ -0,0 +1,69 @@ +// +// MakeGoal.swift +// FeatureMakeGoal +// +// Created by 정지훈 on 5/12/26. +// + +import Foundation + +import DomainCommonInterface +import SharedDesignSystem +import SharedUtil + +public extension MakeGoalReducer.State { + public struct MakeGoal: Equatable { + public var goalId: Int64? + public var category: GoalCategory + public var icon: GoalIcon + public var title: String + public var repeatCycle: RepeatCycle + public var startDate: TXCalendarDate + public var endDate: TXCalendarDate + public var isEndDateOn: Bool = false + public var weeklyPeriodCount: Int + public var monthlyPeriodCount: Int + + public init( + goalId: Int64? = nil, + category: GoalCategory, + icon: GoalIcon? = nil, + title: String?, + repeatCycle: RepeatCycle? = nil, + startDate: String?, + endDate: String?, + weeklyPeriodCount: Int = 1, + monthlyPeriodCount: Int = 1 + ) { + let now = CalendarNow() + let today = TXCalendarDate( + year: now.year, + month: now.month, + day: now.day + ) + + self.goalId = goalId + self.category = category + self.icon = icon ?? GoalIcon.allCases[category.iconIndex] + self.title = title ?? category.title + self.repeatCycle = repeatCycle ?? category.repeatCycle + + if let startDateString = startDate, + let startDate = TXCalendarUtil.parseAPIDateString(startDateString) { + self.startDate = startDate + } else { + self.startDate = today + } + + if let endDateString = endDate, + let endDate = TXCalendarUtil.parseAPIDateString(endDateString) { + self.endDate = endDate + self.isEndDateOn = true + } else { + self.endDate = self.startDate + } + self.weeklyPeriodCount = weeklyPeriodCount + self.monthlyPeriodCount = monthlyPeriodCount + } + } +} diff --git a/Projects/Feature/MakeGoal/Interface/Sources/MakeGoalReducer.swift b/Projects/Feature/MakeGoal/Interface/Sources/MakeGoalReducer.swift index 01a4fd2d..27eede6f 100644 --- a/Projects/Feature/MakeGoal/Interface/Sources/MakeGoalReducer.swift +++ b/Projects/Feature/MakeGoal/Interface/Sources/MakeGoalReducer.swift @@ -19,7 +19,7 @@ import SharedUtil /// ## 사용 예시 /// ```swift /// let store = Store( -/// initialState: MakeGoalReducer.State(category: .book) +/// initialState: MakeGoalReducer.State(mode: .add(.book)) /// ) { /// MakeGoalReducer() /// } @@ -34,7 +34,7 @@ public struct MakeGoalReducer { /// /// ## 사용 예시 /// ```swift - /// let state = MakeGoalReducer.State(category: .book) + /// let state = MakeGoalReducer.State(mode: .add(.book)) /// ``` public struct State: Equatable { public let minimumPeriodCount = 1 @@ -46,30 +46,22 @@ public struct MakeGoalReducer { public let monthlyPeriodText: String = RepeatCycle.monthly.text public var mode: Mode - public var editingGoalId: Int64? - public var category: GoalCategory - public var goalTitle: String - public var selectedPeriod: RepeatCycle - public var weeklyPeriodCount: Int = 1 - public var monthlyPeriodCount: Int = 1 - public var startDate: TXCalendarDate - public var endDate: TXCalendarDate + public var goalData: MakeGoal public var calendarSheetDate: TXCalendarDate public var isCalendarSheetPresented: Bool = false public var calendarTarget: CalendarTarget? - public var isEndDateOn: Bool = false public var isPeriodSheetPresented: Bool = false public var selectedEmojiIndex: Int public var isGoalTitleFocused: Bool = false public var startDateText: String public var endDateText: String - public var showPeriodCount: Bool { selectedPeriod != .daily } - public var periodCountText: String { "\(selectedPeriod.text) \(periodCount)번" } + public var showPeriodCount: Bool { goalData.repeatCycle != .daily } + public var periodCountText: String { "\(goalData.repeatCycle.text) \(periodCount)번" } public var selectedEmoji: GoalIcon { icons[selectedEmojiIndex] } public var completeButtonDisabled: Bool { !isValidTitleLength || isLoading } public var isInvalidTitle: Bool { isValidTitleLength } - public var isValidTitleLength: Bool { 2 <= goalTitle.count && goalTitle.count <= 14 } + public var isValidTitleLength: Bool { 2 <= goalData.title.count && goalData.title.count <= 14 } public var modal: TXModalStyle? public var toast: TXToastType? @@ -78,8 +70,8 @@ public struct MakeGoalReducer { /// 화면 모드를 구분합니다. public enum Mode: Equatable { - case add - case edit + case add(GoalCategory) + case edit(MakeGoal) } public enum CalendarTarget: Equatable { @@ -91,36 +83,31 @@ public struct MakeGoalReducer { /// /// ## 사용 예시 /// ```swift - /// let state = MakeGoalReducer.State(category: .book, mode: .add) + /// let state = MakeGoalReducer.State(mode: .add(.book)) /// ``` public init( - category: GoalCategory, - mode: Mode, - editingGoalId: Int64? = nil + mode: Mode ) { - let now = CalendarNow() - let today = TXCalendarDate( - year: now.year, - month: now.month, - day: now.day - ) - + let goalData: MakeGoal + switch mode { + case let .add(category): + goalData = .init( + category: category, + title: category != .custom ? category.title : "", + startDate: nil, + endDate: nil + ) + + case let .edit(makeGoal): + goalData = makeGoal + } + self.mode = mode - self.editingGoalId = editingGoalId - self.category = category - self.goalTitle = category != .custom ? category.title : "" - self.selectedPeriod = category.repeatCycle - self.selectedEmojiIndex = category.iconIndex - - self.startDate = today - self.endDate = today - self.calendarSheetDate = today - self.startDateText = "\(today.month)월 \(today.day ?? 1)일" - self.endDateText = "\(today.month)월 \(today.day ?? 1)일" - - let repeatCycle = category.repeatCycle - self.weeklyPeriodCount = repeatCycle == .weekly ? category.repeatCount : minimumPeriodCount - self.monthlyPeriodCount = repeatCycle == .monthly ? category.repeatCount : minimumPeriodCount + self.goalData = goalData + self.selectedEmojiIndex = GoalIcon.allCases.firstIndex(of: goalData.icon) ?? goalData.category.iconIndex + self.calendarSheetDate = goalData.startDate + self.startDateText = "\(goalData.startDate.month)월 \(goalData.startDate.day ?? 1)일" + self.endDateText = "\(goalData.endDate.month)월 \(goalData.endDate.day ?? 1)일" } } @@ -138,8 +125,6 @@ public struct MakeGoalReducer { case onDisappear // MARK: - Update State - case fetchGoalCompleted(Goal) - case fetchGoalFailed case createGoalFailed case updateGoalFailed @@ -191,46 +176,32 @@ public struct MakeGoalReducer { // MARK: - Functions public extension MakeGoalReducer.State { var periodCount: Int { - switch selectedPeriod { + switch goalData.repeatCycle { case .daily: return 1 - case .weekly: return weeklyPeriodCount - case .monthly: return monthlyPeriodCount + case .weekly: return goalData.weeklyPeriodCount + case .monthly: return goalData.monthlyPeriodCount } } var isMinusEnable: Bool { periodCount > minimumPeriodCount } var isPlusEnable: Bool { - if case .monthly = selectedPeriod { + if case .monthly = goalData.repeatCycle { return periodCount < monthlyMaximumPeriodCount - } else if case .weekly = selectedPeriod { + } else if case .weekly = goalData.repeatCycle { return periodCount < weeklyMaximumPeriodCount } else { return false } } - var selectedPeriodName: String { - get { - selectedPeriod.text - } set { - if newValue == dailyPeriodText { - selectedPeriod = .daily - } else if newValue == weeklyPeriodText { - selectedPeriod = .weekly - } else if newValue == monthlyPeriodText { - selectedPeriod = .monthly - } - } - } - var calendarMinimumDate: TXCalendarDate? { switch calendarTarget { case .startDate: let now = CalendarNow() return TXCalendarDate(year: now.year, month: now.month, day: now.day) case .endDate: - return startDate + return goalData.startDate case .none: return nil diff --git a/Projects/Feature/MakeGoal/Sources/MakeGoalReducer+Impl.swift b/Projects/Feature/MakeGoal/Sources/MakeGoalReducer+Impl.swift index a75ac514..4eae05b1 100644 --- a/Projects/Feature/MakeGoal/Sources/MakeGoalReducer+Impl.swift +++ b/Projects/Feature/MakeGoal/Sources/MakeGoalReducer+Impl.swift @@ -23,57 +23,10 @@ extension MakeGoalReducer { switch action { // MARK: - LifeCycle case .onAppear: - if case .edit = state.mode, - let goalId = state.editingGoalId { - state.isLoading = true - return .run { send in - do { - let goal = try await goalClient.fetchGoalById(goalId) - await send(.fetchGoalCompleted(goal)) - } catch { - await send(.fetchGoalFailed) - } - } - } return .none case .onDisappear: return .none - - case let .fetchGoalCompleted(goal): - state.isLoading = false - state.goalTitle = goal.title - state.selectedEmojiIndex = state.icons.firstIndex( - of: GoalIcon(from: goal.goalIcon) - ) ?? 0 - if let repeatCycle = goal.repeatCycle { - state.selectedPeriod = repeatCycle - } - if let repeatCount = goal.repeatCount { - switch state.selectedPeriod { - case .weekly: - state.weeklyPeriodCount = repeatCount - case .monthly: - state.monthlyPeriodCount = repeatCount - case .daily: - break - } - } - // 시작일/종료일 설정 - if let startDateString = goal.startDate, - let startDate = TXCalendarUtil.parseAPIDateString(startDateString) { - state.startDate = startDate - } - if let endDateString = goal.endDate, - let endDate = TXCalendarUtil.parseAPIDateString(endDateString) { - state.endDate = endDate - state.isEndDateOn = true - } - return .send(.updateDateText) - - case .fetchGoalFailed: - state.isLoading = false - return .send(.showToast(.warning(message: "목표 정보를 불러오지 못했어요"))) case .createGoalFailed: state.isLoading = false @@ -109,7 +62,7 @@ extension MakeGoalReducer { return .none case let .periodTabSelected(item): - state.selectedPeriod = item.repeatCycle + state.goalData.repeatCycle = item.repeatCycle return .none case .periodSelected: @@ -118,41 +71,41 @@ extension MakeGoalReducer { return .none case .periodSheetWeeklyTapped: - state.selectedPeriod = .weekly + state.goalData.repeatCycle = .weekly return .none case .periodSheetMonthlyTapped: - state.selectedPeriod = .monthly + state.goalData.repeatCycle = .monthly return .none case .periodSheetMinusTapped: - switch state.selectedPeriod { + switch state.goalData.repeatCycle { case .daily: return .none case .weekly: - state.weeklyPeriodCount -= 1 - state.selectedPeriod = .weekly + state.goalData.weeklyPeriodCount -= 1 + state.goalData.repeatCycle = .weekly case .monthly: - state.monthlyPeriodCount -= 1 - state.selectedPeriod = .monthly + state.goalData.monthlyPeriodCount -= 1 + state.goalData.repeatCycle = .monthly } return .none case .periodSheetPlusTapped: - switch state.selectedPeriod { + switch state.goalData.repeatCycle { case .daily: return .none case .weekly: - state.weeklyPeriodCount += 1 - state.selectedPeriod = .weekly + state.goalData.weeklyPeriodCount += 1 + state.goalData.repeatCycle = .weekly case .monthly: - state.monthlyPeriodCount += 1 - state.selectedPeriod = .monthly + state.goalData.monthlyPeriodCount += 1 + state.goalData.repeatCycle = .monthly } return .none @@ -164,17 +117,17 @@ extension MakeGoalReducer { case .startDateTapped: state.isGoalTitleFocused = false state.calendarTarget = .startDate - state.calendarSheetDate = state.startDate + state.calendarSheetDate = state.goalData.startDate state.isCalendarSheetPresented = true return .none case .endDateTapped: state.isGoalTitleFocused = false state.calendarTarget = .endDate - if state.endDate < state.startDate { - state.endDate = state.startDate + if state.goalData.endDate < state.goalData.startDate { + state.goalData.endDate = state.goalData.startDate } - state.calendarSheetDate = state.endDate + state.calendarSheetDate = state.goalData.endDate state.isCalendarSheetPresented = true return .send(.updateDateText) @@ -186,13 +139,13 @@ extension MakeGoalReducer { switch target { case .startDate: - state.startDate = state.calendarSheetDate - if state.endDate < state.startDate { - state.endDate = state.startDate + state.goalData.startDate = state.calendarSheetDate + if state.goalData.endDate < state.goalData.startDate { + state.goalData.endDate = state.goalData.startDate } case .endDate: - state.endDate = state.calendarSheetDate + state.goalData.endDate = state.calendarSheetDate } state.isCalendarSheetPresented = false @@ -205,19 +158,19 @@ extension MakeGoalReducer { } state.isLoading = true - let endDateString: String? = state.isEndDateOn - ? TXCalendarUtil.apiDateString(for: state.endDate) + let endDateString: String? = state.goalData.isEndDateOn + ? TXCalendarUtil.apiDateString(for: state.goalData.endDate) : nil switch state.mode { - case .add: - let category = state.category.rawValue + case let .add(category): + let category = category.rawValue state.submitMessage = "등록 중..." let request = GoalCreateRequestDTO( - name: state.goalTitle, + name: state.goalData.title, icon: state.selectedEmoji.rawValue, - repeatCycle: state.selectedPeriod.rawValue, + repeatCycle: state.goalData.repeatCycle.rawValue, repeatCount: state.periodCount, - startDate: TXCalendarUtil.apiDateString(for: state.startDate), + startDate: TXCalendarUtil.apiDateString(for: state.goalData.startDate), endDate: endDateString ) return .run { send in @@ -235,15 +188,15 @@ extension MakeGoalReducer { case .edit: state.submitMessage = "수정 중..." - guard let goalId = state.editingGoalId else { + guard let goalId = state.goalData.goalId else { state.isLoading = false state.submitMessage = nil return .send(.showToast(.warning(message: "목표 수정에 실패했어요"))) } let request = GoalUpdateRequestDTO( - goalName: state.goalTitle, + goalName: state.goalData.title, icon: state.selectedEmoji.rawValue, - repeatCycle: state.selectedPeriod.rawValue, + repeatCycle: state.goalData.repeatCycle.rawValue, repeatCount: state.periodCount, endDate: endDateString ) @@ -265,12 +218,12 @@ extension MakeGoalReducer { return .none case .updateDateText: - guard let startDay = state.startDate.day, - let endDay = state.endDate.day + guard let startDay = state.goalData.startDate.day, + let endDay = state.goalData.endDate.day else { return .none} - state.startDateText = "\(state.startDate.month)월 \(startDay)일" - state.endDateText = "\(state.endDate.month)월 \(endDay)일" + state.startDateText = "\(state.goalData.startDate.month)월 \(startDay)일" + state.endDateText = "\(state.goalData.endDate.month)월 \(endDay)일" return .none case .delegate: diff --git a/Projects/Feature/MakeGoal/Sources/MakeGoalView.swift b/Projects/Feature/MakeGoal/Sources/MakeGoalView.swift index 46709366..8a4947e5 100644 --- a/Projects/Feature/MakeGoal/Sources/MakeGoalView.swift +++ b/Projects/Feature/MakeGoal/Sources/MakeGoalView.swift @@ -134,7 +134,7 @@ private extension MakeGoalView { var goalTitleField: some View { TXTextField( - text: $store.goalTitle, + text: $store.goalData.title, placeholderText: "목표를 입력해 보세요", isFocused: $isGoalTitleTextFieldFocused, submitLabel: .done, @@ -150,7 +150,7 @@ private extension MakeGoalView { divider endDateToggleRow - if store.isEndDateOn { + if store.goalData.isEndDateOn { divider endDateRow } @@ -204,7 +204,7 @@ private extension MakeGoalView { Spacer() - TXToggleSwitch(isOn: $store.isEndDateOn) + TXToggleSwitch(isOn: $store.goalData.isEndDateOn) } .frame(height: 32) .padding(.vertical, 16) @@ -281,7 +281,7 @@ private extension MakeGoalView { shape: .rect( style: .basic(text: store.weeklyPeriodText), size: .s, - state: store.selectedPeriod == .weekly ? .standard : .line + state: store.goalData.repeatCycle == .weekly ? .standard : .line ), onTap: { store.send(.periodSheetWeeklyTapped) } ) @@ -290,7 +290,7 @@ private extension MakeGoalView { shape: .rect( style: .basic(text: store.monthlyPeriodText), size: .s, - state: store.selectedPeriod == .monthly ? .standard : .line + state: store.goalData.repeatCycle == .monthly ? .standard : .line ), onTap: { store.send(.periodSheetMonthlyTapped) } ) @@ -355,13 +355,13 @@ private extension MakeGoalView { // MARK: - Private Methods private extension MakeGoalView { var validationState: TXTextField.SubTextConfiguration.State { - if store.goalTitle.isEmpty { + if store.goalData.title.isEmpty { return .empty } return store.isInvalidTitle ? .valid : .invalid } var selectedPeriodItem: PeriodItem { - PeriodItem(repeatCycle: store.selectedPeriod) + PeriodItem(repeatCycle: store.goalData.repeatCycle) } } diff --git a/Projects/Feature/Stats/Interface/Sources/Detail/StatsDetailReducer.swift b/Projects/Feature/Stats/Interface/Sources/Detail/StatsDetailReducer.swift index 965a94bb..0b9a4edd 100644 --- a/Projects/Feature/Stats/Interface/Sources/Detail/StatsDetailReducer.swift +++ b/Projects/Feature/Stats/Interface/Sources/Detail/StatsDetailReducer.swift @@ -10,6 +10,7 @@ import Foundation import ComposableArchitecture import DomainStatsInterface import FeatureCommonInterface +import FeatureMakeGoalInterface import SharedDesignSystem /// 통계 상세 화면의 상태와 액션을 관리하는 Reducer입니다. @@ -133,7 +134,7 @@ public struct StatsDetailReducer { public enum Delegate { case navigateBack case goToGoalDetail(goalId: Int64, isCompletedPartner: Bool, date: String) - case goToGoalEdit(goalId: Int64) + case goToGoalEdit(MakeGoalReducer.State.MakeGoal) } } diff --git a/Projects/Feature/Stats/Sources/Coordinator/StatsCoordinator+Impl.swift b/Projects/Feature/Stats/Sources/Coordinator/StatsCoordinator+Impl.swift index fd5238fe..3b84a15f 100644 --- a/Projects/Feature/Stats/Sources/Coordinator/StatsCoordinator+Impl.swift +++ b/Projects/Feature/Stats/Sources/Coordinator/StatsCoordinator+Impl.swift @@ -47,13 +47,9 @@ extension StatsCoordinator { ) return .none - case let .statsDetail(.delegate(.goToGoalEdit(goalId))): + case let .statsDetail(.delegate(.goToGoalEdit(goalData))): state.routes.append(.makeGoal) - state.makeGoal = .init( - category: .custom, - mode: .edit, - editingGoalId: goalId - ) + state.makeGoal = .init(mode: .edit(goalData)) return .none case .statsDetail(.delegate(.navigateBack)): diff --git a/Projects/Feature/Stats/Sources/Detail/StatsDetailReducer+Impl.swift b/Projects/Feature/Stats/Sources/Detail/StatsDetailReducer+Impl.swift index 68f481ee..bc55ce13 100644 --- a/Projects/Feature/Stats/Sources/Detail/StatsDetailReducer+Impl.swift +++ b/Projects/Feature/Stats/Sources/Detail/StatsDetailReducer+Impl.swift @@ -10,6 +10,7 @@ import Foundation import ComposableArchitecture import DomainStatsInterface import FeatureCommonInterface +import FeatureMakeGoalInterface import FeatureStatsInterface import SharedDesignSystem @@ -102,8 +103,7 @@ extension StatsDetailReducer { let goalItem = GoalEditCardItem( id: detail.goalId, goalName: detail.goalName, - // FIXME: - image 연결 - iconImage: .Icon.Illustration.default, + iconImage: GoalIcon(from: detail.goalIcon).thinImage, repeatCycle: summary.repeatCycle.text, startDate: summary.startDate, endDate: summary.endDate ?? "" @@ -113,7 +113,16 @@ extension StatsDetailReducer { switch item { case .edit: - return .send(.delegate(.goToGoalEdit(goalId: state.goalId))) + let goalData = MakeGoalReducer.State.MakeGoal( + goalId: state.goalId, + category: .custom, + icon: GoalIcon(from: detail.goalIcon), + title: detail.goalName, + repeatCycle: summary.repeatCycle, + startDate: summary.startDate, + endDate: summary.endDate + ) + return .send(.delegate(.goToGoalEdit(goalData))) case .finish: state.modal = .info( diff --git a/Projects/Feature/Stats/Sources/Detail/StatsDetailView.swift b/Projects/Feature/Stats/Sources/Detail/StatsDetailView.swift index fa69a413..e4b54617 100644 --- a/Projects/Feature/Stats/Sources/Detail/StatsDetailView.swift +++ b/Projects/Feature/Stats/Sources/Detail/StatsDetailView.swift @@ -155,6 +155,7 @@ private extension StatsDetailView { HStack(spacing: 28) { summaryTitle(for: summary.title) summartyContent(content: summary.content, isCompletedCount: summary.isCompletedCount) + .layoutPriority(1) Spacer() } @@ -187,6 +188,7 @@ private extension StatsDetailView { Text(content[0]) .typography(.b4_12b) .foregroundStyle(Color.Gray.gray500) + .lineLimit(1) if isCompletedCount { Text("|") @@ -197,8 +199,11 @@ private extension StatsDetailView { Text(content[1]) .typography(.b4_12b) .foregroundStyle(Color.Gray.gray500) + .lineLimit(1) } } + .lineLimit(1) + .fixedSize(horizontal: true, vertical: false) } @ViewBuilder diff --git a/Projects/Shared/DesignSystem/Sources/Components/Card/GoalEdit/GoalEditCardItem.swift b/Projects/Shared/DesignSystem/Sources/Components/Card/GoalEdit/GoalEditCardItem.swift index 190809ba..5a93c188 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/Card/GoalEdit/GoalEditCardItem.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/Card/GoalEdit/GoalEditCardItem.swift @@ -10,6 +10,7 @@ import SwiftUI public struct GoalEditCardItem: Identifiable, Equatable { public let id: Int64 public let goalName: String + public let goalIcon: GoalIcon? public let iconImage: Image public let repeatCycle: String public let startDate: String @@ -18,6 +19,7 @@ public struct GoalEditCardItem: Identifiable, Equatable { public init( id: Int64, goalName: String, + goalIcon: GoalIcon? = nil, iconImage: Image, repeatCycle: String, startDate: String, @@ -25,6 +27,7 @@ public struct GoalEditCardItem: Identifiable, Equatable { ) { self.id = id self.goalName = goalName + self.goalIcon = goalIcon self.iconImage = iconImage self.repeatCycle = repeatCycle self.startDate = startDate diff --git a/Projects/Shared/Util/Sources/Extension/String+Extensions.swift b/Projects/Shared/Util/Sources/Extension/String+Extensions.swift index e096d274..30066824 100644 --- a/Projects/Shared/Util/Sources/Extension/String+Extensions.swift +++ b/Projects/Shared/Util/Sources/Extension/String+Extensions.swift @@ -15,4 +15,12 @@ extension String { // components[0]: 년, [1]: 월, [2]: 일 return Self(format: "%d년 %d월 %d일", components[0], components[1], components[2]) } + + public var displayTextToAPIDateString: Self? { + let numbers = components(separatedBy: CharacterSet.decimalDigits.inverted) + .compactMap { Int($0) } + + guard numbers.count == 3 else { return nil } + return String(format: "%04d-%02d-%02d", numbers[0], numbers[1], numbers[2]) + } } From 8ff4ff0cde53b7ab28849f7719f0eb5eb97c657a Mon Sep 17 00:00:00 2001 From: jihun Date: Thu, 14 May 2026 14:19:48 +0900 Subject: [PATCH 32/44] =?UTF-8?q?feat:=20EditableGoal=20Entity=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20-=20#285?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Entity/EditableGoal.swift | 38 ++++++++++ .../MakeGoal/Interface/Sources/MakeGoal.swift | 69 ------------------- 2 files changed, 38 insertions(+), 69 deletions(-) create mode 100644 Projects/Domain/Goal/Interface/Sources/Entity/EditableGoal.swift delete mode 100644 Projects/Feature/MakeGoal/Interface/Sources/MakeGoal.swift diff --git a/Projects/Domain/Goal/Interface/Sources/Entity/EditableGoal.swift b/Projects/Domain/Goal/Interface/Sources/Entity/EditableGoal.swift new file mode 100644 index 00000000..caf417ae --- /dev/null +++ b/Projects/Domain/Goal/Interface/Sources/Entity/EditableGoal.swift @@ -0,0 +1,38 @@ +// +// GoalCreation.swift +// DomainGoalInterface +// +// Created by 정지훈 on 5/14/26. +// + +import Foundation + +import DomainCommonInterface + +public struct EditableGoal: Equatable, Identifiable { + public let id: Int64 + public let name: String + public let icon: String + public let repeatCycle: RepeatCycle + public let repeatCount: Int? + public let startDate: String + public let endDate: String? + + public init( + id: Int64, + name: String, + icon: String, + repeatCycle: RepeatCycle, + repeatCount: Int?, + startDate: String, + endDate: String? + ) { + self.id = id + self.name = name + self.icon = icon + self.repeatCycle = repeatCycle + self.repeatCount = repeatCount + self.startDate = startDate + self.endDate = endDate + } +} diff --git a/Projects/Feature/MakeGoal/Interface/Sources/MakeGoal.swift b/Projects/Feature/MakeGoal/Interface/Sources/MakeGoal.swift deleted file mode 100644 index 4065b085..00000000 --- a/Projects/Feature/MakeGoal/Interface/Sources/MakeGoal.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// MakeGoal.swift -// FeatureMakeGoal -// -// Created by 정지훈 on 5/12/26. -// - -import Foundation - -import DomainCommonInterface -import SharedDesignSystem -import SharedUtil - -public extension MakeGoalReducer.State { - public struct MakeGoal: Equatable { - public var goalId: Int64? - public var category: GoalCategory - public var icon: GoalIcon - public var title: String - public var repeatCycle: RepeatCycle - public var startDate: TXCalendarDate - public var endDate: TXCalendarDate - public var isEndDateOn: Bool = false - public var weeklyPeriodCount: Int - public var monthlyPeriodCount: Int - - public init( - goalId: Int64? = nil, - category: GoalCategory, - icon: GoalIcon? = nil, - title: String?, - repeatCycle: RepeatCycle? = nil, - startDate: String?, - endDate: String?, - weeklyPeriodCount: Int = 1, - monthlyPeriodCount: Int = 1 - ) { - let now = CalendarNow() - let today = TXCalendarDate( - year: now.year, - month: now.month, - day: now.day - ) - - self.goalId = goalId - self.category = category - self.icon = icon ?? GoalIcon.allCases[category.iconIndex] - self.title = title ?? category.title - self.repeatCycle = repeatCycle ?? category.repeatCycle - - if let startDateString = startDate, - let startDate = TXCalendarUtil.parseAPIDateString(startDateString) { - self.startDate = startDate - } else { - self.startDate = today - } - - if let endDateString = endDate, - let endDate = TXCalendarUtil.parseAPIDateString(endDateString) { - self.endDate = endDate - self.isEndDateOn = true - } else { - self.endDate = self.startDate - } - self.weeklyPeriodCount = weeklyPeriodCount - self.monthlyPeriodCount = monthlyPeriodCount - } - } -} From 8e55b1f15d5f8d8bf0e925f260264b35a120a4a1 Mon Sep 17 00:00:00 2001 From: jihun Date: Thu, 14 May 2026 14:20:47 +0900 Subject: [PATCH 33/44] =?UTF-8?q?refactor:=20EditableGoal=20Entity=20->=20?= =?UTF-8?q?MakeGoal=EC=97=90=EC=84=9C=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EB=8A=94=20GoalForm=20=ED=83=80=EC=9E=85=20=EB=B3=80=ED=99=98?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=9E=91=EC=97=85=20&=20state?= =?UTF-8?q?=20=EC=A0=95=EB=A6=AC=20-=20#285?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Interface/Sources/MakeGoalReducer.swift | 109 ++++++++++++++---- .../Sources/MakeGoalReducer+Impl.swift | 19 +-- .../MakeGoal/Sources/MakeGoalView.swift | 2 +- 3 files changed, 91 insertions(+), 39 deletions(-) diff --git a/Projects/Feature/MakeGoal/Interface/Sources/MakeGoalReducer.swift b/Projects/Feature/MakeGoal/Interface/Sources/MakeGoalReducer.swift index 27eede6f..feb72f0d 100644 --- a/Projects/Feature/MakeGoal/Interface/Sources/MakeGoalReducer.swift +++ b/Projects/Feature/MakeGoal/Interface/Sources/MakeGoalReducer.swift @@ -37,28 +37,27 @@ public struct MakeGoalReducer { /// let state = MakeGoalReducer.State(mode: .add(.book)) /// ``` public struct State: Equatable { + public let icons: [GoalIcon] = GoalIcon.allCases public let minimumPeriodCount = 1 public let weeklyMaximumPeriodCount = 6 public let monthlyMaximumPeriodCount = 25 - public let icons: [GoalIcon] = GoalIcon.allCases - public let dailyPeriodText: String = RepeatCycle.daily.text - public let weeklyPeriodText: String = RepeatCycle.weekly.text - public let monthlyPeriodText: String = RepeatCycle.monthly.text public var mode: Mode - public var goalData: MakeGoal + public var goalData: GoalForm public var calendarSheetDate: TXCalendarDate public var isCalendarSheetPresented: Bool = false public var calendarTarget: CalendarTarget? public var isPeriodSheetPresented: Bool = false - public var selectedEmojiIndex: Int public var isGoalTitleFocused: Bool = false - public var startDateText: String - public var endDateText: String public var showPeriodCount: Bool { goalData.repeatCycle != .daily } public var periodCountText: String { "\(goalData.repeatCycle.text) \(periodCount)번" } - public var selectedEmoji: GoalIcon { icons[selectedEmojiIndex] } + public var selectedEmojiIndex: Int { icons.firstIndex(of: goalData.icon) ?? 0 } + public var startDateText: String { "\(goalData.startDate.month)월 \(goalData.startDate.day ?? 1)일" } + public var endDateText: String { "\(goalData.endDate.month)월 \(goalData.endDate.day ?? 1)일" } + public var dailyPeriodText: String { RepeatCycle.daily.text } + public var weeklyPeriodText: String { RepeatCycle.weekly.text } + public var monthlyPeriodText: String { RepeatCycle.monthly.text } public var completeButtonDisabled: Bool { !isValidTitleLength || isLoading } public var isInvalidTitle: Bool { isValidTitleLength } public var isValidTitleLength: Bool { 2 <= goalData.title.count && goalData.title.count <= 14 } @@ -71,13 +70,70 @@ public struct MakeGoalReducer { /// 화면 모드를 구분합니다. public enum Mode: Equatable { case add(GoalCategory) - case edit(MakeGoal) + case edit(EditableGoal) } public enum CalendarTarget: Equatable { case startDate case endDate } + + public struct GoalForm: Equatable { + public var goalId: Int64? + public var category: GoalCategory? + public var icon: GoalIcon + public var title: String + public var repeatCycle: RepeatCycle + public var startDate: TXCalendarDate + public var endDate: TXCalendarDate + public var isEndDateOn: Bool + public var weeklyPeriodCount: Int + public var monthlyPeriodCount: Int + + public init( + category: GoalCategory, + today: TXCalendarDate, + minimumPeriodCount: Int + ) { + self.goalId = nil + self.category = category + self.icon = GoalIcon.allCases[category.iconIndex] + self.title = category != .custom ? category.title : "" + self.repeatCycle = category.repeatCycle + self.startDate = today + self.endDate = today + self.isEndDateOn = false + self.weeklyPeriodCount = category.repeatCycle == .weekly + ? category.repeatCount + : minimumPeriodCount + self.monthlyPeriodCount = category.repeatCycle == .monthly + ? category.repeatCount + : minimumPeriodCount + } + + public init( + editableGoal: EditableGoal, + today: TXCalendarDate, + minimumPeriodCount: Int + ) { + let startDate = TXCalendarUtil.parseAPIDateString(editableGoal.startDate) ?? today + + self.goalId = editableGoal.id + self.category = nil + self.icon = GoalIcon(from: editableGoal.icon) + self.title = editableGoal.name + self.repeatCycle = editableGoal.repeatCycle + self.startDate = startDate + self.endDate = editableGoal.endDate.flatMap(TXCalendarUtil.parseAPIDateString) ?? startDate + self.isEndDateOn = editableGoal.endDate != nil + self.weeklyPeriodCount = editableGoal.repeatCycle == .weekly + ? editableGoal.repeatCount ?? minimumPeriodCount + : minimumPeriodCount + self.monthlyPeriodCount = editableGoal.repeatCycle == .monthly + ? editableGoal.repeatCount ?? minimumPeriodCount + : minimumPeriodCount + } + } /// 목표 생성/수정 화면의 상태를 생성합니다. /// @@ -85,29 +141,34 @@ public struct MakeGoalReducer { /// ```swift /// let state = MakeGoalReducer.State(mode: .add(.book)) /// ``` - public init( - mode: Mode - ) { - let goalData: MakeGoal + public init(mode: Mode) { + let now = CalendarNow() + let today = TXCalendarDate( + year: now.year, + month: now.month, + day: now.day + ) + let goalData: GoalForm + switch mode { case let .add(category): - goalData = .init( + goalData = GoalForm( category: category, - title: category != .custom ? category.title : "", - startDate: nil, - endDate: nil + today: today, + minimumPeriodCount: minimumPeriodCount ) - case let .edit(makeGoal): - goalData = makeGoal + case let .edit(editableGoal): + goalData = GoalForm( + editableGoal: editableGoal, + today: today, + minimumPeriodCount: minimumPeriodCount + ) } self.mode = mode self.goalData = goalData - self.selectedEmojiIndex = GoalIcon.allCases.firstIndex(of: goalData.icon) ?? goalData.category.iconIndex self.calendarSheetDate = goalData.startDate - self.startDateText = "\(goalData.startDate.month)월 \(goalData.startDate.day ?? 1)일" - self.endDateText = "\(goalData.endDate.month)월 \(goalData.endDate.day ?? 1)일" } } @@ -142,7 +203,6 @@ public struct MakeGoalReducer { case startDateTapped case endDateTapped case monthCalendarConfirmTapped - case updateDateText case completeButtonTapped case navigationBackButtonTapped case modalConfirmTapped(Int) @@ -200,6 +260,7 @@ public extension MakeGoalReducer.State { case .startDate: let now = CalendarNow() return TXCalendarDate(year: now.year, month: now.month, day: now.day) + case .endDate: return goalData.startDate diff --git a/Projects/Feature/MakeGoal/Sources/MakeGoalReducer+Impl.swift b/Projects/Feature/MakeGoal/Sources/MakeGoalReducer+Impl.swift index 4eae05b1..418aa6b4 100644 --- a/Projects/Feature/MakeGoal/Sources/MakeGoalReducer+Impl.swift +++ b/Projects/Feature/MakeGoal/Sources/MakeGoalReducer+Impl.swift @@ -50,7 +50,7 @@ extension MakeGoalReducer { return .none case let .modalConfirmTapped(index): - state.selectedEmojiIndex = index + state.goalData.icon = state.icons[index] return .none case let .goalTitleFocusChanged(isFocused): @@ -129,7 +129,7 @@ extension MakeGoalReducer { } state.calendarSheetDate = state.goalData.endDate state.isCalendarSheetPresented = true - return .send(.updateDateText) + return .none case .monthCalendarConfirmTapped: guard let target = state.calendarTarget else { @@ -149,7 +149,7 @@ extension MakeGoalReducer { } state.isCalendarSheetPresented = false - return .send(.updateDateText) + return .none case .completeButtonTapped: guard !state.isLoading else { return .none } @@ -167,7 +167,7 @@ extension MakeGoalReducer { state.submitMessage = "등록 중..." let request = GoalCreateRequestDTO( name: state.goalData.title, - icon: state.selectedEmoji.rawValue, + icon: state.goalData.icon.rawValue, repeatCycle: state.goalData.repeatCycle.rawValue, repeatCount: state.periodCount, startDate: TXCalendarUtil.apiDateString(for: state.goalData.startDate), @@ -195,7 +195,7 @@ extension MakeGoalReducer { } let request = GoalUpdateRequestDTO( goalName: state.goalData.title, - icon: state.selectedEmoji.rawValue, + icon: state.goalData.icon.rawValue, repeatCycle: state.goalData.repeatCycle.rawValue, repeatCount: state.periodCount, endDate: endDateString @@ -217,15 +217,6 @@ extension MakeGoalReducer { state.toast = toast return .none - case .updateDateText: - guard let startDay = state.goalData.startDate.day, - let endDay = state.goalData.endDate.day - else { return .none} - - state.startDateText = "\(state.goalData.startDate.month)월 \(startDay)일" - state.endDateText = "\(state.goalData.endDate.month)월 \(endDay)일" - return .none - case .delegate: return .none diff --git a/Projects/Feature/MakeGoal/Sources/MakeGoalView.swift b/Projects/Feature/MakeGoal/Sources/MakeGoalView.swift index 8a4947e5..6e26984f 100644 --- a/Projects/Feature/MakeGoal/Sources/MakeGoalView.swift +++ b/Projects/Feature/MakeGoal/Sources/MakeGoalView.swift @@ -98,7 +98,7 @@ private extension MakeGoalView { } var emojiCircle: some View { - store.selectedEmoji.thinImage + store.goalData.icon.thinImage .resizable() .frame(width: 64, height: 64) .padding(22) From 68d9379cb34a459c1e4eade9f63a52d03260baae Mon Sep 17 00:00:00 2001 From: jihun Date: Thu, 14 May 2026 14:21:59 +0900 Subject: [PATCH 34/44] =?UTF-8?q?refactor:=20EditableGoal=EC=9D=84=20sourc?= =?UTF-8?q?e=20of=20truth=EB=A1=9C=20=EA=B0=96=EA=B2=8C=EB=81=94=20EditGoa?= =?UTF-8?q?lList=20=EC=88=98=EC=A0=95=20-=20#285?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Goal/EditGoalListReducer.swift | 7 +- .../Goal/EditGoalListReducer+Impl.swift | 75 +++++++++---------- 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/Projects/Feature/Home/Interface/Sources/Goal/EditGoalListReducer.swift b/Projects/Feature/Home/Interface/Sources/Goal/EditGoalListReducer.swift index 939e0d29..2b91479d 100644 --- a/Projects/Feature/Home/Interface/Sources/Goal/EditGoalListReducer.swift +++ b/Projects/Feature/Home/Interface/Sources/Goal/EditGoalListReducer.swift @@ -8,8 +8,8 @@ import Foundation import ComposableArchitecture +import DomainGoalInterface import FeatureCommonInterface -import FeatureMakeGoalInterface import SharedDesignSystem import SharedUtil import SwiftUI @@ -38,6 +38,7 @@ public struct EditGoalListReducer { public var calendarDate: TXCalendarDate public var calendarWeeks: [[TXCalendarDateItem]] + public var editableGoals: [EditableGoal]? public var cards: [GoalEditCardItem]? public var hasCards: Bool { !(cards?.isEmpty ?? true) } public var selectedCardMenu: GoalEditCardItem? @@ -85,7 +86,7 @@ public struct EditGoalListReducer { // MARK: - Update State case setCalendarDate(TXCalendarDate) case fetchGoals - case fetchGoalsCompleted([GoalEditCardItem], date: TXCalendarDate) + case fetchGoalsCompleted([EditableGoal], date: TXCalendarDate) case deleteGoalCompleted(goalId: Int64) case completeGoalCompleted(goalId: Int64) case apiError(String) @@ -96,7 +97,7 @@ public struct EditGoalListReducer { public enum Delegate { case navigateBack - case goToGoalEdit(MakeGoalReducer.State.MakeGoal) + case goToGoalEdit(EditableGoal) case goToCompletedStats } } diff --git a/Projects/Feature/Home/Sources/Goal/EditGoalListReducer+Impl.swift b/Projects/Feature/Home/Sources/Goal/EditGoalListReducer+Impl.swift index 3f948a1f..36cf2b46 100644 --- a/Projects/Feature/Home/Sources/Goal/EditGoalListReducer+Impl.swift +++ b/Projects/Feature/Home/Sources/Goal/EditGoalListReducer+Impl.swift @@ -13,9 +13,7 @@ import DomainCommonInterface import DomainGoalInterface import FeatureCommonInterface import FeatureHomeInterface -import FeatureMakeGoalInterface import SharedDesignSystem -import SharedUtil extension EditGoalListReducer { /// 실제 로직을 포함한 EditGoalListReducer를 생성합니다. @@ -89,16 +87,10 @@ extension EditGoalListReducer { if isPast { state.toast = .warning(message: "이미 완료한 목표입니다!") } else { - let goalData = MakeGoalReducer.State.MakeGoal( - goalId: card.id, - category: .custom, - icon: card.goalIcon, - title: card.goalName, - repeatCycle: RepeatCycle(displayText: card.repeatCycle), - startDate: card.startDate.displayTextToAPIDateString, - endDate: card.endDate.displayTextToAPIDateString - ) - return .send(.delegate(.goToGoalEdit(goalData))) + guard let editableGoal = state.editableGoals?.first(where: { $0.id == card.id }) else { + return .send(.apiError("목표 수정에 실패했어요")) + } + return .send(.delegate(.goToGoalEdit(editableGoal))) } case .finish: @@ -181,29 +173,46 @@ extension EditGoalListReducer { let date = state.calendarDate return .run { send in do { - let items = try await goalClient.fetchGoalEditList(TXCalendarUtil.apiDateString(for: date)) - let editItems = items.map { - GoalEditCardItem( - id: $0.id, - goalName: $0.title, - goalIcon: GoalIcon(from: $0.goalIcon), - iconImage: GoalIcon(from: $0.goalIcon).thinImage, - repeatCycle: $0.repeatCycle?.text ?? "", - startDate: $0.startDate?.dateDisplayString ?? "", - endDate: $0.endDate?.dateDisplayString ?? "미설정" - ) - } - await send(.fetchGoalsCompleted(editItems, date: date)) + let goals = try await goalClient.fetchGoalEditList(TXCalendarUtil.apiDateString(for: date)) + .compactMap { goal -> EditableGoal? in + guard let repeatCycle = goal.repeatCycle, + let startDate = goal.startDate else { + return nil + } + + return EditableGoal( + id: goal.id, + name: goal.title, + icon: goal.goalIcon, + repeatCycle: repeatCycle, + repeatCount: goal.repeatCount, + startDate: startDate, + endDate: goal.endDate + ) + } + await send(.fetchGoalsCompleted(goals, date: date)) } catch { await send(.apiError("목표 조회에 실패했어요")) } } - case let .fetchGoalsCompleted(items, date): + case let .fetchGoalsCompleted(goals, date): if date != state.calendarDate { return .none } state.isLoading = false + state.editableGoals = goals + let items = goals.map { + GoalEditCardItem( + id: $0.id, + goalName: $0.name, + goalIcon: GoalIcon(from: $0.icon), + iconImage: GoalIcon(from: $0.icon).thinImage, + repeatCycle: $0.repeatCycle.text, + startDate: $0.startDate.dateDisplayString, + endDate: $0.endDate?.dateDisplayString ?? "미설정" + ) + } if state.cards != items { state.cards = items } @@ -213,6 +222,7 @@ extension EditGoalListReducer { state.isLoading = false state.pendingGoalId = nil state.pendingAction = nil + state.editableGoals?.removeAll { $0.id == goalId } state.cards?.removeAll { $0.id == goalId } return .send(.showToast(.delete(message: "목표가 삭제되었어요"))) @@ -220,6 +230,7 @@ extension EditGoalListReducer { state.isLoading = false state.pendingGoalId = nil state.pendingAction = nil + state.editableGoals?.removeAll { $0.id == goalId } state.cards?.removeAll { $0.id == goalId } return .send(.showToast(.success(message: "목표를 이뤘어요", buttonText: "보러가기"))) @@ -244,15 +255,3 @@ extension EditGoalListReducer { self.init(reducer: reducer) } } - -/// 재사용될 시 RepeatCycle 내부로 이동 -private extension RepeatCycle { - init?(displayText: String) { - switch displayText { - case RepeatCycle.daily.text: self = .daily - case RepeatCycle.weekly.text: self = .weekly - case RepeatCycle.monthly.text: self = .monthly - default: return nil - } - } -} From 0e12c5a81582e3e65af934c73be11a3d0d3eaad9 Mon Sep 17 00:00:00 2001 From: jihun Date: Thu, 14 May 2026 14:22:24 +0900 Subject: [PATCH 35/44] =?UTF-8?q?refactor:=20StatsDetail=20EditableGoal=20?= =?UTF-8?q?=EC=A3=BC=EC=9E=85=20-=20#285?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Detail/StatsDetailReducer.swift | 4 ++-- Projects/Feature/Stats/Project.swift | 2 ++ .../Sources/Detail/StatsDetailReducer+Impl.swift | 14 +++++++------- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Projects/Feature/Stats/Interface/Sources/Detail/StatsDetailReducer.swift b/Projects/Feature/Stats/Interface/Sources/Detail/StatsDetailReducer.swift index 0b9a4edd..46b79833 100644 --- a/Projects/Feature/Stats/Interface/Sources/Detail/StatsDetailReducer.swift +++ b/Projects/Feature/Stats/Interface/Sources/Detail/StatsDetailReducer.swift @@ -8,9 +8,9 @@ import Foundation import ComposableArchitecture +import DomainGoalInterface import DomainStatsInterface import FeatureCommonInterface -import FeatureMakeGoalInterface import SharedDesignSystem /// 통계 상세 화면의 상태와 액션을 관리하는 Reducer입니다. @@ -134,7 +134,7 @@ public struct StatsDetailReducer { public enum Delegate { case navigateBack case goToGoalDetail(goalId: Int64, isCompletedPartner: Bool, date: String) - case goToGoalEdit(MakeGoalReducer.State.MakeGoal) + case goToGoalEdit(EditableGoal) } } diff --git a/Projects/Feature/Stats/Project.swift b/Projects/Feature/Stats/Project.swift index 45c10a5e..86cb3b12 100644 --- a/Projects/Feature/Stats/Project.swift +++ b/Projects/Feature/Stats/Project.swift @@ -8,6 +8,7 @@ let project = Project.makeModule( interface: .stats, config: .init( dependencies: [ + .domain(interface: .goal), .domain(interface: .stats), .feature(interface: .common), .shared(implements: .designSystem), @@ -22,6 +23,7 @@ let project = Project.makeModule( config: .init( dependencies: [ .domain(interface: .common), + .domain(interface: .goal), .feature(interface: .common), .feature(interface: .stats), .feature(interface: .goalDetail), diff --git a/Projects/Feature/Stats/Sources/Detail/StatsDetailReducer+Impl.swift b/Projects/Feature/Stats/Sources/Detail/StatsDetailReducer+Impl.swift index bc55ce13..531f0c34 100644 --- a/Projects/Feature/Stats/Sources/Detail/StatsDetailReducer+Impl.swift +++ b/Projects/Feature/Stats/Sources/Detail/StatsDetailReducer+Impl.swift @@ -8,9 +8,9 @@ import Foundation import ComposableArchitecture +import DomainGoalInterface import DomainStatsInterface import FeatureCommonInterface -import FeatureMakeGoalInterface import FeatureStatsInterface import SharedDesignSystem @@ -113,16 +113,16 @@ extension StatsDetailReducer { switch item { case .edit: - let goalData = MakeGoalReducer.State.MakeGoal( - goalId: state.goalId, - category: .custom, - icon: GoalIcon(from: detail.goalIcon), - title: detail.goalName, + let editableGoal = EditableGoal( + id: state.goalId, + name: detail.goalName, + icon: detail.goalIcon, repeatCycle: summary.repeatCycle, + repeatCount: nil, startDate: summary.startDate, endDate: summary.endDate ) - return .send(.delegate(.goToGoalEdit(goalData))) + return .send(.delegate(.goToGoalEdit(editableGoal))) case .finish: state.modal = .info( From adfae784b78c5c6d6600c0807b4dd4f054531d50 Mon Sep 17 00:00:00 2001 From: jihun Date: Thu, 14 May 2026 14:52:35 +0900 Subject: [PATCH 36/44] =?UTF-8?q?fix:=20=EC=B0=8C=EB=A5=B4=EA=B8=B0=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=EB=B0=9B=EA=B8=B0=20=EC=A0=84=20=EB=82=A0?= =?UTF-8?q?=EC=A7=9C=20=EB=B3=80=EA=B2=BD=20=EC=8B=9C=20=EB=8B=A4=EB=A5=B8?= =?UTF-8?q?=20=EB=82=A0=EC=A7=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=ED=95=A0=20=EC=88=98=20=EC=9E=88?= =?UTF-8?q?=EB=8D=98=20=EB=A6=AC=EC=8A=A4=ED=81=AC=20=EC=A0=9C=EA=B1=B0=20?= =?UTF-8?q?-=20#285?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Interface/Sources/Home/HomeReducer.swift | 2 +- .../Home/Sources/Home/HomeReducer+Impl.swift | 35 ++++++++----------- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/Projects/Feature/Home/Interface/Sources/Home/HomeReducer.swift b/Projects/Feature/Home/Interface/Sources/Home/HomeReducer.swift index 1bd6c0af..fbd0281a 100644 --- a/Projects/Feature/Home/Interface/Sources/Home/HomeReducer.swift +++ b/Projects/Feature/Home/Interface/Sources/Home/HomeReducer.swift @@ -117,7 +117,7 @@ public struct HomeReducer { case setCalendarDate(TXCalendarDate) case setCalendarSheetPresented(Bool) case showToast(TXToastType) - case setPokeButtonDisabled(goalId: Int64, Bool) + case setPokeButtonDisabled(goalId: Int64, Bool, date: TXCalendarDate) case authorizationCompleted(id: Int64, isAuthorized: Bool) case proofPhotoDismissed case addGoalButtonTapped(GoalCategory) diff --git a/Projects/Feature/Home/Sources/Home/HomeReducer+Impl.swift b/Projects/Feature/Home/Sources/Home/HomeReducer+Impl.swift index d975680f..5bf7f5ea 100644 --- a/Projects/Feature/Home/Sources/Home/HomeReducer+Impl.swift +++ b/Projects/Feature/Home/Sources/Home/HomeReducer+Impl.swift @@ -234,15 +234,16 @@ extension HomeReducer { } // 상대방 미인증 시 찌르기 API 호출 let goalId = card.id - setPokeButtonDisabled(state: &state, goalId: goalId, isDisabled: true) + let pokeDate = state.calendarDate return .run { send in PokeCooldownManager.recordPoke(goalId: goalId) do { try await goalClient.pokePartner(goalId) + await send(.setPokeButtonDisabled(goalId: goalId, true, date: pokeDate)) await send(.showToast(.poke(message: "상대방을 찔렀어요!"))) } catch { PokeCooldownManager.removePoke(goalId: goalId) - await send(.setPokeButtonDisabled(goalId: goalId, false)) + await send(.setPokeButtonDisabled(goalId: goalId, false, date: pokeDate)) await send(.showToast(.warning(message: "찌르기에 실패했어요"))) } } @@ -314,7 +315,7 @@ extension HomeReducer { } state.isLoading = false - + if state.items != items { state.items = items } @@ -380,9 +381,17 @@ extension HomeReducer { case let .showToast(toast): state.toast = toast return .none - - case let .setPokeButtonDisabled(goalId, isDisabled): - setPokeButtonDisabled(state: &state, goalId: goalId, isDisabled: isDisabled) + + case let .setPokeButtonDisabled(goalId, isDisabled, date): + if date == state.calendarDate { + updatePokeButtonDisabled(in: &state.items, goalId: goalId, isDisabled: isDisabled) + } + + let cacheKey = TXCalendarUtil.apiDateString(for: date) + guard var cachedItems = state.goalsCache[cacheKey] else { return .none } + + updatePokeButtonDisabled(in: &cachedItems, goalId: goalId, isDisabled: isDisabled) + state.goalsCache[cacheKey] = cachedItems return .none case let .authorizationCompleted(id, isAuthorized): @@ -484,7 +493,6 @@ extension HomeReducer { proofPhotoReducer: proofPhotoReducer ) } - } private extension HomeGoalItem { @@ -501,19 +509,6 @@ private func refreshPokeCooldownStates(state: inout HomeReducer.State) { state.goalsCache[TXCalendarUtil.apiDateString(for: state.calendarDate)] = state.items } -private func setPokeButtonDisabled( - state: inout HomeReducer.State, - goalId: Int64, - isDisabled: Bool -) { - updatePokeButtonDisabled(in: &state.items, goalId: goalId, isDisabled: isDisabled) - - let cacheKey = TXCalendarUtil.apiDateString(for: state.calendarDate) - guard var cachedItems = state.goalsCache[cacheKey] else { return } - updatePokeButtonDisabled(in: &cachedItems, goalId: goalId, isDisabled: isDisabled) - state.goalsCache[cacheKey] = cachedItems -} - private func updatePokeButtonDisabled( in items: inout [HomeGoalItem], goalId: Int64, From cd4e7112510edf27effc416badfcb414db29ca1d Mon Sep 17 00:00:00 2001 From: Jiyong Jung Date: Thu, 14 May 2026 12:50:29 +0900 Subject: [PATCH 37/44] =?UTF-8?q?fix:=20GitHub=20Actions=EC=97=90=EC=84=9C?= =?UTF-8?q?=EC=9D=98=20MacOS=20=EB=B2=84=EC=A0=84=20=EB=B3=80=EA=B2=BD=20-?= =?UTF-8?q?=20#300?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/actions/setup-build-env/action.yml | 30 ++++++++++++++++++++-- .github/workflows/cd_develop.yml | 2 +- .github/workflows/cd_main.yml | 2 +- .github/workflows/ci_pr.yml | 6 ++--- 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/.github/actions/setup-build-env/action.yml b/.github/actions/setup-build-env/action.yml index 397c6700..9fafe3bd 100644 --- a/.github/actions/setup-build-env/action.yml +++ b/.github/actions/setup-build-env/action.yml @@ -33,6 +33,32 @@ runs: security list-keychains -d user -s "$KEYCHAIN_NAME" "$(security default-keychain | tr -d '"')" security default-keychain -s "$KEYCHAIN_NAME" + - name: 🧰 Select Xcode + shell: bash + run: | + set -euo pipefail + XCODE_APP="/Applications/Xcode_26.4.1.app" + if [ -d "$XCODE_APP" ]; then + sudo xcode-select -s "$XCODE_APP/Contents/Developer" + else + echo "::notice::$XCODE_APP is not installed. Falling back to the runner default Xcode." + fi + + - name: 🧰 Verify Xcode SDK + shell: bash + run: | + set -euo pipefail + xcodebuild -version + SDK_VERSION="$(xcrun --sdk iphoneos --show-sdk-version)" + echo "iPhoneOS SDK: $SDK_VERSION" + case "$SDK_VERSION" in + 26.*|[3-9][0-9].*) ;; + *) + echo "::error::iPhoneOS SDK $SDK_VERSION is too old for App Store Connect upload. Use a runner with Xcode 26 or later." + exit 1 + ;; + esac + - name: 💫 Set up Ruby uses: ruby/setup-ruby@v1 with: @@ -66,9 +92,9 @@ runs: uses: actions/cache@v4 with: path: Tuist/.build - key: tuist-spm-${{ runner.os }}-${{ hashFiles('Tuist/Package.resolved') }} + key: tuist-spm-xcode26-${{ runner.os }}-${{ hashFiles('Tuist/Package.resolved') }} restore-keys: | - tuist-spm-${{ runner.os }}- + tuist-spm-xcode26-${{ runner.os }}- - name: 🛠 Install tuist dependencies if: inputs.skip-spm-cache == 'true' || steps.spm-cache.outputs.cache-hit != 'true' diff --git a/.github/workflows/cd_develop.yml b/.github/workflows/cd_develop.yml index 177610ca..5d58d928 100644 --- a/.github/workflows/cd_develop.yml +++ b/.github/workflows/cd_develop.yml @@ -10,7 +10,7 @@ concurrency: jobs: deploy_develop: - runs-on: macos-15 + runs-on: macos-26 steps: - name: 🔄 Checkout source code uses: actions/checkout@v4 diff --git a/.github/workflows/cd_main.yml b/.github/workflows/cd_main.yml index 92d1156e..aa74f852 100644 --- a/.github/workflows/cd_main.yml +++ b/.github/workflows/cd_main.yml @@ -10,7 +10,7 @@ concurrency: jobs: deploy_main: - runs-on: macos-15 + runs-on: macos-26 steps: - name: 🔄 Checkout source code uses: actions/checkout@v4 diff --git a/.github/workflows/ci_pr.yml b/.github/workflows/ci_pr.yml index dc0b55ba..b07b2bc3 100644 --- a/.github/workflows/ci_pr.yml +++ b/.github/workflows/ci_pr.yml @@ -9,7 +9,7 @@ concurrency: jobs: build: - runs-on: macos-15 + runs-on: macos-26 steps: - name: 🔄 Checkout source code uses: actions/checkout@v4 @@ -24,9 +24,9 @@ jobs: uses: actions/cache@v4 with: path: ~/Library/Developer/Xcode/DerivedData - key: deriveddata-${{ runner.os }}-${{ hashFiles('Tuist/Package.resolved') }} + key: deriveddata-xcode26-${{ runner.os }}-${{ hashFiles('Tuist/Package.resolved') }} restore-keys: | - deriveddata-${{ runner.os }}- + deriveddata-xcode26-${{ runner.os }}- - name: 🧪 Run PR Checks (build + test) env: From 7def1aac1e1c4a9307e6d69dcdf375491f27b887 Mon Sep 17 00:00:00 2001 From: Jiyong Jung Date: Thu, 14 May 2026 16:39:19 +0900 Subject: [PATCH 38/44] =?UTF-8?q?chore:=20TCA=20=EB=B2=84=EC=A0=84=20?= =?UTF-8?q?=EC=97=85=EA=B7=B8=EB=A0=88=EC=9D=B4=EB=93=9C=20-=20#300?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Tuist/Package.resolved | 10 +++++----- Tuist/Package.swift | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Tuist/Package.resolved b/Tuist/Package.resolved index c66945c4..073520b3 100644 --- a/Tuist/Package.resolved +++ b/Tuist/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "bd613a373f02fcf21f1fd25f63850e7df57374983fdffc49444594756fc35dbd", + "originHash" : "9197694783e05e3348510c9f23717c36107095d2e7c251bba02755efaf40ed72", "pins" : [ { "identity" : "abseil-cpp-binary", @@ -222,8 +222,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-composable-architecture", "state" : { - "revision" : "5b0890fabfd68a2d375d68502bc3f54a8548c494", - "version" : "1.23.1" + "revision" : "1eaa6fa2ee57ac42843283b9fd3457af408c858d", + "version" : "1.25.5" } }, { @@ -267,8 +267,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-navigation", "state" : { - "revision" : "bf498690e1f6b4af790260f542e8428a4ba10d78", - "version" : "2.6.0" + "revision" : "32f35241b8be0719c4c7f00eb27713b1cadb6248", + "version" : "2.8.0" } }, { diff --git a/Tuist/Package.swift b/Tuist/Package.swift index 27a577ce..ced539b1 100644 --- a/Tuist/Package.swift +++ b/Tuist/Package.swift @@ -24,7 +24,7 @@ import PackageDescription let package = Package( name: "Twix", dependencies: [ - .package(url: "https://github.com/pointfreeco/swift-composable-architecture", from: "1.23.1"), + .package(url: "https://github.com/pointfreeco/swift-composable-architecture", from: "1.23.2"), .package(url: "https://github.com/onevcat/Kingfisher", from: "8.0.0"), .package(url: "https://github.com/kean/Pulse", from: "5.1.4"), .package(url: "https://github.com/kakao/kakao-ios-sdk", from: "2.27.1"), From 84b213def57f9816d2da789b67e0b613893e8a46 Mon Sep 17 00:00:00 2001 From: Jiyong Jung Date: Thu, 14 May 2026 17:18:28 +0900 Subject: [PATCH 39/44] =?UTF-8?q?chore:=20tuist=20=EB=B2=84=EC=A0=84=20?= =?UTF-8?q?=EC=97=85=EA=B7=B8=EB=A0=88=EC=9D=B4=EB=93=9C=20-=20#300?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/actions/setup-build-env/action.yml | 4 ++-- Makefile | 2 +- mise.toml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/actions/setup-build-env/action.yml b/.github/actions/setup-build-env/action.yml index 9fafe3bd..b237634c 100644 --- a/.github/actions/setup-build-env/action.yml +++ b/.github/actions/setup-build-env/action.yml @@ -70,13 +70,13 @@ runs: uses: actions/cache@v4 with: path: ~/.tuist-bin - key: tuist-binary-${{ runner.os }}-4.118.0 + key: tuist-binary-${{ runner.os }}-4.194.0 - name: 🛠 Install Tuist (manual binary) if: steps.tuist-binary-cache.outputs.cache-hit != 'true' shell: bash run: | - TUIST_VERSION=4.118.0 + TUIST_VERSION=4.194.0 mkdir -p ~/.tuist-bin curl -L "https://github.com/tuist/tuist/releases/download/${TUIST_VERSION}/tuist.zip" -o tuist.zip unzip -o tuist.zip -d ~/.tuist-bin diff --git a/Makefile b/Makefile index e9b5aac2..20cdec88 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ init: mise exec node@24 -- node -v mise use --global node@24 go@1 mise install tuist - mise use tuist@4.115.1 + mise use tuist@4.194.0 clean: tuist clean diff --git a/mise.toml b/mise.toml index bc882f5c..a364dea5 100644 --- a/mise.toml +++ b/mise.toml @@ -1,2 +1,2 @@ [tools] -tuist = "4.118.0" +tuist = "4.194.0" From 21ec4f3eddece031ab5c3230b2dcf8108f1a5a33 Mon Sep 17 00:00:00 2001 From: Jiyong Jung Date: Thu, 14 May 2026 17:38:17 +0900 Subject: [PATCH 40/44] =?UTF-8?q?fix:=20Firebase=20=EB=B9=8C=EB=93=9C=20?= =?UTF-8?q?=EC=8B=A4=ED=8C=A8=20=EA=B4=80=EB=A0=A8=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?-=20#300?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Tuist/Package.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Tuist/Package.swift b/Tuist/Package.swift index ced539b1..1c5a8892 100644 --- a/Tuist/Package.swift +++ b/Tuist/Package.swift @@ -16,7 +16,9 @@ import PackageDescription "FirebaseAnalytics": .staticLibrary, "FirebaseRemoteConfig" : .staticLibrary, "FirebaseMessaging": .staticLibrary, - "FirebaseCrashlytics": .staticLibrary + "FirebaseCrashlytics": .staticLibrary, + "FirebaseAnalyticsTarget": .framework, + "FirebaseAnalyticsWrapper": .framework ] ) #endif From 3537cc534b67da659968ee2f4aa7239c2b4e8268 Mon Sep 17 00:00:00 2001 From: Jiyong Jung Date: Thu, 14 May 2026 22:00:01 +0900 Subject: [PATCH 41/44] =?UTF-8?q?refactor:=20Firebase=20wrapper=20productT?= =?UTF-8?q?ype=20=EC=98=A4=EB=B2=84=EB=9D=BC=EC=9D=B4=EB=93=9C=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20-=20#300?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Tuist/Package.swift | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/Tuist/Package.swift b/Tuist/Package.swift index 1c5a8892..05c0e2cb 100644 --- a/Tuist/Package.swift +++ b/Tuist/Package.swift @@ -11,14 +11,7 @@ import PackageDescription "Pulse": .framework, "KakaoSDK": .staticLibrary, "GoogleSignIn": .staticLibrary, - "GoogleSignInSwift": .staticLibrary, - "FirebaseCore": .staticLibrary, - "FirebaseAnalytics": .staticLibrary, - "FirebaseRemoteConfig" : .staticLibrary, - "FirebaseMessaging": .staticLibrary, - "FirebaseCrashlytics": .staticLibrary, - "FirebaseAnalyticsTarget": .framework, - "FirebaseAnalyticsWrapper": .framework + "GoogleSignInSwift": .staticLibrary ] ) #endif From 8c104e9ac89458ee539aab0799ffdf9730a5d401 Mon Sep 17 00:00:00 2001 From: Jiyong Jung Date: Thu, 14 May 2026 22:00:17 +0900 Subject: [PATCH 42/44] =?UTF-8?q?chore:=20Firebase=20SDK=2012.13.0=20?= =?UTF-8?q?=EC=97=85=EA=B7=B8=EB=A0=88=EC=9D=B4=EB=93=9C=20-=20#300?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Tuist/Package.resolved | 14 +++++++------- Tuist/Package.swift | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Tuist/Package.resolved b/Tuist/Package.resolved index 073520b3..2e613ec7 100644 --- a/Tuist/Package.resolved +++ b/Tuist/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "9197694783e05e3348510c9f23717c36107095d2e7c251bba02755efaf40ed72", + "originHash" : "5c0d0ed23de9ecd5ee8f58f55c05e9268ba72f94994e48c0f6f308e2b3d7430e", "pins" : [ { "identity" : "abseil-cpp-binary", @@ -51,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/firebase-ios-sdk", "state" : { - "revision" : "9b3aed4fa6226125305b82d4d86c715bef250785", - "version" : "12.9.0" + "revision" : "d10045cace0b4c335c4efa8f7df7e9a9fc5a7c60", + "version" : "12.13.0" } }, { @@ -60,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk", "state" : { - "revision" : "35b601a60fbbea2de3ea461f604deaaa4d8bbd0c", - "version" : "3.2.0" + "revision" : "19dffda9a9caf8d86570ff846535902d8509d7bf", + "version" : "3.5.0" } }, { @@ -69,8 +69,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleAppMeasurement.git", "state" : { - "revision" : "2ffd220823f3716904733162e9ae685545c276d1", - "version" : "12.8.0" + "revision" : "c2c76bebcfbb90d90ea10599f934f9af160e1604", + "version" : "12.13.0" } }, { diff --git a/Tuist/Package.swift b/Tuist/Package.swift index 05c0e2cb..750193a4 100644 --- a/Tuist/Package.swift +++ b/Tuist/Package.swift @@ -24,6 +24,6 @@ let package = Package( .package(url: "https://github.com/kean/Pulse", from: "5.1.4"), .package(url: "https://github.com/kakao/kakao-ios-sdk", from: "2.27.1"), .package(url: "https://github.com/google/GoogleSignIn-iOS", from: "9.1.0"), - .package(url: "https://github.com/firebase/firebase-ios-sdk", from: "12.9.0") + .package(url: "https://github.com/firebase/firebase-ios-sdk", from: "12.13.0") ] ) From 4197d5e5a2eb05f776be638bc1b68b06ae83fb87 Mon Sep 17 00:00:00 2001 From: Jiyong Jung Date: Thu, 14 May 2026 22:00:29 +0900 Subject: [PATCH 43/44] =?UTF-8?q?fix:=20tuist=20binary=20cache=20=EB=B9=84?= =?UTF-8?q?=ED=99=9C=EC=84=B1=ED=99=94=EB=A1=9C=20=EB=B9=8C=EB=93=9C=20?= =?UTF-8?q?=EC=8B=A4=ED=8C=A8=20=ED=95=B4=EA=B2=B0=20-=20#300?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/actions/setup-build-env/action.yml | 18 ++---------------- .github/workflows/cd_main.yml | 1 - Makefile | 2 +- fastlane/Fastfile | 9 +++++++-- 4 files changed, 10 insertions(+), 20 deletions(-) diff --git a/.github/actions/setup-build-env/action.yml b/.github/actions/setup-build-env/action.yml index b237634c..4ca30ffa 100644 --- a/.github/actions/setup-build-env/action.yml +++ b/.github/actions/setup-build-env/action.yml @@ -8,10 +8,6 @@ inputs: keychain-password: description: "Keychain password" required: true - include-pulse: - description: "Whether to include Pulse in cache (false for production builds)" - required: false - default: "true" skip-spm-cache: description: "Skip SPM cache for release builds to avoid simulator artifacts" required: false @@ -107,17 +103,7 @@ runs: if [ "${{ inputs.skip-spm-cache }}" == "true" ]; then echo "⏭️ SPM cache SKIPPED (release build)" elif [ "${{ steps.spm-cache.outputs.cache-hit }}" == "true" ]; then - echo "✅ SPM cache HIT - skipping tuist cache warm up" + echo "✅ SPM cache HIT - using restored Tuist/.build" else - echo "❌ SPM cache MISS - will build dependencies" + echo "❌ SPM cache MISS - dependencies will be compiled from source" fi - - - name: 🏗 Warm up external dependencies (with Pulse) - if: inputs.skip-spm-cache != 'true' && steps.spm-cache.outputs.cache-hit != 'true' && inputs.include-pulse == 'true' - shell: bash - run: tuist cache ComposableArchitecture Kingfisher Pulse KakaoSDKAuth KakaoSDKCommon GoogleSignIn GoogleSignInSwift - - - name: 🏗 Warm up external dependencies (without Pulse) - if: inputs.skip-spm-cache != 'true' && steps.spm-cache.outputs.cache-hit != 'true' && inputs.include-pulse != 'true' - shell: bash - run: tuist cache ComposableArchitecture Kingfisher KakaoSDKAuth KakaoSDKCommon GoogleSignIn GoogleSignInSwift diff --git a/.github/workflows/cd_main.yml b/.github/workflows/cd_main.yml index aa74f852..0f505d88 100644 --- a/.github/workflows/cd_main.yml +++ b/.github/workflows/cd_main.yml @@ -22,7 +22,6 @@ jobs: with: keychain-name: ${{ secrets.KEYCHAIN_NAME }} keychain-password: ${{ secrets.KEYCHAIN_PASSWORD }} - include-pulse: "false" skip-spm-cache: "true" - name: 🚀 Deploy to App Store Connect (Twix) diff --git a/Makefile b/Makefile index 20cdec88..feaa0df8 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ clean: generate: tuist install - tuist generate + tuist generate --cache-profile none module: swift Scripts/GenerateModule.swift diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 6b1b9824..76ed1422 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -103,18 +103,23 @@ platform :ios do lane :generate do UI.message("🏗️ Generating Tuist project...") + # --cache-profile none: SPM 의존성을 source에서 컴파일. + # Tuist의 binary cache(~/.cache/tuist/Binaries)가 채워져 있을 때 + # 다수의 XCFramework 경로가 link line에 누적되어 ARG_MAX 초과로 + # `Argument list too long` 빌드 실패가 발생하는 것을 방지. if ENV["CI"] == "true" - sh("cd .. && tuist generate") + sh("cd .. && tuist generate --no-open --cache-profile none") else sh("cd .. && make generate") end - + UI.success("✅ Tuist project generated!") UI.message("📋 Available schemes:") sh("xcodebuild -workspace ../#{WORKSPACE_NAME} -list") end + desc "Build all modules (without main app)" lane :build_modules do UI.message("🔨 Building all modules...") From 0a44c1a53f8682d23bdc9fae653bcedfbc158351 Mon Sep 17 00:00:00 2001 From: Jiyong Jung Date: Thu, 14 May 2026 22:00:36 +0900 Subject: [PATCH 44/44] =?UTF-8?q?feat:=20Crashlytics=20dSYM=20=EC=97=85?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=20=EA=B2=AC=EA=B3=A0=ED=99=94=20-=20#300?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Scripts/CrashlyticsScript.swift | 53 +++++++++++++------ fastlane/Fastfile | 12 +++++ 2 files changed, 50 insertions(+), 15 deletions(-) diff --git a/Tuist/ProjectDescriptionHelpers/Scripts/CrashlyticsScript.swift b/Tuist/ProjectDescriptionHelpers/Scripts/CrashlyticsScript.swift index 139204dc..dd91c6cf 100644 --- a/Tuist/ProjectDescriptionHelpers/Scripts/CrashlyticsScript.swift +++ b/Tuist/ProjectDescriptionHelpers/Scripts/CrashlyticsScript.swift @@ -3,27 +3,50 @@ import ProjectDescription public extension TargetScript { /// Firebase Crashlytics dSYM 업로드 스크립트입니다. /// - /// Archive 빌드 시 생성된 dSYM을 Firebase Console에 업로드해 크래시 스택을 심볼화합니다. - /// SPM 기준 경로를 사용하며, `basedOnDependencyAnalysis: false`로 항상 실행됩니다. - /// Tuist는 SPM 패키지를 Tuist/.build/checkouts/에 관리합니다. - /// `run` 대신 `upload-symbols`를 직접 호출해 dSYM 경로와 GoogleService-Info.plist를 명시합니다. + /// 로컬 Archive 빌드 시 생성된 dSYM 전부를 Firebase Console로 업로드해 크래시 스택을 + /// 심볼화합니다. 앱뿐 아니라 `$DWARF_DSYM_FOLDER_PATH` 안의 모든 `.dSYM`(Firebase, + /// GoogleSignIn, Kakao 등 framework 포함)을 순회합니다. + /// + /// CI 환경(`$CI == "true"`)에선 Fastlane `upload_symbols_to_crashlytics`가 동일 작업을 + /// 더 신뢰성 있게 수행하므로 중복 방지를 위해 즉시 종료합니다. static let crashlyticsUploadSymbols = TargetScript.post( - script: """ - UPLOAD_SYMBOLS=$(find "$SRCROOT" -name "upload-symbols" -path "*/Crashlytics/*" 2>/dev/null | head -1) - if [ -z "$UPLOAD_SYMBOLS" ]; then - UPLOAD_SYMBOLS=$(find "$SRCROOT/../.." -maxdepth 6 -name "upload-symbols" -path "*/Crashlytics/*" 2>/dev/null | head -1) + script: #""" + if [ "${CI:-}" = "true" ]; then + echo "note: CI environment detected — dSYM upload handled by Fastlane (skipping build-phase upload)" + exit 0 + fi + + if [ ! -d "$DWARF_DSYM_FOLDER_PATH" ]; then + echo "note: $DWARF_DSYM_FOLDER_PATH not found (DEBUG_INFORMATION_FORMAT may not be dwarf-with-dsym) — skipping" + exit 0 + fi + + UPLOAD_SYMBOLS="$SRCROOT/../../Tuist/.build/checkouts/firebase-ios-sdk/Crashlytics/upload-symbols" + if [ ! -x "$UPLOAD_SYMBOLS" ]; then + UPLOAD_SYMBOLS=$(find "$SRCROOT/../.." -maxdepth 6 -name "upload-symbols" -path "*/firebase-ios-sdk/Crashlytics/*" 2>/dev/null | head -1) + fi + if [ -z "$UPLOAD_SYMBOLS" ] || [ ! -x "$UPLOAD_SYMBOLS" ]; then + echo "warning: Firebase Crashlytics upload-symbols not found. Run 'tuist install'." + exit 0 fi - if [ -z "$UPLOAD_SYMBOLS" ]; then - echo "warning: Firebase Crashlytics upload-symbols not found. Run tuist install." + GSP="$SRCROOT/Resources/GoogleService-Info.plist" + if [ ! -f "$GSP" ]; then + echo "warning: GoogleService-Info.plist not found at $GSP — skipping" exit 0 fi - "$UPLOAD_SYMBOLS" \ - -gsp "$SRCROOT/Resources/GoogleService-Info.plist" \ - -p ios \ - "$DWARF_DSYM_FOLDER_PATH/$DWARF_DSYM_FILE_NAME" - """, + FAIL=0 + for DSYM in "$DWARF_DSYM_FOLDER_PATH"/*.dSYM; do + [ -e "$DSYM" ] || continue + echo "Uploading dSYM: $DSYM" + if ! "$UPLOAD_SYMBOLS" -gsp "$GSP" -p ios "$DSYM"; then + echo "warning: upload-symbols failed for $DSYM" + FAIL=1 + fi + done + exit $FAIL + """#, name: "Firebase Crashlytics", basedOnDependencyAnalysis: false ) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 76ed1422..25e11f5f 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -195,6 +195,13 @@ platform :ios do } ) + # build_app가 lane_context[SharedValues::DSYM_OUTPUT_PATH]에 모든 framework dSYM을 모은 + # zip 경로를 채워둠 — upload_symbols_to_crashlytics가 그걸 자동 사용. + upload_symbols_to_crashlytics( + gsp_path: "Projects/App/Resources/GoogleService-Info.plist", + binary_path: "Tuist/.build/checkouts/firebase-ios-sdk/Crashlytics/upload-symbols" + ) + upload_to_testflight( api_key: api_key, app_identifier: BUNDLE_ID, @@ -233,6 +240,11 @@ platform :ios do } ) + upload_symbols_to_crashlytics( + gsp_path: "Projects/App/Resources/GoogleService-Info.plist", + binary_path: "Tuist/.build/checkouts/firebase-ios-sdk/Crashlytics/upload-symbols" + ) + upload_to_testflight( api_key: api_key, app_identifier: BUNDLE_ID,