diff --git a/TnT/Projects/Presentation/Sources/Coordinator/AppFlow/AppFlowCoordinatorFeature.swift b/TnT/Projects/Presentation/Sources/Coordinator/AppFlow/AppFlowCoordinatorFeature.swift new file mode 100644 index 00000000..ea16a854 --- /dev/null +++ b/TnT/Projects/Presentation/Sources/Coordinator/AppFlow/AppFlowCoordinatorFeature.swift @@ -0,0 +1,79 @@ +// +// AppFlowCoordinatorFeature.swift +// Presentation +// +// Created by 박민서 on 2/4/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import SwiftUI +import ComposableArchitecture + +import Domain + +@Reducer +public struct AppFlowCoordinatorFeature { + @ObservableState + public struct State: Equatable { + var userType: UserType? + + // MARK: SubFeature state + var trainerMainState: TrainerMainFlowFeature.State? + var traineeMainState: TraineeMainFlowFeature.State? + var onboardingState: OnboardingFlowFeature.State? + + public init( + userType: UserType? = nil, + onboardingState: OnboardingFlowFeature.State? = .init(), + trainerMainState: TrainerMainFlowFeature.State? = nil, + traineeMainState: TraineeMainFlowFeature.State? = nil + ) { + self.userType = userType + self.onboardingState = onboardingState + self.trainerMainState = trainerMainState + self.traineeMainState = traineeMainState + } + } + + public enum Action { + /// 하위 코디네이터에서 일어나는 액션을 처리합니다 + case subFeature(SubFeatureAction) + case onAppear + + @CasePathable + public enum SubFeatureAction: Sendable { + /// 온보딩 플로우 코디네이터에서 발생하는 액션 처리 + case onboardingFlow(OnboardingFlowFeature.Action) + /// 트레이너 메인탭 플로우 코디네이터에서 발생하는 액션 처리 + case trainerMainFlow(TrainerMainFlowFeature.Action) + /// 트레이니 메인탭 플로우 코디네이터에서 발생하는 액션 처리 + case traineeMainFlow(TraineeMainFlowFeature.Action) + } + } + + public init() {} + + public var body: some Reducer { + Reduce { state, action in + switch action { + case .subFeature(let internalAction): + switch internalAction { + case .onboardingFlow: + return .none + + case .trainerMainFlow: + return .none + + case .traineeMainFlow: + return .none + } + + case .onAppear: + return .none + } + } + .ifLet(\.onboardingState, action: \.subFeature.onboardingFlow) { OnboardingFlowFeature() } + .ifLet(\.trainerMainState, action: \.subFeature.trainerMainFlow) { TrainerMainFlowFeature() } + .ifLet(\.traineeMainState, action: \.subFeature.traineeMainFlow) { TraineeMainFlowFeature() } + } +} diff --git a/TnT/Projects/Presentation/Sources/Coordinator/AppFlow/AppFlowCoordinatorView.swift b/TnT/Projects/Presentation/Sources/Coordinator/AppFlow/AppFlowCoordinatorView.swift new file mode 100644 index 00000000..f89252ae --- /dev/null +++ b/TnT/Projects/Presentation/Sources/Coordinator/AppFlow/AppFlowCoordinatorView.swift @@ -0,0 +1,42 @@ +// +// AppFlowCoordinatorView.swift +// Presentation +// +// Created by 박민서 on 2/4/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import SwiftUI +import ComposableArchitecture + +public struct AppFlowCoordinatorView: View { + let store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + Group { + if let userType = store.userType { + switch userType { + case .trainee: + if let store = store.scope(state: \.traineeMainState, action: \.subFeature.traineeMainFlow) { + TraineeMainFlowView(store: store) + } + case .trainer: + if let store = store.scope(state: \.trainerMainState, action: \.subFeature.trainerMainFlow) { + TrainerMainFlowView(store: store) + } + } + } else { + if let store = store.scope(state: \.onboardingState, action: \.subFeature.onboardingFlow) { + OnboardingFlowView(store: store) + } + } + } + .onAppear { + store.send(.onAppear) + } + } +} diff --git a/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift b/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift new file mode 100644 index 00000000..3cd36667 --- /dev/null +++ b/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift @@ -0,0 +1,109 @@ +// +// OnboardingNavigationFeature.swift +// Presentation +// +// Created by 박서연 on 1/24/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import SwiftUI +import ComposableArchitecture + +import Domain + +@Reducer +public struct OnboardingFlowFeature { + @ObservableState + public struct State: Equatable { + public var path: StackState + + public init(path: StackState = .init([.snsLogin(.init())])) { + self.path = path + } + } + + public enum Action: Sendable { + /// 현재 표시되고 있는 path 화면 내부에서 일어나는 액션을 처리합니다. + case path(StackActionOf) + case onAppear + } + + public init() {} + + public var body: some ReducerOf { + Reduce { state, action in + switch action { + case let .path(action): + switch action { + + case .element(id: _, action: .snsLogin(.view(.tappedAppleLogin))): + state.path.append(.trainerSignUpComplete(.init())) + return .none + + /// 트레이너 프로필 생성 완료 -> 다음 버튼 tapped + case .element(id: _, action: .trainerSignUpComplete(.setNavigating)): + state.path.append(.trainerMakeInvitationCode(MakeInvitationCodeFeature.State())) + return .none + + /// 트레이너의 초대코드 화면 -> 건너뛰기 버튼 tapped + case .element(id: _, action: .trainerMakeInvitationCode(.setNavigation)): + // 추후에 홈과 연결 + return .none + + /// 약관 화면 -> 트레이너/트레이니 선택 화면 이동 + case .element(id: _, action: .userTypeSelection): + return .none + + default: + return .none + } + + case .onAppear: + return .none + } + } + .forEach(\.path, action: \.path) + } +} + +extension OnboardingFlowFeature { + @Reducer(state: .equatable, .sendable) + public enum Path { + // MARK: Common + /// SNS 로그인 뷰 + case snsLogin(LoginFeature) + /// 약관동의뷰 + case term(TermFeature) + /// 트레이너/트레이니 선택 뷰 + case userTypeSelection(UserTypeSelectionFeature) + /// 트레이너/트레이니의 이름 입력 뷰 + case createProfile(CreateProfileFeature) + + // MARK: Trainer + /// 트레이너 회원 가입 완료 뷰 + /// TODO: 트레이너/트레이니 회원 가입 완료 화면으로 통합 필요 + case trainerSignUpComplete(TrainerSignUpCompleteFeature) + /// 트레이너의 초대코드 발급 뷰 + case trainerMakeInvitationCode(MakeInvitationCodeFeature) + /// 트레이너의 트레이니 프로필 확인 뷰 + case trainerConnectedTraineeProfile(ConnectedTraineeProfileFeature) + + // MARK: Trainee + /// 트레이니 기본 정보 입력 + case traineeBasicInfoInput(TraineeBasicInfoInputFeature) + /// 트레이니 PT 목적 입력 + case traineeTrainingPurpose(TraineeTrainingPurposeFeature) + /// 트레이니 주의사항 입력 + case traineePrecautionInput(TraineePrecautionInputFeature) + /// 트레이니 프로필 생성 완료 + /// TODO: 트레이너/트레이니 회원 가입 완료 화면으로 통합 필요 + case traineeProfileCompletion(TraineeProfileCompletionFeature) + /// 트레이니 초대 코드입력 + case traineeInvitationCodeInput(TraineeInvitationCodeInputFeature) + /// 트레이니 수업 정보 입력 + case traineeTrainingInfoInput(TraineeTrainingInfoInputFeature) + /// 트레이니 연결 완료 + /// TODO: 트레이너/트레이니 연결 완료 화면으로 통합 필요 + case traineeConnectionComplete(TraineeConnectionCompleteFeature) + } +} diff --git a/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowView.swift b/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowView.swift new file mode 100644 index 00000000..aa98e0ad --- /dev/null +++ b/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowView.swift @@ -0,0 +1,62 @@ +// +// OnboardingNavigationView.swift +// Presentation +// +// Created by 박민서 on 2/4/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import SwiftUI +import ComposableArchitecture + +import DesignSystem + +public struct OnboardingFlowView: View { + @Bindable var store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + NavigationStack(path: $store.scope(state: \.path, action: \.path)) { + EmptyView() + } destination: { store in + switch store.case { + // MARK: Common + case .snsLogin(let store): + LoginView(store: store) + case .term(let store): + TermView(store: store) + case .userTypeSelection(let store): + UserTypeSelectionView(store: store) + case .createProfile(let store): + CreateProfileView(store: store) + + // MARK: Trainer + case .trainerSignUpComplete(let store): + TrainerSignUpCompleteView(store: store) + case .trainerMakeInvitationCode(let store): + MakeInvitationCodeView(store: store) + case .trainerConnectedTraineeProfile(let store): + ConnectedTraineeProfileView(store: store) + + // MARK: Trainee + case .traineeBasicInfoInput(let store): + TraineeBasicInfoInputView(store: store) + case .traineeTrainingPurpose(let store): + TraineeTrainingPurposeView(store: store) + case .traineePrecautionInput(let store): + TraineePrecautionInputView(store: store) + case .traineeProfileCompletion(let store): + TraineeProfileCompletionView(store: store) + case .traineeInvitationCodeInput(let store): + TraineeInvitationCodeInputView(store: store) + case .traineeTrainingInfoInput(let store): + TraineeTrainingInfoInputView(store: store) + case .traineeConnectionComplete(let store): + TraineeConnectionCompleteView(store: store) + } + } + } +} diff --git a/TnT/Projects/Presentation/Sources/Coordinator/TraineeMainFlow/TraineeMainFlowFeature.swift b/TnT/Projects/Presentation/Sources/Coordinator/TraineeMainFlow/TraineeMainFlowFeature.swift new file mode 100644 index 00000000..32af2c84 --- /dev/null +++ b/TnT/Projects/Presentation/Sources/Coordinator/TraineeMainFlow/TraineeMainFlowFeature.swift @@ -0,0 +1,118 @@ +// +// TraineeMainFlowFeature.swift +// Presentation +// +// Created by 박민서 on 2/5/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import SwiftUI +import ComposableArchitecture + +import Domain + +@Reducer +public struct TraineeMainFlowFeature { + @ObservableState + public struct State: Equatable { + public var path: StackState + + public init(path: StackState = .init([.mainTab(.home(.init()))])) { + self.path = path + } + } + + public enum Action: Sendable { + /// 현재 표시되고 있는 path 화면 내부에서 일어나는 액션을 처리합니다. + case path(StackActionOf) + case onAppear + } + + public init() {} + + public var body: some ReducerOf { + Reduce { state, action in + switch action { + case let .path(action): + switch action { + /// 트레이니 탭뷰의 네비 관련 액션 처리 + case .element(_, action: .mainTab(.setNavigating(let screen))): + switch screen { + /// 트레이니 홈 + case .traineeHome(let screen): + switch screen { + /// 홈 화면 알림 버튼 탭 -> 알림 화면 이동 + case .alarmPage: + state.path.append(.alarmCheck(.init(userType: .trainee))) + return .none + case .sessionRecordPage: + return .none + case .recordFeedbackPage: + return .none + case .addWorkoutRecordPage: + return .none + case .addMealRecordPage: + return .none + } + /// 트레이니 마이페이지 + case .traineeMyPage(let screen): + switch screen { + case .traineeInfoEdit: + return .none + + /// 마이페이지 초대코드 입력하기 버튼 탭-> 초대코드 입력 화면 이동 + case .traineeInvitationCodeInput: + state.path.append(.traineeInvitationCodeInput(.init())) + return .none + } + } + + /// 알림 목록 특정 알림 탭 -> 해당 알림 내용 화면 이동 + case .element(_, action: .alarmCheck(.setNavigating)): + // 특정 화면 append + return .none + + /// 마이페이지 초대코드 입력화면 다음 버튼 탭 - > PT 정보 입력 화면 이동 + case .element(_, action: .traineeInvitationCodeInput(.setNavigating)): + state.path.append(.traineeTrainingInfoInput(.init())) + return .none + + /// PT 정보 입력 화면 다음 버튼 탭 -> 연결 완료 화면 이동 + case .element(_, action: .traineeTrainingInfoInput(.setNavigating)): + state.path.append(.traineeConnectionComplete(.init(userType: .trainee, traineeName: "여기에", trainerName: "데이터 연결"))) + return .none + + default: + return .none + } + + case .onAppear: + return .none + } + } + .forEach(\.path, action: \.path) + + } +} + +extension TraineeMainFlowFeature { + @Reducer(state: .equatable, .sendable) + public enum Path { + // MARK: MainTab + /// 트레이니 메인탭 - 홈/마이페이지 + case mainTab(TraineeMainTabFeature) + + // MARK: Home + /// 알림 목록 + case alarmCheck(AlarmCheckFeature) + + // MARK: MyPage + /// 트레이니 초대 코드입력 + case traineeInvitationCodeInput(TraineeInvitationCodeInputFeature) + /// 트레이니 수업 정보 입력 + case traineeTrainingInfoInput(TraineeTrainingInfoInputFeature) + /// 트레이니-트레이너 연결 완료 + /// TODO: 트레이너/트레이니 연결 완료 화면으로 통합 필요 + case traineeConnectionComplete(TraineeConnectionCompleteFeature) + } +} diff --git a/TnT/Projects/Presentation/Sources/Coordinator/TraineeMainFlow/TraineeMainFlowView.swift b/TnT/Projects/Presentation/Sources/Coordinator/TraineeMainFlow/TraineeMainFlowView.swift new file mode 100644 index 00000000..f9fd6b9b --- /dev/null +++ b/TnT/Projects/Presentation/Sources/Coordinator/TraineeMainFlow/TraineeMainFlowView.swift @@ -0,0 +1,44 @@ +// +// TraineeMainFlowView.swift +// Presentation +// +// Created by 박민서 on 2/5/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import SwiftUI +import ComposableArchitecture + +import DesignSystem + +public struct TraineeMainFlowView: View { + @Bindable public var store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + NavigationStack(path: $store.scope(state: \.path, action: \.path)) { + EmptyView() + } destination: { store in + switch store.case { + // MARK: MainTab + case .mainTab(let store): + TraineeMainTabView(store: store) + + // MARK: Home + case .alarmCheck(let store): + AlarmCheckView(store: store) + + // MARK: MyPage + case .traineeInvitationCodeInput(let store): + TraineeInvitationCodeInputView(store: store) + case .traineeTrainingInfoInput(let store): + TraineeTrainingInfoInputView(store: store) + case .traineeConnectionComplete(let store): + TraineeConnectionCompleteView(store: store) + } + } + } +} diff --git a/TnT/Projects/Presentation/Sources/Coordinator/TrainerMainFlow/TrainerMainFlowFeature.swift b/TnT/Projects/Presentation/Sources/Coordinator/TrainerMainFlow/TrainerMainFlowFeature.swift new file mode 100644 index 00000000..f26fbe32 --- /dev/null +++ b/TnT/Projects/Presentation/Sources/Coordinator/TrainerMainFlow/TrainerMainFlowFeature.swift @@ -0,0 +1,118 @@ +// +// TrainerMainFlowFeature.swift +// Presentation +// +// Created by 박민서 on 2/5/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import SwiftUI +import ComposableArchitecture + +import Domain + +@Reducer +public struct TrainerMainFlowFeature { + @ObservableState + public struct State: Equatable { + public var path: StackState + + public init(path: StackState = .init([])) { + self.path = path + } + } + + public enum Action: Sendable { + /// 현재 표시되고 있는 path 화면 내부에서 일어나는 액션을 처리합니다. + case path(StackActionOf) + case onAppear + } + + public init() {} + + public var body: some ReducerOf { + Reduce { state, action in + switch action { + case let .path(action): + switch action { + /// 트레이니 탭뷰의 네비 관련 액션 처리 + case .element(_, action: .mainTab(.setNavigating(let screen))): + switch screen { + /// 트레이니 홈 + case .traineeHome(let screen): + switch screen { + /// 홈 화면 알림 버튼 탭 -> 알림 화면 이동 + case .alarmPage: + state.path.append(.alarmCheck(.init(userType: .trainee))) + return .none + case .sessionRecordPage: + return .none + case .recordFeedbackPage: + return .none + case .addWorkoutRecordPage: + return .none + case .addMealRecordPage: + return .none + } + /// 트레이니 마이페이지 + case .traineeMyPage(let screen): + switch screen { + case .traineeInfoEdit: + return .none + + /// 마이페이지 초대코드 입력하기 버튼 탭-> 초대코드 입력 화면 이동 + case .traineeInvitationCodeInput: + state.path.append(.traineeInvitationCodeInput(.init())) + return .none + } + } + + /// 알림 목록 특정 알림 탭 -> 해당 알림 내용 화면 이동 + case .element(_, action: .alarmCheck(.setNavigating)): + // 특정 화면 append + return .none + + /// 마이페이지 초대코드 입력화면 다음 버튼 탭 - > PT 정보 입력 화면 이동 + case .element(_, action: .traineeInvitationCodeInput(.setNavigating)): + state.path.append(.traineeTrainingInfoInput(.init())) + return .none + + /// PT 정보 입력 화면 다음 버튼 탭 -> 연결 완료 화면 이동 + case .element(_, action: .traineeTrainingInfoInput(.setNavigating)): + state.path.append(.traineeConnectionComplete(.init(userType: .trainee, traineeName: "여기에", trainerName: "데이터 연결"))) + return .none + + default: + return .none + } + + case .onAppear: + return .none + } + } + .forEach(\.path, action: \.path) + + } +} + +extension TrainerMainFlowFeature { + @Reducer(state: .equatable, .sendable) + public enum Path { + // MARK: MainTab + /// 트레이니 메인탭 - 홈/마이페이지 + case mainTab(TraineeMainTabFeature) + + // MARK: Home + /// 알림 목록 + case alarmCheck(AlarmCheckFeature) + + // MARK: MyPage + /// 트레이니 초대 코드입력 + case traineeInvitationCodeInput(TraineeInvitationCodeInputFeature) + /// 트레이니 수업 정보 입력 + case traineeTrainingInfoInput(TraineeTrainingInfoInputFeature) + /// 트레이니-트레이너 연결 완료 + /// TODO: 트레이너/트레이니 연결 완료 화면으로 통합 필요 + case traineeConnectionComplete(TraineeConnectionCompleteFeature) + } +} diff --git a/TnT/Projects/Presentation/Sources/Coordinator/TrainerMainFlow/TrainerMainFlowView.swift b/TnT/Projects/Presentation/Sources/Coordinator/TrainerMainFlow/TrainerMainFlowView.swift new file mode 100644 index 00000000..30f03043 --- /dev/null +++ b/TnT/Projects/Presentation/Sources/Coordinator/TrainerMainFlow/TrainerMainFlowView.swift @@ -0,0 +1,44 @@ +// +// TrainerMainFlowView.swift +// Presentation +// +// Created by 박민서 on 2/5/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import SwiftUI +import ComposableArchitecture + +import DesignSystem + +public struct TrainerMainFlowView: View { + @Bindable public var store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + NavigationStack(path: $store.scope(state: \.path, action: \.path)) { + EmptyView() + } destination: { store in + switch store.case { + // MARK: MainTab + case .mainTab(let store): + TraineeMainTabView(store: store) + + // MARK: Home + case .alarmCheck(let store): + AlarmCheckView(store: store) + + // MARK: MyPage + case .traineeInvitationCodeInput(let store): + TraineeInvitationCodeInputView(store: store) + case .traineeTrainingInfoInput(let store): + TraineeTrainingInfoInputView(store: store) + case .traineeConnectionComplete(let store): + TraineeConnectionCompleteView(store: store) + } + } + } +} diff --git a/TnT/Projects/Presentation/Sources/MainTab/Trainee/TraineeMainTabFeature.swift b/TnT/Projects/Presentation/Sources/MainTab/Trainee/TraineeMainTabFeature.swift index e8ad8dd3..d5a35d66 100644 --- a/TnT/Projects/Presentation/Sources/MainTab/Trainee/TraineeMainTabFeature.swift +++ b/TnT/Projects/Presentation/Sources/MainTab/Trainee/TraineeMainTabFeature.swift @@ -67,7 +67,7 @@ public struct TraineeMainTabFeature { state = .home(.init()) return .none case .mypage: - state = .myPage(.init()) + state = .myPage(.init()) return .none } }