Skip to content

Commit 227947f

Browse files
authored
[#580] ProfileView에 TCA를 적용한다 (#614)
* feat: tca 1차 구현 * feat: ProfileHeatmapBuilder로 수정 * refactor: send의 animation api 적용 * refactor: 개행 제거 * refactor: Button 사용 예시 통일 * refactor: 토글의 isOn에 BindingAction 처리 * fix: 텍스트필드가 포커싱 될 때 프로필 사진이 깜빡거리는 현상 해결 * refactor: refreshable이 끝나기 전 ProgressView가 사라지는 현상 해결
1 parent a9db3b8 commit 227947f

7 files changed

Lines changed: 1181 additions & 242 deletions

File tree

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
//
2+
// ProfileFeature+Dependencies.swift
3+
// DevLogPresentation
4+
//
5+
// Created by opfic on 6/15/26.
6+
//
7+
8+
import ComposableArchitecture
9+
import DevLogDomain
10+
11+
extension DependencyValues {
12+
var profileFetchUserDataUseCase: FetchUserDataUseCase {
13+
get { self[ProfileFetchUserDataKey.self] }
14+
set { self[ProfileFetchUserDataKey.self] = newValue }
15+
}
16+
17+
var profileFetchImageDataUseCase: FetchProfileImageDataUseCase {
18+
get { self[ProfileFetchImageDataKey.self] }
19+
set { self[ProfileFetchImageDataKey.self] = newValue }
20+
}
21+
22+
var profileFetchTodosUseCase: FetchTodosUseCase {
23+
get { self[ProfileFetchTodosKey.self] }
24+
set { self[ProfileFetchTodosKey.self] = newValue }
25+
}
26+
27+
var profileUpsertStatusMessageUseCase: UpsertStatusMessageUseCase {
28+
get { self[ProfileUpsertStatusMessageKey.self] }
29+
set { self[ProfileUpsertStatusMessageKey.self] = newValue }
30+
}
31+
32+
var profileFetchHeatmapActivityTypesUseCase: FetchHeatmapActivityTypesUseCase {
33+
get { self[ProfileFetchHeatmapTypesKey.self] }
34+
set { self[ProfileFetchHeatmapTypesKey.self] = newValue }
35+
}
36+
37+
var profileUpdateHeatmapActivityTypesUseCase: UpdateHeatmapActivityTypesUseCase {
38+
get { self[ProfileUpdateHeatmapTypesKey.self] }
39+
set { self[ProfileUpdateHeatmapTypesKey.self] = newValue }
40+
}
41+
}
42+
43+
private enum ProfileFetchUserDataKey: DependencyKey {
44+
static var liveValue: FetchUserDataUseCase {
45+
preconditionFailure("FetchUserDataUseCase must be provided.")
46+
}
47+
48+
static var testValue: FetchUserDataUseCase {
49+
liveValue
50+
}
51+
}
52+
53+
private enum ProfileFetchImageDataKey: DependencyKey {
54+
static var liveValue: FetchProfileImageDataUseCase {
55+
preconditionFailure("FetchProfileImageDataUseCase must be provided.")
56+
}
57+
58+
static var testValue: FetchProfileImageDataUseCase {
59+
liveValue
60+
}
61+
}
62+
63+
private enum ProfileFetchTodosKey: DependencyKey {
64+
static var liveValue: FetchTodosUseCase {
65+
preconditionFailure("FetchTodosUseCase must be provided.")
66+
}
67+
68+
static var testValue: FetchTodosUseCase {
69+
liveValue
70+
}
71+
}
72+
73+
private enum ProfileUpsertStatusMessageKey: DependencyKey {
74+
static var liveValue: UpsertStatusMessageUseCase {
75+
preconditionFailure("UpsertStatusMessageUseCase must be provided.")
76+
}
77+
78+
static var testValue: UpsertStatusMessageUseCase {
79+
liveValue
80+
}
81+
}
82+
83+
private enum ProfileFetchHeatmapTypesKey: DependencyKey {
84+
static var liveValue: FetchHeatmapActivityTypesUseCase {
85+
preconditionFailure("FetchHeatmapActivityTypesUseCase must be provided.")
86+
}
87+
88+
static var testValue: FetchHeatmapActivityTypesUseCase {
89+
liveValue
90+
}
91+
}
92+
93+
private enum ProfileUpdateHeatmapTypesKey: DependencyKey {
94+
static var liveValue: UpdateHeatmapActivityTypesUseCase {
95+
preconditionFailure("UpdateHeatmapActivityTypesUseCase must be provided.")
96+
}
97+
98+
static var testValue: UpdateHeatmapActivityTypesUseCase {
99+
liveValue
100+
}
101+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
//
2+
// ProfileFeature+State.swift
3+
// DevLogPresentation
4+
//
5+
// Created by opfic on 6/15/26.
6+
//
7+
8+
import DevLogCore
9+
import Foundation
10+
11+
extension ProfileFeature.State {
12+
var isLoading: Bool {
13+
loading.isLoading
14+
}
15+
16+
var quarterTitle: String {
17+
guard let start = selectedQuarterStart else { return "" }
18+
let year = Calendar.current.component(.year, from: start)
19+
let month = Calendar.current.component(.month, from: start)
20+
let quarter = ((month - 1) / 3) + 1
21+
return String.localizedStringWithFormat(
22+
String(localized: "profile_year_quarter_format"),
23+
String(year),
24+
String(quarter)
25+
)
26+
}
27+
28+
var selectedDayActivities: [HeatmapActivityItem] {
29+
guard let selectedDay else { return [] }
30+
let dayStart = Calendar.current.startOfDay(for: selectedDay.date)
31+
let activities = dayActivitiesByDate[dayStart] ?? []
32+
33+
return activities.filter { activity in
34+
!Set(activity.activityKinds).isDisjoint(with: selectedActivityKinds)
35+
}
36+
}
37+
38+
var isCreatedActivitySelected: Bool {
39+
get { selectedActivityKinds.contains(.created) }
40+
set { setActivityKind(.created, isSelected: newValue) }
41+
}
42+
43+
var isCompletedActivitySelected: Bool {
44+
get { selectedActivityKinds.contains(.completed) }
45+
set { setActivityKind(.completed, isSelected: newValue) }
46+
}
47+
48+
var isDeletedActivitySelected: Bool {
49+
get { selectedActivityKinds.contains(.deleted) }
50+
set { setActivityKind(.deleted, isSelected: newValue) }
51+
}
52+
53+
var isCreatedActivityToggleDisabled: Bool {
54+
selectedActivityKinds == [.created]
55+
}
56+
57+
var isCompletedActivityToggleDisabled: Bool {
58+
selectedActivityKinds == [.completed]
59+
}
60+
61+
var isDeletedActivityToggleDisabled: Bool {
62+
selectedActivityKinds == [.deleted]
63+
}
64+
65+
var canMoveToPreviousQuarter: Bool {
66+
ProfileHeatmapBuilder.canMoveToQuarter(offsetMonths: -3, state: self)
67+
}
68+
69+
var canMoveToNextQuarter: Bool {
70+
ProfileHeatmapBuilder.canMoveToQuarter(offsetMonths: 3, state: self)
71+
}
72+
73+
var isViewingCurrentQuarter: Bool {
74+
guard let selectedQuarterStart,
75+
let currentQuarterStart = ProfileHeatmapBuilder.quarterStart(for: Date()) else {
76+
return false
77+
}
78+
return selectedQuarterStart == currentQuarterStart
79+
}
80+
81+
var availableQuarterYears: [Int] {
82+
guard let earliestQuarterStart,
83+
let currentQuarterStart = ProfileHeatmapBuilder.quarterStart(for: Date()) else {
84+
return [selectedQuarterPickerYear]
85+
}
86+
let earliestYear = Calendar.current.component(.year, from: earliestQuarterStart)
87+
let currentYear = Calendar.current.component(.year, from: currentQuarterStart)
88+
return Array(stride(from: currentYear, through: earliestYear, by: -1))
89+
}
90+
91+
func quarterStartForPicker(quarter: Int) -> Date? {
92+
ProfileHeatmapBuilder.quarterStart(year: selectedQuarterPickerYear, quarter: quarter)
93+
}
94+
95+
func isQuarterSelectableForPicker(_ quarter: Int) -> Bool {
96+
guard let quarterStart = quarterStartForPicker(quarter: quarter) else { return false }
97+
return ProfileHeatmapBuilder.canSelectQuarter(quarterStart, state: self)
98+
}
99+
100+
func isQuarterSelectedForPicker(_ quarter: Int) -> Bool {
101+
quarterStartForPicker(quarter: quarter) == selectedQuarterStart
102+
}
103+
104+
private mutating func setActivityKind(_ activityKind: ActivityKind, isSelected: Bool) {
105+
if isSelected {
106+
selectedActivityKinds.insert(activityKind)
107+
} else if 1 < selectedActivityKinds.count {
108+
selectedActivityKinds.remove(activityKind)
109+
}
110+
}
111+
}

0 commit comments

Comments
 (0)