Skip to content

Commit 93decd5

Browse files
authored
Merge pull request #49 from YAPP-Github/TNT-206-traineeMyPage
[TNT-206] TraineeMyPage 작성, TToast 작성
2 parents c69d970 + 5bbdd1f commit 93decd5

File tree

7 files changed

+491
-5
lines changed

7 files changed

+491
-5
lines changed

TnT/Projects/DesignSystem/Sources/Components/PopUp/TPopUpAlertState.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,26 @@ public struct TPopupAlertState: Equatable {
1515
public var title: String
1616
/// 팝업 메시지 (옵션)
1717
public var message: String?
18+
/// 팝업의 경고 아이콘 표시 (옵션)
19+
public var showAlertIcon: Bool
1820
/// 팝업에 표시될 버튼 배열
1921
public var buttons: [ButtonState]
2022

2123
/// TPopupAlertState 초기화 메서드
2224
/// - Parameters:
2325
/// - title: 팝업의 제목
2426
/// - message: 팝업의 메시지 (선택 사항, 기본값: `nil`)
27+
/// - showAlertIcon: 팝업의 경고 아이콘 표시 (기본값: `false`)
2528
/// - buttons: 팝업에 표시할 버튼 배열 (기본값: 빈 배열)
2629
public init(
2730
title: String,
2831
message: String? = nil,
32+
showAlertIcon: Bool = false,
2933
buttons: [ButtonState] = []
3034
) {
3135
self.title = title
3236
self.message = message
37+
self.showAlertIcon = showAlertIcon
3338
self.buttons = buttons
3439
}
3540
}

