Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TNT-206] TraineeMyPage 작성, TToast 작성 #49

Merged
merged 3 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,26 @@ public struct TPopupAlertState: Equatable {
public var title: String
/// 팝업 메시지 (옵션)
public var message: String?
/// 팝업의 경고 아이콘 표시 (옵션)
public var showAlertIcon: Bool
/// 팝업에 표시될 버튼 배열
public var buttons: [ButtonState]

/// TPopupAlertState 초기화 메서드
/// - Parameters:
/// - title: 팝업의 제목
/// - message: 팝업의 메시지 (선택 사항, 기본값: `nil`)
/// - showAlertIcon: 팝업의 경고 아이콘 표시 (기본값: `false`)
/// - buttons: 팝업에 표시할 버튼 배열 (기본값: 빈 배열)
public init(
title: String,
message: String? = nil,
showAlertIcon: Bool = false,
buttons: [ButtonState] = []
) {
self.title = title
self.message = message
self.showAlertIcon = showAlertIcon
self.buttons = buttons
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,20 @@ public struct TPopUpAlertView: View {
VStack(spacing: 20) {
// 텍스트 Section
VStack(spacing: 8) {
Text(alertState.title)
.typographyStyle(.heading3, with: .neutral900)
.multilineTextAlignment(.center)
.padding(.top, 20)
VStack(spacing: 0) {
if alertState.showAlertIcon {
Image(.icnWarning)
.resizable()
.frame(width: 80, height: 80)
} else {
Color.clear
.frame(height: 20)
}
Text(alertState.title)
.typographyStyle(.heading3, with: .neutral900)
.multilineTextAlignment(.center)
}

if let message = alertState.message {
Text(message)
.typographyStyle(.body2Medium, with: .neutral500)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public struct TPopUpModifier<InnerContent: View>: ViewModifier {
/// 팝업 그림자의 기본 반경
private let defaultShadowRadius: CGFloat = 10
/// 팝업 콘텐츠의 기본 크기
private let defaultContentSize: CGSize = .init(width: 297, height: 175)
private let defaultContentSize: CGSize = .init(width: 297, height: 151)

/// 팝업에 표시될 내부 콘텐츠 클로저
private let innerContent: () -> InnerContent
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
//
// TToastModifier.swift
// DesignSystem
//
// Created by 박민서 on 2/3/25.
// Copyright © 2025 yapp25thTeamTnT. All rights reserved.
//

import SwiftUI

/// 토스트 메시지를 화면에 표시하는 ViewModifier
/// - `isPresented` 바인딩을 통해 토스트 메시지의 표시 여부를 제어
/// - 애니메이션을 적용하여 자연스럽게 나타났다가 사라지는 효과
public struct TToastViewModifier<InnerContent: View>: ViewModifier {
/// 토스트에 표시될 내부 콘텐츠 클로저
private let innerContent: () -> InnerContent
/// 토스트 표시 여부
@Binding private var isPresented: Bool
/// 토스트가 화면에 보이는 상태인지 여부 (애니메이션용)
@State private var isVisible: Bool = false

/// TToastViewModifier 초기화 메서드
/// - Parameters:
/// - isPresented: 토스트 표시 여부를 제어하는 Binding
/// - newContent: 토스트에 표시될 내부 콘텐츠 클로저
public init(
isPresented: Binding<Bool>,
newContent: @escaping () -> InnerContent
) {
self._isPresented = isPresented
self.innerContent = newContent
}

public func body(content: Content) -> some View {
ZStack {
// 기존 뷰
content
.onTapGesture {
isPresented = false
}

if isPresented {
// 토스트 뷰
self.innerContent()
.padding(.horizontal, 20)
.padding(.bottom, 24)
.opacity(isVisible ? 1 : 0)
.transition(.move(edge: .bottom).combined(with: .opacity))
.onAppear {
showToast()
}
}
}
.animation(.easeInOut, value: isPresented)
}

/// 토스트 메시지를 표시하고 자동으로 사라지도록 처리하는 함수
private func showToast() {
// 페이드인
withAnimation(.easeInOut(duration: 0.3)) {
isVisible = true
}
// 2초간 유지 - 자동으로 사라짐
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
// 페이드 아웃
withAnimation(.easeInOut(duration: 0.3)) {
isVisible = false
}
// present 해제
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
isPresented = false
}
}
}
}

public extension View {
/// 토스트 메시지를 화면에 표시하는 ViewModifier
///
/// - `isPresented`: 토스트의 표시 여부를 제어하는 Binding.
/// - `message`: 토스트에 표시할 메시지.
/// - `leftView`: 토스트 좌측에 추가할 아이콘이나 뷰.
func tToast<LeftView: View>(
isPresented: Binding<Bool>,
message: String,
leftView: @escaping () -> LeftView
) -> some View {
self.modifier(
TToastViewModifier(
isPresented: isPresented,
newContent: {
TToastView(message: message, leftView: leftView)
}
)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// TToastView.swift
// DesignSystem
//
// Created by 박민서 on 2/3/25.
// Copyright © 2025 yapp25thTeamTnT. All rights reserved.
//

import SwiftUI

/// 앱 전반적으로 사용되는 토스트 메시지 뷰
/// - 짧은 시간 동안 하단에 나타났다가 사라지는 UI 컴포넌트.
public struct TToastView<LeftView: View>: View {
/// 토스트 메세지
private let message: String
/// 토스트 좌측 뷰
private let leftView: () -> LeftView

/// TToastView 초기화 메서드
/// - Parameters:
/// - message: 표시할 메시지
/// - leftView: 좌측 아이콘 또는 커스텀 뷰를 반환하는 클로저
public init(message: String, leftView: @escaping () -> LeftView) {
self.message = message
self.leftView = leftView
}

public var body: some View {
VStack {
Spacer()

HStack(spacing: 8) {
leftView()

Text(message)
.typographyStyle(.label1Medium, with: .neutral50)

Spacer()
}
.padding(.vertical, 16)
.padding(.horizontal, 20)
.frame(maxWidth: .infinity)
.background(Color.neutral900.opacity(0.8))
.clipShape(.rect(cornerRadius: 16))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
//
// TraineeMyPageFeature.swift
// Presentation
//
// Created by 박민서 on 2/3/25.
// Copyright © 2025 yapp25thTeamTnT. All rights reserved.
//

import Foundation
import ComposableArchitecture

import Domain
import DesignSystem

@Reducer
public struct TraineeMyPageFeature {

public typealias FocusField = TraineeBasicInfoInputView.Field

@ObservableState
public struct State: Equatable {
// MARK: Data related state
/// 사용자 이름
var userName: String
/// 사용자 이미지 URL
var userImageUrl: String?
/// 앱 푸시 알림 허용 여부
var appPushNotificationAllowed: Bool
/// 버전 정보
var versionInfo: String
/// 트레이너 연결 여부
var isTrainerConnected: Bool

// MARK: UI related state

public init(
userName: String,
userImageUrl: String? = nil,
appPushNotificationAllowed: Bool,
versionInfo: String,
isTrainerConnected: Bool
) {
self.userName = userName
self.userImageUrl = userImageUrl
self.appPushNotificationAllowed = appPushNotificationAllowed
self.versionInfo = versionInfo
self.isTrainerConnected = isTrainerConnected
}

}

@Dependency(\.userUseCase) private var userUseCase: UserUseCase

public enum Action: Sendable, ViewAction {
/// 뷰에서 발생한 액션을 처리합니다.
case view(View)
/// 네비게이션 여부 설정
case setNavigating

@CasePathable
public enum View: Sendable, BindableAction {
/// 바인딩할 액션을 처리
case binding(BindingAction<State>)
/// 개인정보 수정 버튼 탭
case tapEditProfileButton
/// 트레이너와 연결하기 버튼 탭
case tapConnectTrainerButton
/// 서비스 이용약관 버튼 탭
case tapTOSButton
/// 개인정보 처리방침 버튼 탭
case tapPrivacyPolicyButton
/// 오픈소스 라이선스 버튼 탭
case tapOpenSourceLicenseButton
/// 트레이너와 연결끊기 버튼 탭
case tapDisconnectTrainerButton
/// 로그아웃 버튼 탭
case tapLogoutButton
/// 계정 탈퇴 버튼 탭
case tapWithdrawButton
}
}

public init() {}

public var body: some ReducerOf<Self> {
BindingReducer(action: \.view)

Reduce { state, action in
switch action {
case .view(let action):
switch action {
case .binding(\.appPushNotificationAllowed):
print("푸쉬알림 변경: \(state.appPushNotificationAllowed)")
return .none
case .binding:
return .none
case .tapEditProfileButton:
print("tapEditProfileButton")
return .none

case .tapConnectTrainerButton:
print("tapConnectTrainerButton")
return .none

case .tapTOSButton:
print("tapTOSButton")
return .none

case .tapPrivacyPolicyButton:
print("tapPrivacyPolicyButton")
return .none

case .tapOpenSourceLicenseButton:
print("tapOpenSourceLicenseButton")
return .none

case .tapDisconnectTrainerButton:
print("tapDisconnectTrainerButton")
return .none

case .tapLogoutButton:
print("tapLogoutButton")
return .none

case .tapWithdrawButton:
print("tapWithdrawButton")
return .none
}

case .setNavigating:
return .none
}
}
}
}
Loading