Skip to content

Commit 186c6b0

Browse files
committed
[Feat] TraineeMyPage 화면 작성
1 parent 887ed50 commit 186c6b0

File tree

2 files changed

+327
-0
lines changed

2 files changed

+327
-0
lines changed
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
//
2+
// TraineeMyPageFeature.swift
3+
// Presentation
4+
//
5+
// Created by 박민서 on 2/3/25.
6+
// Copyright © 2025 yapp25thTeamTnT. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import ComposableArchitecture
11+
12+
import Domain
13+
import DesignSystem
14+
15+
@Reducer
16+
public struct TraineeMyPageFeature {
17+
18+
public typealias FocusField = TraineeBasicInfoInputView.Field
19+
20+
@ObservableState
21+
public struct State: Equatable {
22+
// MARK: Data related state
23+
/// 사용자 이름
24+
var userName: String
25+
/// 사용자 이미지 URL
26+
var userImageUrl: String?
27+
/// 앱 푸시 알림 허용 여부
28+
var appPushNotificationAllowed: Bool
29+
/// 버전 정보
30+
var versionInfo: String
31+
/// 트레이너 연결 여부
32+
var isTrainerConnected: Bool
33+
34+
// MARK: UI related state
35+
36+
public init(
37+
userName: String,
38+
userImageUrl: String? = nil,
39+
appPushNotificationAllowed: Bool,
40+
versionInfo: String,
41+
isTrainerConnected: Bool
42+
) {
43+
self.userName = userName
44+
self.userImageUrl = userImageUrl
45+
self.appPushNotificationAllowed = appPushNotificationAllowed
46+
self.versionInfo = versionInfo
47+
self.isTrainerConnected = isTrainerConnected
48+
}
49+
50+
}
51+
52+
@Dependency(\.userUseCase) private var userUseCase: UserUseCase
53+
54+
public enum Action: Sendable, ViewAction {
55+
/// 뷰에서 발생한 액션을 처리합니다.
56+
case view(View)
57+
/// 네비게이션 여부 설정
58+
case setNavigating
59+
60+
@CasePathable
61+
public enum View: Sendable, BindableAction {
62+
/// 바인딩할 액션을 처리
63+
case binding(BindingAction<State>)
64+
/// 개인정보 수정 버튼 탭
65+
case tapEditProfileButton
66+
/// 트레이너와 연결하기 버튼 탭
67+
case tapConnectTrainerButton
68+
/// 서비스 이용약관 버튼 탭
69+
case tapTOSButton
70+
/// 개인정보 처리방침 버튼 탭
71+
case tapPrivacyPolicyButton
72+
/// 오픈소스 라이선스 버튼 탭
73+
case tapOpenSourceLicenseButton
74+
/// 트레이너와 연결끊기 버튼 탭
75+
case tapDisconnectTrainerButton
76+
/// 로그아웃 버튼 탭
77+
case tapLogoutButton
78+
/// 계정 탈퇴 버튼 탭
79+
case tapWithdrawButton
80+
}
81+
}
82+
83+
public init() {}
84+
85+
public var body: some ReducerOf<Self> {
86+
BindingReducer(action: \.view)
87+
88+
Reduce { state, action in
89+
switch action {
90+
case .view(let action):
91+
switch action {
92+
case .binding(\.appPushNotificationAllowed):
93+
print("푸쉬알림 변경: \(state.appPushNotificationAllowed)")
94+
return .none
95+
case .binding:
96+
return .none
97+
case .tapEditProfileButton:
98+
print("tapEditProfileButton")
99+
return .none
100+
101+
case .tapConnectTrainerButton:
102+
print("tapConnectTrainerButton")
103+
return .none
104+
105+
case .tapTOSButton:
106+
print("tapTOSButton")
107+
return .none
108+
109+
case .tapPrivacyPolicyButton:
110+
print("tapPrivacyPolicyButton")
111+
return .none
112+
113+
case .tapOpenSourceLicenseButton:
114+
print("tapOpenSourceLicenseButton")
115+
return .none
116+
117+
case .tapDisconnectTrainerButton:
118+
print("tapDisconnectTrainerButton")
119+
return .none
120+
121+
case .tapLogoutButton:
122+
print("tapLogoutButton")
123+
return .none
124+
125+
case .tapWithdrawButton:
126+
print("tapWithdrawButton")
127+
return .none
128+
}
129+
130+
case .setNavigating:
131+
return .none
132+
}
133+
}
134+
}
135+
}
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
//
2+
// TraineeMyPageView.swift
3+
// Presentation
4+
//
5+
// Created by 박민서 on 2/3/25.
6+
// Copyright © 2025 yapp25thTeamTnT. All rights reserved.
7+
//
8+
9+
import SwiftUI
10+
import ComposableArchitecture
11+
12+
import Domain
13+
import DesignSystem
14+
15+
@ViewAction(for: TraineeMyPageFeature.self)
16+
public struct TraineeMyPageView: View {
17+
18+
@Bindable public var store: StoreOf<TraineeMyPageFeature>
19+
20+
public init(store: StoreOf<TraineeMyPageFeature>) {
21+
self.store = store
22+
}
23+
24+
public var body: some View {
25+
ScrollView {
26+
VStack(spacing: 0) {
27+
ProfileSection()
28+
29+
VStack(spacing: 12) {
30+
TopItemSection()
31+
InfoItemSection()
32+
BottomItemSection()
33+
}
34+
.padding(20)
35+
}
36+
}
37+
.background(Color.neutral50)
38+
.navigationBarBackButtonHidden()
39+
}
40+
41+
// MARK: - Sections
42+
@ViewBuilder
43+
private func ProfileSection() -> some View {
44+
VStack(spacing: 0) {
45+
ProfileImageView(imageURL: store.userImageUrl, name: store.userName)
46+
.padding(.vertical, 12)
47+
48+
Text(store.userName)
49+
.typographyStyle(.heading2, with: .neutral950)
50+
.padding(.bottom, store.isTrainerConnected ? 8 : 16)
51+
52+
if store.isTrainerConnected {
53+
TButton(
54+
title: "개인정보 수정",
55+
config: .small,
56+
state: .default(.gray(isEnabled: true)),
57+
action: { send(.tapEditProfileButton) }
58+
)
59+
.frame(width: 90, height: 34)
60+
}
61+
}
62+
}
63+
64+
@ViewBuilder
65+
private func TopItemSection() -> some View {
66+
VStack(spacing: 12) {
67+
if !store.isTrainerConnected {
68+
ProfileItemView(title: "트레이너 연결하기", tapAction: { send(.tapConnectTrainerButton) })
69+
.padding(.vertical, 4)
70+
.background(Color.common0)
71+
.clipShape(.rect(cornerRadius: 12))
72+
}
73+
74+
ProfileItemView(title: "앱 푸시 알림", rightView: {
75+
Toggle("appPushNotification", isOn: $store.appPushNotificationAllowed)
76+
.applyTToggleStyle()
77+
})
78+
.padding(.vertical, 4)
79+
.background(Color.common0)
80+
.clipShape(.rect(cornerRadius: 12))
81+
}
82+
}
83+
84+
@ViewBuilder
85+
private func InfoItemSection() -> some View {
86+
VStack(spacing: 12) {
87+
ProfileItemView(title: "서비스 이용약관", tapAction: { send(.tapTOSButton) })
88+
ProfileItemView(title: "개인정보 처리방침", tapAction: { send(.tapPrivacyPolicyButton) })
89+
ProfileItemView(title: "버전 정보", rightView: {
90+
Text(store.versionInfo)
91+
.typographyStyle(.body2Medium, with: .neutral400)
92+
})
93+
ProfileItemView(title: "오픈소스 라이선스", tapAction: { send(.tapOpenSourceLicenseButton) })
94+
}
95+
.padding(.vertical, 12)
96+
.background(Color.common0)
97+
.clipShape(.rect(cornerRadius: 12))
98+
}
99+
100+
@ViewBuilder
101+
private func BottomItemSection() -> some View {
102+
VStack(spacing: 12) {
103+
if store.isTrainerConnected {
104+
ProfileItemView(title: "트레이너와 연결끊기", tapAction: { send(.tapDisconnectTrainerButton) })
105+
.padding(.vertical, 4)
106+
.background(Color.common0)
107+
.clipShape(.rect(cornerRadius: 12))
108+
}
109+
110+
VStack(spacing: 12) {
111+
ProfileItemView(title: "로그아웃", tapAction: { send(.tapLogoutButton) })
112+
ProfileItemView(title: "계정 탈퇴", tapAction: { send(.tapWithdrawButton) })
113+
}
114+
.padding(.vertical, 12)
115+
.background(Color.common0)
116+
.clipShape(.rect(cornerRadius: 12))
117+
}
118+
}
119+
}
120+
121+
private extension TraineeMyPageView {
122+
struct ProfileImageView: View {
123+
let imageURL: String?
124+
let name: String
125+
126+
var body: some View {
127+
if let urlString = imageURL, let url = URL(string: urlString) {
128+
AsyncImage(url: url) { phase in
129+
switch phase {
130+
case .empty:
131+
ProgressView()
132+
.tint(.red500)
133+
.frame(width: 132, height: 132)
134+
135+
case .success(let image):
136+
image
137+
.resizable()
138+
.aspectRatio(contentMode: .fill)
139+
.frame(width: 132, height: 132)
140+
.clipShape(Circle())
141+
142+
case .failure:
143+
Image(.imgDefaultTraineeImage)
144+
.resizable()
145+
.aspectRatio(contentMode: .fill)
146+
.frame(width: 132, height: 132)
147+
.clipShape(Circle())
148+
149+
@unknown default:
150+
EmptyView()
151+
}
152+
}
153+
} else {
154+
Image(.imgDefaultTraineeImage)
155+
.resizable()
156+
.scaledToFill()
157+
.frame(width: 132, height: 132)
158+
.clipShape(Circle())
159+
}
160+
}
161+
}
162+
163+
struct ProfileItemView<RightView: View>: View {
164+
let title: String
165+
let rightView: () -> RightView
166+
let tapAction: (() -> Void)?
167+
168+
init(
169+
title: String,
170+
rightView: @escaping () -> RightView = { EmptyView() },
171+
tapAction: (() -> Void)? = nil
172+
) {
173+
self.title = title
174+
self.rightView = rightView
175+
self.tapAction = tapAction
176+
}
177+
178+
var body: some View {
179+
HStack {
180+
Text(title)
181+
.typographyStyle(.body2Medium, with: .neutral700)
182+
Spacer()
183+
rightView()
184+
}
185+
.onTapGesture {
186+
tapAction?()
187+
}
188+
.padding(.horizontal, 20)
189+
.padding(.vertical, 8)
190+
}
191+
}
192+
}

0 commit comments

Comments
 (0)