TnT/Projects/DesignSystem/Sources/Components/PopUp/TPopUpAlertView.swift

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,20 @@ public struct TPopUpAlertView: View {
2323
VStack(spacing: 20) {
2424
// 텍스트 Section
2525
VStack(spacing: 8) {
26-
Text(alertState.title)
27-
.typographyStyle(.heading3, with: .neutral900)
28-
.multilineTextAlignment(.center)
29-
.padding(.top, 20)
26+
VStack(spacing: 0) {
27+
if alertState.showAlertIcon {
28+
Image(.icnWarning)
29+
.resizable()
30+
.frame(width: 80, height: 80)
31+
} else {
32+
Color.clear
33+
.frame(height: 20)
34+
}
35+
Text(alertState.title)
36+
.typographyStyle(.heading3, with: .neutral900)
37+
.multilineTextAlignment(.center)
38+
}
39+
3040
if let message = alertState.message {
3141
Text(message)
3242
.typographyStyle(.body2Medium, with: .neutral500)

TnT/Projects/DesignSystem/Sources/Components/PopUp/TPopUpModifier.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public struct TPopUpModifier<InnerContent: View>: ViewModifier {
2323
/// 팝업 그림자의 기본 반경
2424
private let defaultShadowRadius: CGFloat = 10
2525
/// 팝업 콘텐츠의 기본 크기
26-
private let defaultContentSize: CGSize = .init(width: 297, height: 175)
26+
private let defaultContentSize: CGSize = .init(width: 297, height: 151)
2727

2828
/// 팝업에 표시될 내부 콘텐츠 클로저
2929
private let innerContent: () -> InnerContent
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
//
2+
// TToastModifier.swift
3+
// DesignSystem
4+
//
5+
// Created by 박민서 on 2/3/25.
6+
// Copyright © 2025 yapp25thTeamTnT. All rights reserved.
7+
//
8+
9+
import SwiftUI
10+
11+
/// 토스트 메시지를 화면에 표시하는 ViewModifier
12+
/// - `isPresented` 바인딩을 통해 토스트 메시지의 표시 여부를 제어
13+
/// - 애니메이션을 적용하여 자연스럽게 나타났다가 사라지는 효과
14+
public struct TToastViewModifier<InnerContent: View>: ViewModifier {
15+
/// 토스트에 표시될 내부 콘텐츠 클로저
16+
private let innerContent: () -> InnerContent
17+
/// 토스트 표시 여부
18+
@Binding private var isPresented: Bool
19+
/// 토스트가 화면에 보이는 상태인지 여부 (애니메이션용)
20+
@State private var isVisible: Bool = false
21+
22+
/// TToastViewModifier 초기화 메서드
23+
/// - Parameters:
24+
/// - isPresented: 토스트 표시 여부를 제어하는 Binding
25+
/// - newContent: 토스트에 표시될 내부 콘텐츠 클로저
26+
public init(
27+
isPresented: Binding<Bool>,
28+
newContent: @escaping () -> InnerContent
29+
) {
30+
self._isPresented = isPresented
31+
self.innerContent = newContent
32+
}
33+
34+
public func body(content: Content) -> some View {
35+
ZStack {
36+
// 기존 뷰
37+
content
38+
.onTapGesture {
39+
isPresented = false
40+
}
41+
42+
if isPresented {
43+
// 토스트 뷰
44+
self.innerContent()
45+
.padding(.horizontal, 20)
46+
.padding(.bottom, 24)
47+
.opacity(isVisible ? 1 : 0)
48+
.transition(.move(edge: .bottom).combined(with: .opacity))
49+
.onAppear {
50+
showToast()
51+
}
52+
}
53+
}
54+
.animation(.easeInOut, value: isPresented)
55+
}
56+
57+
/// 토스트 메시지를 표시하고 자동으로 사라지도록 처리하는 함수
58+
private func showToast() {
59+
// 페이드인
60+
withAnimation(.easeInOut(duration: 0.3)) {
61+
isVisible = true
62+
}
63+
// 2초간 유지 - 자동으로 사라짐
64+
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
65+
// 페이드 아웃
66+
withAnimation(.easeInOut(duration: 0.3)) {
67+
isVisible = false
68+
}
69+
// present 해제
70+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
71+
isPresented = false
72+
}
73+
}
74+
}
75+
}
76+
77+
public extension View {
78+
/// 토스트 메시지를 화면에 표시하는 ViewModifier
79+
///
80+
/// - `isPresented`: 토스트의 표시 여부를 제어하는 Binding.
81+
/// - `message`: 토스트에 표시할 메시지.
82+
/// - `leftView`: 토스트 좌측에 추가할 아이콘이나 뷰.
83+
func tToast<LeftView: View>(
84+
isPresented: Binding<Bool>,
85+
message: String,
86+
leftView: @escaping () -> LeftView
87+
) -> some View {
88+
self.modifier(
89+
TToastViewModifier(
90+
isPresented: isPresented,
91+
newContent: {
92+
TToastView(message: message, leftView: leftView)
93+
}
94+
)
95+
)
96+
}
97+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
//
2+
// TToastView.swift
3+
// DesignSystem
4+
//
5+
// Created by 박민서 on 2/3/25.
6+
// Copyright © 2025 yapp25thTeamTnT. All rights reserved.
7+
//
8+
9+
import SwiftUI
10+
11+
/// 앱 전반적으로 사용되는 토스트 메시지 뷰
12+
/// - 짧은 시간 동안 하단에 나타났다가 사라지는 UI 컴포넌트.
13+
public struct TToastView<LeftView: View>: View {
14+
/// 토스트 메세지
15+
private let message: String
16+
/// 토스트 좌측 뷰
17+
private let leftView: () -> LeftView
18+
19+
/// TToastView 초기화 메서드
20+
/// - Parameters:
21+
/// - message: 표시할 메시지
22+
/// - leftView: 좌측 아이콘 또는 커스텀 뷰를 반환하는 클로저
23+
public init(message: String, leftView: @escaping () -> LeftView) {
24+
self.message = message
25+
self.leftView = leftView
26+
}
27+
28+
public var body: some View {
29+
VStack {
30+
Spacer()
31+
32+
HStack(spacing: 8) {
33+
leftView()
34+
35+
Text(message)
36+
.typographyStyle(.label1Medium, with: .neutral50)
37+
38+
Spacer()
39+
}
40+
.padding(.vertical, 16)
41+
.padding(.horizontal, 20)
42+
.frame(maxWidth: .infinity)
43+
.background(Color.neutral900.opacity(0.8))
44+
.clipShape(.rect(cornerRadius: 16))
45+
}
46+
}
47+
}
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+
}

0 commit comments

Comments
 (0)