From 0fa1da0a58c58865ba40d8456f1abe10e1c8deba Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Tue, 11 Feb 2025 02:21:28 +0900 Subject: [PATCH 1/7] =?UTF-8?q?[Feat]=20=ED=8A=B8=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EB=84=88=20=EC=B4=88=EB=8C=80=20=EC=BD=94=EB=93=9C=20=EB=B0=9C?= =?UTF-8?q?=EA=B8=89/=EC=9E=AC=EB=B0=9C=EA=B8=89=20=ED=99=94=EB=A9=B4=20AP?= =?UTF-8?q?I=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GetReissuanceInvitationCodeDTO.swift | 7 +- .../Code/MakeInvitationCodeFeature.swift | 76 ++++++++++++++++--- .../Login/Code/MakeInvitationCodeView.swift | 8 +- 3 files changed, 70 insertions(+), 21 deletions(-) diff --git a/TnT/Projects/Domain/Sources/DTO/Trainer/GetReissuanceInvitationCodeDTO.swift b/TnT/Projects/Domain/Sources/DTO/Trainer/GetReissuanceInvitationCodeDTO.swift index 56eb6f35..6cb9727a 100644 --- a/TnT/Projects/Domain/Sources/DTO/Trainer/GetReissuanceInvitationCodeDTO.swift +++ b/TnT/Projects/Domain/Sources/DTO/Trainer/GetReissuanceInvitationCodeDTO.swift @@ -9,14 +9,9 @@ import Foundation public struct GetReissuanceInvitationCodeDTO: Decodable { - public let trainerId: String public let invitationCode: String - public init( - trainerId: String, - invitationCode: String - ) { - self.trainerId = trainerId + public init(invitationCode: String) { self.invitationCode = invitationCode } } diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Trainer/Login/Code/MakeInvitationCodeFeature.swift b/TnT/Projects/Presentation/Sources/Onboarding/Trainer/Login/Code/MakeInvitationCodeFeature.swift index 73fe6f89..7fce3d12 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Trainer/Login/Code/MakeInvitationCodeFeature.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Trainer/Login/Code/MakeInvitationCodeFeature.swift @@ -7,50 +7,104 @@ // import SwiftUI -import DesignSystem import ComposableArchitecture +import DesignSystem +import DIContainer + @Reducer public struct MakeInvitationCodeFeature { @ObservableState public struct State: Equatable { - var view_invitationCode: String = "" + var invitationCode: String = "" public init() { } } - public enum Action: Equatable, ViewAction { - case setNavigation + @Dependency(\.trainerRepoUseCase) var trainerRepoUseCase + + public enum Action: ViewAction { + /// 화면 내 발생 액션 처리 case view(View) + /// api 콜 액션 처리 + case api(APIAction) + /// 초대 코드 설정 + case setInvitationCode(String) + /// 네비게이션 처리 + case setNavigation - public enum View { + @CasePathable + public enum View: Sendable, BindableAction { + /// 바인딩 액션 처리 + case binding(BindingAction) + /// 우측 상단 건너뛰기 버튼 탭 case tappedNextButton - case tappedIssuanceButton - case copyCode + /// 코드 재발급 버튼 탭 + case tappedReissuanceButton + /// 코드 카피 영역 탭 + case tapCodeToCopy + /// 화면 표시될 때 + case onAppear + } + + @CasePathable + public enum APIAction: Sendable { + /// 초대 코드 불러오기 API + case getInvitationCode + /// 초대 코드 재발급하기 API + case reissueInvitationCode } } public init() { } public var body: some ReducerOf { + BindingReducer(action: \.view) + Reduce { state, action in switch action { case .view(let view): switch view { - case .copyCode: - UIPasteboard.general.string = state.view_invitationCode + case .binding: return .none - case .tappedIssuanceButton: + case .tapCodeToCopy: + UIPasteboard.general.string = state.invitationCode + NotificationCenter.default.post(toast: .init(presentType: .text("⚠"), message: "코드가 복사되었어요!")) return .none + case .tappedReissuanceButton: + return .send(.api(.reissueInvitationCode)) + case .tappedNextButton: return .send(.setNavigation) + + case .onAppear: + return .send(.api(.getInvitationCode)) } - case .setNavigation: + + case .api(let action): + switch action { + case .getInvitationCode: + return .run { send in + let result = try await trainerRepoUseCase.getTheFirstInvitationCode() + await send(.setInvitationCode(result.invitationCode)) + } + + case .reissueInvitationCode: + return .run { send in + let result = try await trainerRepoUseCase.getReissuanceInvitationCode() + await send(.setInvitationCode(result.invitationCode)) + } + } + + case .setInvitationCode(let code): + state.invitationCode = code return .none + case .setNavigation: + return .none } } } diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Trainer/Login/Code/MakeInvitationCodeView.swift b/TnT/Projects/Presentation/Sources/Onboarding/Trainer/Login/Code/MakeInvitationCodeView.swift index f8647598..604a008a 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Trainer/Login/Code/MakeInvitationCodeView.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Trainer/Login/Code/MakeInvitationCodeView.swift @@ -25,6 +25,7 @@ public struct MakeInvitationCodeView: View { InvitationCode() } .navigationBarBackButtonHidden() + .onAppear { send(.onAppear) } } @ViewBuilder @@ -55,12 +56,12 @@ public struct MakeInvitationCodeView: View { HStack(spacing: 0) { ZStack(alignment: .bottom) { - Text("\(store.state.view_invitationCode)") + Text("\(store.invitationCode)") .typographyStyle(.body1Medium, with: .neutral600) .padding(8) .frame(maxWidth: .infinity, alignment: .leading) .onTapGesture { - send(.copyCode) + send(.tapCodeToCopy) } TDivider(height: 1, color: .neutral300) @@ -71,9 +72,8 @@ public struct MakeInvitationCodeView: View { config: .small, state: .default(.gray(isEnabled: true)) ) { - send(.tappedIssuanceButton) + send(.tappedReissuanceButton) } - .frame(width: 82) } } From 765785fbb18dd6f0168aeffc7fe15d1e33c5ef3d Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Tue, 11 Feb 2025 02:46:10 +0900 Subject: [PATCH 2/7] =?UTF-8?q?[Feat]=20TraineeInvitationCodeInput=20API?= =?UTF-8?q?=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DTO/Trainer/TrainerResponseDTO.swift | 2 + .../TraineeInvitationCodeInputFeature.swift | 44 ++++++++++++++++--- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/TnT/Projects/Domain/Sources/DTO/Trainer/TrainerResponseDTO.swift b/TnT/Projects/Domain/Sources/DTO/Trainer/TrainerResponseDTO.swift index 45525c0d..f914af95 100644 --- a/TnT/Projects/Domain/Sources/DTO/Trainer/TrainerResponseDTO.swift +++ b/TnT/Projects/Domain/Sources/DTO/Trainer/TrainerResponseDTO.swift @@ -12,4 +12,6 @@ import Foundation public struct GetVerifyInvitationCodeResDTO: Decodable { /// 초대 코드 인증 여부 public let isVerified: Bool + /// 트레이너 이름 + public let trainerName: String? } diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeInvitationCodeInput/TraineeInvitationCodeInputFeature.swift b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeInvitationCodeInput/TraineeInvitationCodeInputFeature.swift index 087ba647..5139b536 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeInvitationCodeInput/TraineeInvitationCodeInputFeature.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeInvitationCodeInput/TraineeInvitationCodeInputFeature.swift @@ -21,6 +21,8 @@ public struct TraineeInvitationCodeInputFeature { // MARK: Data related state /// 입력된 초대코드 var invitationCode: String + /// 트레이너 이름 + var trainerName: String? // MARK: UI related state /// 텍스트 필드 상태 (빈 값 / 입력됨 / 유효하지 않음) @@ -44,6 +46,7 @@ public struct TraineeInvitationCodeInputFeature { /// `TraineeInvitationCodeInputFeature.State`의 생성자 /// - Parameters: /// - invitationCode: 사용자가 입력한 초대 코드 (기본값: `""`) + /// - trainerName: 초대코드에 연결된 트레이너 이름 (기본값: `nil`) /// - view_invitationCodeStatus: 텍스트 필드 상태 (`.empty`, `.valid`, `.invalid` 등) /// - view_textFieldFooterText: 텍스트 필드 하단에 표시될 메시지 (기본값: `""`) /// - view_isFieldFocused: 현재 텍스트 필드가 포커스를 받고 있는지 여부 (기본값: `false`) @@ -54,6 +57,7 @@ public struct TraineeInvitationCodeInputFeature { /// - view_navigationType: 현재 화면의 네비게이션 타입 (`.newUser`: "건너뛰기" 버튼 있음, `.existingUser`: "뒤로가기" 버튼 있음) public init( invitationCode: String = "", + trainerName: String? = nil, view_invitationCodeStatus: TTextField.Status = .empty, view_textFieldFooterText: String = "", view_isFieldFocused: Bool = false, @@ -64,6 +68,7 @@ public struct TraineeInvitationCodeInputFeature { view_navigationType: NavigationType = .newUser ) { self.invitationCode = invitationCode + self.trainerName = trainerName self.view_popUp = view_popUp self.view_invitationCodeStatus = view_invitationCodeStatus self.view_textFieldFooterText = view_textFieldFooterText @@ -76,14 +81,19 @@ public struct TraineeInvitationCodeInputFeature { } @Dependency(\.traineeUseCase) private var traineeUseCase: TraineeUseCase + @Dependency(\.trainerRepoUseCase) private var trainerRepoUseCase public enum Action: Sendable, ViewAction { /// 뷰에서 발생한 액션을 처리합니다. case view(View) + /// api 콜 액션 처리 + case api(APIAction) /// 네비게이션 여부 설정 case setNavigating(RoutingScreen) /// 다음 버튼 활성화 상태 조작 case updateVerificationStatus(Bool) + /// 트레이너 이름 설정 + case setTrainerName(String) @CasePathable public enum View: Sendable, BindableAction { @@ -105,6 +115,12 @@ public struct TraineeInvitationCodeInputFeature { /// 팝업 우측 primary 버튼 탭 case tapPopUpPrimaryButton(popUp: PopUp?) } + + @CasePathable + public enum APIAction: Sendable { + /// 초대 코드 인증하기 API + case verifyInvitationCode(code: String) + } } public init() {} @@ -123,13 +139,12 @@ public struct TraineeInvitationCodeInputFeature { return .none case .tapVerifyButton: - return .run { [state] send in - let result: Bool = try await traineeUseCase.verifyTrainerInvitationCode(state.invitationCode) - await send(.updateVerificationStatus(result)) - } + return .send(.api(.verifyInvitationCode(code: state.invitationCode))) case .tapNextButton: - return .send(.setNavigating(.trainingInfoInput)) + guard let trainerName = state.trainerName else { return .none } + print("trainerName: \(trainerName)") + return .send(.setNavigating(.trainingInfoInput(trainerName: trainerName))) case .setFocus(let isFocused): state.view_isFieldFocused = isFocused @@ -148,12 +163,29 @@ public struct TraineeInvitationCodeInputFeature { return self.setPopUpStatus(&state, status: nil) } + case .api(let action): + switch action { + case .verifyInvitationCode(let code): + return .run { send in + if let result = try? await trainerRepoUseCase.getVerifyInvitationCode(code: code), let trainerName = result.trainerName { + await send(.updateVerificationStatus(result.isVerified)) + await send(.setTrainerName(trainerName)) + } else { + await send(.updateVerificationStatus(false)) + } + } + } + case .updateVerificationStatus(let isVerified): state.view_textFieldFooterText = isVerified ? "인증에 성공했습니다" : "인증에 실패했습니다" state.view_invitationCodeStatus = isVerified ? .valid : .invalid state.view_isNextButtonEnabled = isVerified return .none + case .setTrainerName(let name): + state.trainerName = name + return .none + case .setNavigating: return .none } @@ -197,7 +229,7 @@ public extension TraineeInvitationCodeInputFeature { /// 본 화면에서 라우팅(파생)되는 화면 enum RoutingScreen: Sendable { case traineeHome - case trainingInfoInput + case trainingInfoInput(trainerName: String) } /// 본 화면의 네비게이션 타입 From 201452fa6a7daf3071f5c068267933d03abb0ce2 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Tue, 11 Feb 2025 02:46:31 +0900 Subject: [PATCH 3/7] =?UTF-8?q?[Feat]=20=EC=B4=88=EB=8C=80=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20->=20=EC=88=98=EC=97=85=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=9E=85=EB=A0=A5=20=ED=99=94=EB=A9=B4=20=ED=9D=90=EB=A6=84=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Coordinator/OnboardingFlow/OnboardingFlowFeature.swift | 4 ++-- .../Coordinator/TraineeMainFlow/TraineeMainFlowFeature.swift | 4 ++-- .../TraineeTrainingInfoInputFeature.swift | 1 + .../TraineeTrainingInfoInputView.swift | 2 +- .../Trainer/Login/Code/MakeInvitationCodeView.swift | 1 + 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift b/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift index eef08c65..fec6bd28 100644 --- a/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift +++ b/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift @@ -111,8 +111,8 @@ public struct OnboardingFlowFeature { switch screen { case .traineeHome: return .send(.switchFlow(.traineeMainFlow)) - case .trainingInfoInput: - state.path.append(.traineeTrainingInfoInput(.init())) + case .trainingInfoInput(let name): + state.path.append(.traineeTrainingInfoInput(.init(trainerName: name))) return .none } diff --git a/TnT/Projects/Presentation/Sources/Coordinator/TraineeMainFlow/TraineeMainFlowFeature.swift b/TnT/Projects/Presentation/Sources/Coordinator/TraineeMainFlow/TraineeMainFlowFeature.swift index a0cb0d0f..31eaf6e7 100644 --- a/TnT/Projects/Presentation/Sources/Coordinator/TraineeMainFlow/TraineeMainFlowFeature.swift +++ b/TnT/Projects/Presentation/Sources/Coordinator/TraineeMainFlow/TraineeMainFlowFeature.swift @@ -86,8 +86,8 @@ public struct TraineeMainFlowFeature { switch screen { case .traineeHome: state.path.removeLast() - case .trainingInfoInput: - state.path.append(.traineeTrainingInfoInput(.init())) + case .trainingInfoInput(let trainerName): + state.path.append(.traineeTrainingInfoInput(.init(trainerName: trainerName))) } return .none diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeTrainingInfoInput/TraineeTrainingInfoInputFeature.swift b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeTrainingInfoInput/TraineeTrainingInfoInputFeature.swift index 8aa842a6..e683a326 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeTrainingInfoInput/TraineeTrainingInfoInputFeature.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeTrainingInfoInput/TraineeTrainingInfoInputFeature.swift @@ -43,6 +43,7 @@ public struct TraineeTrainingInfoInputFeature { /// `TraineeBasicInfoInputFeature.State`의 생성자 /// - Parameters: + /// - trainerName: 트레이너 이름 (기본값: `""`) /// - birthDate: 입력된 생년월일 (기본값: `""`) /// - height: 입력된 키 (기본값: `""`) /// - weight: 입력된 몸무게 (기본값: `""`) diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeTrainingInfoInput/TraineeTrainingInfoInputView.swift b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeTrainingInfoInput/TraineeTrainingInfoInputView.swift index e95d0ca8..e6059fce 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeTrainingInfoInput/TraineeTrainingInfoInputView.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeTrainingInfoInput/TraineeTrainingInfoInputView.swift @@ -35,7 +35,7 @@ public struct TraineeTrainingInfoInputView: View { leftAction: { dismiss() } ) - TInfoTitleHeader(title: "\(store.trainerName)트레이너와\n언제부터 함께하셨나요?") + TInfoTitleHeader(title: "\(store.trainerName) 트레이너와\n언제부터 함께하셨나요?") .padding(.top, 24) .padding(.bottom, 48) diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Trainer/Login/Code/MakeInvitationCodeView.swift b/TnT/Projects/Presentation/Sources/Onboarding/Trainer/Login/Code/MakeInvitationCodeView.swift index 604a008a..e7b66b79 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Trainer/Login/Code/MakeInvitationCodeView.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Trainer/Login/Code/MakeInvitationCodeView.swift @@ -74,6 +74,7 @@ public struct MakeInvitationCodeView: View { ) { send(.tappedReissuanceButton) } + .frame(width: 82) } } From 4e57ad894d1faeb7d130a2983cf7b16aed732124 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Tue, 11 Feb 2025 03:34:09 +0900 Subject: [PATCH 4/7] =?UTF-8?q?[Feat]=20=ED=8A=B8=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EB=8B=88=20API=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DIContainer/Sources/DIContainer.swift | 12 +++++++++++ .../Trainee/TraineeRepositoryImpl.swift | 11 ++-------- .../DTO/Trainee/TraineeResponseDTO.swift | 8 +++---- .../Entity/TraineeConnectInfoEntity.swift | 21 ------------------- .../Repository/TraineeRepository.swift | 4 ++-- .../Sources/UseCase/TraineeUseCase.swift | 13 +++++++++--- 6 files changed, 30 insertions(+), 39 deletions(-) delete mode 100644 TnT/Projects/Domain/Sources/Entity/TraineeConnectInfoEntity.swift diff --git a/TnT/Projects/DIContainer/Sources/DIContainer.swift b/TnT/Projects/DIContainer/Sources/DIContainer.swift index 7f50c864..3ec1dc77 100644 --- a/TnT/Projects/DIContainer/Sources/DIContainer.swift +++ b/TnT/Projects/DIContainer/Sources/DIContainer.swift @@ -26,6 +26,13 @@ private enum TraineeUseCaseKey: DependencyKey { ) } +private enum TraineeRepoUseCaseKey: DependencyKey { + static let liveValue: TraineeRepository = DefaultTraineeUseCase( + trainerRepository: TrainerRepositoryImpl(), + traineeRepository: TraineeRepositoryImpl() + ) +} + private enum SocialUseCaseKey: DependencyKey { static let liveValue: SocialLoginUseCase = SocialLoginUseCase(socialLoginRepository: SocialLogInRepositoryImpl(loginManager: SNSLoginManager())) } @@ -60,6 +67,11 @@ public extension DependencyValues { set { self[TraineeUseCaseKey.self] = newValue } } + var traineeRepoUseCase: TraineeRepository { + get { self[TraineeRepoUseCaseKey.self] } + set { self[TraineeRepoUseCaseKey.self] = newValue } + } + var socialLogInUseCase: SocialLoginUseCase { get { self[SocialUseCaseKey.self] } set { self[SocialUseCaseKey.self] = newValue } diff --git a/TnT/Projects/Data/Sources/Network/Service/Trainee/TraineeRepositoryImpl.swift b/TnT/Projects/Data/Sources/Network/Service/Trainee/TraineeRepositoryImpl.swift index 48c7a65b..eb16b99c 100644 --- a/TnT/Projects/Data/Sources/Network/Service/Trainee/TraineeRepositoryImpl.swift +++ b/TnT/Projects/Data/Sources/Network/Service/Trainee/TraineeRepositoryImpl.swift @@ -17,16 +17,9 @@ public struct TraineeRepositoryImpl: TraineeRepository { public init() {} - public func postConnectTrainer(_ info: TraineeConnectInfoEntity) async throws -> PostConnectTrainerResDTO { - let requestDTO = PostConnectTrainerReqDTO( - invitationCode: info.invitationCode, - startDate: info.startDate, - totalPtCount: info.totalPtCount, - finishedPtCount: info.finishedPtCount - ) - + public func postConnectTrainer(_ reqDTO: PostConnectTrainerReqDTO) async throws -> PostConnectTrainerResDTO { return try await networkService.request( - TraineeTargetType.postConnectTrainer(reqDto: requestDTO), + TraineeTargetType.postConnectTrainer(reqDto: reqDTO), decodingType: PostConnectTrainerResDTO.self ) } diff --git a/TnT/Projects/Domain/Sources/DTO/Trainee/TraineeResponseDTO.swift b/TnT/Projects/Domain/Sources/DTO/Trainee/TraineeResponseDTO.swift index 8a03e0b8..f51d513d 100644 --- a/TnT/Projects/Domain/Sources/DTO/Trainee/TraineeResponseDTO.swift +++ b/TnT/Projects/Domain/Sources/DTO/Trainee/TraineeResponseDTO.swift @@ -11,11 +11,11 @@ import Foundation /// 트레이너 연결 응답 DTO public struct PostConnectTrainerResDTO: Decodable { /// 트레이너 이름 - let trainerName: String + public let trainerName: String /// 트레이니 이름 - let traineeName: String + public let traineeName: String /// 트레이너 프로필 이미지 URL - let trainerProfileImageUrl: String + public let trainerProfileImageUrl: String /// 트레이니 프로필 이미지 URL - let traineeProfileImageUrl: String + public let traineeProfileImageUrl: String } diff --git a/TnT/Projects/Domain/Sources/Entity/TraineeConnectInfoEntity.swift b/TnT/Projects/Domain/Sources/Entity/TraineeConnectInfoEntity.swift deleted file mode 100644 index c917dd2e..00000000 --- a/TnT/Projects/Domain/Sources/Entity/TraineeConnectInfoEntity.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// TraineeConnectInfoEntity.swift -// Domain -// -// Created by 박민서 on 1/27/25. -// Copyright © 2025 yapp25thTeamTnT. All rights reserved. -// - -import Foundation - -/// 트레이너 연결을 위한 도메인 모델 -public struct TraineeConnectInfoEntity { - /// 초대 코드 - public let invitationCode: String - /// 시작 날짜 - public let startDate: String - /// 총 PT 횟수 - public let totalPtCount: Int - /// 현재 PT 횟수 - public let finishedPtCount: Int -} diff --git a/TnT/Projects/Domain/Sources/Repository/TraineeRepository.swift b/TnT/Projects/Domain/Sources/Repository/TraineeRepository.swift index 4877ec0c..386a2147 100644 --- a/TnT/Projects/Domain/Sources/Repository/TraineeRepository.swift +++ b/TnT/Projects/Domain/Sources/Repository/TraineeRepository.swift @@ -12,8 +12,8 @@ import Foundation /// - 실제 네트워크 요청은 이 인터페이스를 구현한 `TraineeRepositoryImpl`에서 수행됩니다. public protocol TraineeRepository { /// 트레이너와 트레이니의 연결을 요청합니다. - /// - Parameter info: 트레이너 연결을 위한 연결 정보 (`TraineeConnectInfo`) + /// - Parameter info: 트레이너 연결을 위한 연결 정보 (`PostConnectTrainerReqDTO`) /// - Returns: 연결 성공 시, 연결된 트레이너 정보가 포함된 응답 DTO (`PostConnectTrainerResDTO`) /// - Throws: 네트워크 오류 또는 잘못된 요청 데이터로 인한 서버 오류 발생 가능 - func postConnectTrainer(_ info: TraineeConnectInfoEntity) async throws -> PostConnectTrainerResDTO + func postConnectTrainer(_ reqDTO: PostConnectTrainerReqDTO) async throws -> PostConnectTrainerResDTO } diff --git a/TnT/Projects/Domain/Sources/UseCase/TraineeUseCase.swift b/TnT/Projects/Domain/Sources/UseCase/TraineeUseCase.swift index 0cf7af59..6554d6a9 100644 --- a/TnT/Projects/Domain/Sources/UseCase/TraineeUseCase.swift +++ b/TnT/Projects/Domain/Sources/UseCase/TraineeUseCase.swift @@ -19,7 +19,7 @@ public protocol TraineeUseCase { /// API Call - 트레이너 초대 코드 인증 API 호출 func verifyTrainerInvitationCode(_ code: String) async throws -> Bool /// API Call - 트레이니 - 트레이너 연결 API 호출 - func connectTrainer(_ info: TraineeConnectInfoEntity) async throws -> ConnectionInfoEntity + func connectTrainer(_ reqDTO: PostConnectTrainerReqDTO) async throws -> ConnectionInfoEntity } // MARK: - Default 구현체 @@ -50,8 +50,8 @@ public struct DefaultTraineeUseCase: TraineeUseCase { return result.isVerified } - public func connectTrainer(_ info: TraineeConnectInfoEntity) async throws -> ConnectionInfoEntity { - let resDTO: PostConnectTrainerResDTO = try await traineeRepository.postConnectTrainer(info) + public func connectTrainer(_ reqDTO: PostConnectTrainerReqDTO) async throws -> ConnectionInfoEntity { + let resDTO: PostConnectTrainerResDTO = try await traineeRepository.postConnectTrainer(reqDTO) return ConnectionInfoEntity( trainerName: resDTO.trainerName, traineeName: resDTO.traineeName, @@ -60,3 +60,10 @@ public struct DefaultTraineeUseCase: TraineeUseCase { ) } } + +// MARK: Repository +extension DefaultTraineeUseCase: TraineeRepository { + public func postConnectTrainer(_ reqDTO: PostConnectTrainerReqDTO) async throws -> PostConnectTrainerResDTO { + return try await traineeRepository.postConnectTrainer(reqDTO) + } +} From ffbb3065bf6f148317bd79e1895b29d88642e27f Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Tue, 11 Feb 2025 03:34:48 +0900 Subject: [PATCH 5/7] =?UTF-8?q?[Feat]=20=ED=8A=B8=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EB=8B=88=20=20PT=20=EC=A0=95=EB=B3=B4=20=EC=9E=85=EB=A0=A5=20-?= =?UTF-8?q?>=20=EC=97=B0=EA=B2=B0=20=EC=99=84=EB=A3=8C=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20API,=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=ED=9D=90?= =?UTF-8?q?=EB=A6=84=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OnboardingFlowFeature.swift | 18 +++++-- .../TraineeMainFlowFeature.swift | 24 ++++++--- .../TraineeInvitationCodeInputFeature.swift | 10 ++-- .../TraineeInvitationCodeInputView.swift | 5 ++ .../TraineeTrainingInfoInputFeature.swift | 53 +++++++++++++++++-- 5 files changed, 92 insertions(+), 18 deletions(-) diff --git a/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift b/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift index fec6bd28..c8e19506 100644 --- a/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift +++ b/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift @@ -111,14 +111,24 @@ public struct OnboardingFlowFeature { switch screen { case .traineeHome: return .send(.switchFlow(.traineeMainFlow)) - case .trainingInfoInput(let name): - state.path.append(.traineeTrainingInfoInput(.init(trainerName: name))) + case let .trainingInfoInput(trainerName, invitationCode): + state.path.append(.traineeTrainingInfoInput(.init(trainerName: trainerName, invitationCode: invitationCode))) return .none } /// 트레이니 PT 정보 입력 화면 -> 연결 완료 화면 - case .element(id: _, action: .traineeTrainingInfoInput(.setNavigating)): - state.path.append(.traineeConnectionComplete(.init(userType: .trainee, traineeName: "1", trainerName: "2"))) + case let .element(id: _, action: .traineeTrainingInfoInput(.setNavigating(.connectionComplete(trainerName, traineeName, trainerImageUrl, traineeImageUrl)))): + state.path.append( + .traineeConnectionComplete( + .init( + userType: .trainee, + traineeName: traineeName, + traineeImageURL: traineeImageUrl, + trainerName: trainerName, + trainerImageURL: trainerImageUrl + ) + ) + ) return .none /// 트레이니 트레이너 연결완료 -> 트레이니 홈화면 diff --git a/TnT/Projects/Presentation/Sources/Coordinator/TraineeMainFlow/TraineeMainFlowFeature.swift b/TnT/Projects/Presentation/Sources/Coordinator/TraineeMainFlow/TraineeMainFlowFeature.swift index 31eaf6e7..4590902d 100644 --- a/TnT/Projects/Presentation/Sources/Coordinator/TraineeMainFlow/TraineeMainFlowFeature.swift +++ b/TnT/Projects/Presentation/Sources/Coordinator/TraineeMainFlow/TraineeMainFlowFeature.swift @@ -33,7 +33,9 @@ public struct TraineeMainFlowFeature { public init() {} public var body: some ReducerOf { - Reduce { state, action in + Reduce { + state, + action in switch action { case let .path(action): switch action { @@ -80,20 +82,30 @@ public struct TraineeMainFlowFeature { case .element(_, action: .alarmCheck(.setNavigating)): // 특정 화면 append return .none - + /// 마이페이지 초대코드 입력화면 다음 버튼 탭 - > PT 정보 입력 화면 or 홈 이동 case .element(_, action: .traineeInvitationCodeInput(.setNavigating(let screen))): switch screen { case .traineeHome: state.path.removeLast() - case .trainingInfoInput(let trainerName): - state.path.append(.traineeTrainingInfoInput(.init(trainerName: trainerName))) + case let .trainingInfoInput(trainerName, invitationCode): + state.path.append(.traineeTrainingInfoInput(.init(trainerName: trainerName, invitationCode: invitationCode))) } return .none /// PT 정보 입력 화면 다음 버튼 탭 -> 연결 완료 화면 이동 - case .element(_, action: .traineeTrainingInfoInput(.setNavigating)): - state.path.append(.traineeConnectionComplete(.init(userType: .trainee, traineeName: "여기에", trainerName: "데이터 연결"))) + case let .element(id: _, action: .traineeTrainingInfoInput(.setNavigating(.connectionComplete(trainerName, traineeName, trainerImageUrl, traineeImageUrl)))): + state.path.append( + .traineeConnectionComplete( + .init( + userType: .trainee, + traineeName: traineeName, + traineeImageURL: traineeImageUrl, + trainerName: trainerName, + trainerImageURL: trainerImageUrl + ) + ) + ) return .none default: diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeInvitationCodeInput/TraineeInvitationCodeInputFeature.swift b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeInvitationCodeInput/TraineeInvitationCodeInputFeature.swift index 5139b536..2642c1d2 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeInvitationCodeInput/TraineeInvitationCodeInputFeature.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeInvitationCodeInput/TraineeInvitationCodeInputFeature.swift @@ -139,12 +139,14 @@ public struct TraineeInvitationCodeInputFeature { return .none case .tapVerifyButton: - return .send(.api(.verifyInvitationCode(code: state.invitationCode))) + return .concatenate( + .send(.view(.setFocus(false))), + .send(.api(.verifyInvitationCode(code: state.invitationCode))) + ) case .tapNextButton: guard let trainerName = state.trainerName else { return .none } - print("trainerName: \(trainerName)") - return .send(.setNavigating(.trainingInfoInput(trainerName: trainerName))) + return .send(.setNavigating(.trainingInfoInput(trainerName: trainerName, invitationCode: state.invitationCode))) case .setFocus(let isFocused): state.view_isFieldFocused = isFocused @@ -229,7 +231,7 @@ public extension TraineeInvitationCodeInputFeature { /// 본 화면에서 라우팅(파생)되는 화면 enum RoutingScreen: Sendable { case traineeHome - case trainingInfoInput(trainerName: String) + case trainingInfoInput(trainerName: String, invitationCode: String) } /// 본 화면의 네비게이션 타입 diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeInvitationCodeInput/TraineeInvitationCodeInputView.swift b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeInvitationCodeInput/TraineeInvitationCodeInputView.swift index 1986a047..c1d140b4 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeInvitationCodeInput/TraineeInvitationCodeInputView.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeInvitationCodeInput/TraineeInvitationCodeInputView.swift @@ -54,6 +54,11 @@ public struct TraineeInvitationCodeInputView: View { send(.setFocus(newValue)) } } + .onChange(of: store.view_isFieldFocused) { oldValue, newValue in + if oldValue != newValue { + focusedField = newValue + } + } .tPopUp(isPresented: $store.view_isPopupPresented) { PopUpView() } diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeTrainingInfoInput/TraineeTrainingInfoInputFeature.swift b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeTrainingInfoInput/TraineeTrainingInfoInputFeature.swift index e683a326..21683ff3 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeTrainingInfoInput/TraineeTrainingInfoInputFeature.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeTrainingInfoInput/TraineeTrainingInfoInputFeature.swift @@ -28,6 +28,8 @@ public struct TraineeTrainingInfoInputFeature { var currentCount: String /// 입력된 몸무게 var totalCount: String + /// 트레이너 초대코드 + var invitationCode: String // MARK: UI related state /// 텍스트 필드 상태 (빈 값 / 입력됨 / 유효하지 않음) @@ -57,6 +59,7 @@ public struct TraineeTrainingInfoInputFeature { startDate: String = "", currentCount: String = "", totalCount: String = "", + invitationCode: String = "", view_startDateStatus: TTextField.Status = .empty, view_currentCountStatus: TTextField.Status = .empty, view_totalCountStatus: TTextField.Status = .empty, @@ -67,6 +70,7 @@ public struct TraineeTrainingInfoInputFeature { self.startDate = startDate self.currentCount = currentCount self.totalCount = totalCount + self.invitationCode = invitationCode self.view_startDateStatus = view_startDateStatus self.view_currentCountStatus = view_currentCountStatus self.view_totalCountStatus = view_totalCountStatus @@ -76,12 +80,15 @@ public struct TraineeTrainingInfoInputFeature { } @Dependency(\.traineeUseCase) private var traineeUseCase: TraineeUseCase + @Dependency(\.traineeRepoUseCase) private var traineeRepoUseCase public enum Action: Sendable, ViewAction { /// 뷰에서 발생한 액션을 처리합니다. case view(View) + /// api 콜 액션 처리 + case api(APIAction) /// 네비게이션 여부 설정 - case setNavigating + case setNavigating(RoutingScreen) @CasePathable public enum View: Sendable, BindableAction { @@ -96,6 +103,12 @@ public struct TraineeTrainingInfoInputFeature { /// 포커스 상태 변경 case setFocus(FocusField?, FocusField?) } + + @CasePathable + public enum APIAction: Sendable { + /// 초대 코드 인증하기 API + case verifyInvitationCode(code: String) + } } public init() {} @@ -109,11 +122,11 @@ public struct TraineeTrainingInfoInputFeature { switch action { case .binding: return .none - + case .tapStartDateTextField: state.view_isDatePickerPresented = true return .send(.view(.setFocus(state.view_focusField, .startDate))) - + case .tapStartDatePickerDoneButton(let date): state.view_isDatePickerPresented = false state.startDate = date.toString(format: .yyyyMMddSlash) @@ -127,7 +140,33 @@ public struct TraineeTrainingInfoInputFeature { : .none case .tapNextButton: - return .send(.setNavigating) + return .send(.api(.verifyInvitationCode(code: state.invitationCode))) + } + + case .api(let action): + switch action { + case .verifyInvitationCode(let code): + guard let currentCount = Int(state.currentCount), let totalCount = Int(state.totalCount) else { return .none } + return .run { [state] send in + let result = try await traineeRepoUseCase.postConnectTrainer( + .init( + invitationCode: state.invitationCode, + startDate: state.startDate.replacingOccurrences(of: "/", with: "-"), + totalPtCount: totalCount, + finishedPtCount: currentCount + ) + ) + await send( + .setNavigating( + .connectionComplete( + trainerName: result.trainerName, + traineeName: result.traineeName, + trainerImageUrl: result.trainerProfileImageUrl, + traineeImageUrl: result.traineeProfileImageUrl + ) + ) + ) + } } case .setNavigating: @@ -182,3 +221,9 @@ private extension TraineeTrainingInfoInputFeature { return .none } } + +extension TraineeTrainingInfoInputFeature { + public enum RoutingScreen { + case connectionComplete(trainerName: String, traineeName: String, trainerImageUrl: String, traineeImageUrl: String) + } +} From 53f1566700c3c23aa40a8bd4237e8dfa2aff45b2 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Tue, 11 Feb 2025 05:18:04 +0900 Subject: [PATCH 6/7] =?UTF-8?q?[Feat]=20Trainer=20-=20=EC=97=B0=EA=B2=B0?= =?UTF-8?q?=EB=90=9C=20=ED=8A=B8=EB=A0=88=EC=9D=B4=EB=8B=88=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EB=B6=88=EB=9F=AC=EC=98=A4=EA=B8=B0=20API=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Trainer/TrainerRepositoryImpl.swift | 6 ++- .../Service/Trainer/TrainerTargetType.swift | 25 ++++++++--- .../GetConnectedTraineeInfoResponseDTO.swift | 41 +++++++++++++++++ .../ConnectedTraineeProfileEntity.swift | 45 +++++++++++++++++++ .../Sources/Entity/ConnectionInfoEntity.swift | 2 +- .../Domain/Sources/Mapper/TrainerMapper.swift | 34 ++++++++++++++ .../Repository/TrainerRepository.swift | 3 ++ .../Sources/UseCase/TrainerUseCase.swift | 4 ++ 8 files changed, 152 insertions(+), 8 deletions(-) create mode 100644 TnT/Projects/Domain/Sources/DTO/Trainer/GetConnectedTraineeInfoResponseDTO.swift create mode 100644 TnT/Projects/Domain/Sources/Entity/ConnectedTraineeProfileEntity.swift create mode 100644 TnT/Projects/Domain/Sources/Mapper/TrainerMapper.swift diff --git a/TnT/Projects/Data/Sources/Network/Service/Trainer/TrainerRepositoryImpl.swift b/TnT/Projects/Data/Sources/Network/Service/Trainer/TrainerRepositoryImpl.swift index dc6339a1..c5271449 100644 --- a/TnT/Projects/Data/Sources/Network/Service/Trainer/TrainerRepositoryImpl.swift +++ b/TnT/Projects/Data/Sources/Network/Service/Trainer/TrainerRepositoryImpl.swift @@ -41,7 +41,7 @@ public struct TrainerRepositoryImpl: TrainerRepository { public func getDateSessionList(date: String) async throws -> GetDateSessionListDTO { return try await networkService.request( - TrainerTargetType.getDateLessionList(date: date), + TrainerTargetType.getDateLessonList(date: date), decodingType: GetDateSessionListDTO.self ) } @@ -52,4 +52,8 @@ public struct TrainerRepositoryImpl: TrainerRepository { decodingType: GetMembersListDTO.self ) } + + public func getConnectedTraineeInfo(trainerId: Int, traineeId: Int) async throws -> GetConnectedTraineeInfoResponseDTO { + return try await networkService.request(TrainerTargetType.getConnectedTraineeInfo(trainerId: trainerId, traineeId: traineeId), decodingType: GetConnectedTraineeInfoResponseDTO.self) + } } diff --git a/TnT/Projects/Data/Sources/Network/Service/Trainer/TrainerTargetType.swift b/TnT/Projects/Data/Sources/Network/Service/Trainer/TrainerTargetType.swift index f42faa3e..b55c4a42 100644 --- a/TnT/Projects/Data/Sources/Network/Service/Trainer/TrainerTargetType.swift +++ b/TnT/Projects/Data/Sources/Network/Service/Trainer/TrainerTargetType.swift @@ -17,11 +17,13 @@ public enum TrainerTargetType { /// 트레이너 초대코드 불러오기 case getFirstInvitationCode /// 트레이너 캘린더, 특정 날짜의 PT 리스트 불러오기 - case getDateLessionList(date: String) + case getDateLessonList(date: String) /// 트레이너 초대코드 재발급 case getReissuanceInvitationCode /// 회원 조희 case getMemebersList + /// 연결 완료된 트레이니 최초로 정보 불러오기 + case getConnectedTraineeInfo(trainerId: Int, traineeId: Int) } extension TrainerTargetType: TargetType { @@ -36,12 +38,14 @@ extension TrainerTargetType: TargetType { return "/invitation-code/verify/\(code)" case .getFirstInvitationCode: return "/invitation-code" - case .getDateLessionList(let date): - return "/lessions/\(date)" + case .getDateLessonList(let date): + return "/lessons/\(date)" case .getReissuanceInvitationCode: return "/invitation-code/reissue" case .getMemebersList: return "/members" + case .getConnectedTraineeInfo: + return "/first-connected-trainee" } } @@ -51,12 +55,14 @@ extension TrainerTargetType: TargetType { return .get case .getFirstInvitationCode: return .get - case .getDateLessionList: + case .getDateLessonList: return .get case .getReissuanceInvitationCode: return .put case .getMemebersList: return .get + case .getConnectedTraineeInfo: + return .get } } @@ -66,12 +72,17 @@ extension TrainerTargetType: TargetType { return .requestPlain case .getFirstInvitationCode: return .requestPlain - case .getDateLessionList: + case .getDateLessonList: return .requestPlain case .getReissuanceInvitationCode: return .requestPlain case .getMemebersList: return .requestPlain + case let .getConnectedTraineeInfo(trainerId, traineeId): + return .requestParameters(parameters: [ + "trainerId": trainerId, + "traineeId": traineeId + ], encoding: .url) } } @@ -81,12 +92,14 @@ extension TrainerTargetType: TargetType { return ["Content-Type": "application/json"] case .getFirstInvitationCode: return nil - case .getDateLessionList: + case .getDateLessonList: return ["Content-Type": "application/json"] case .getReissuanceInvitationCode: return ["Content-Type": "application/json"] case .getMemebersList: return ["Content-Type": "application/json"] + case .getConnectedTraineeInfo: + return nil } } diff --git a/TnT/Projects/Domain/Sources/DTO/Trainer/GetConnectedTraineeInfoResponseDTO.swift b/TnT/Projects/Domain/Sources/DTO/Trainer/GetConnectedTraineeInfoResponseDTO.swift new file mode 100644 index 00000000..96ca2dd7 --- /dev/null +++ b/TnT/Projects/Domain/Sources/DTO/Trainer/GetConnectedTraineeInfoResponseDTO.swift @@ -0,0 +1,41 @@ +// +// GetConnectedTraineeInfoResponseDTO.swift +// Domain +// +// Created by 박민서 on 2/11/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import Foundation + +/// 연결 완료된 트레이니 정보 불러오기 응답 DTO +public struct GetConnectedTraineeInfoResponseDTO: Decodable { + /// 연결 트레이니 DTO + public let trainer: ConnectTrainerInfoDTO + /// 연결 트레이너 DTO + public let trainee: ConnectTraineeInfoDTO +} + +public struct ConnectTrainerInfoDTO: Decodable { + /// 트레이너 이름 + let trainerName: String + /// 트레이너 프로필 이미지 + let trainerProfileImageUrl: String +} + +public struct ConnectTraineeInfoDTO: Decodable { + /// 트레이니 이름 + let traineeName: String + /// 트레이니 프로필 이미지 url + let traineeProfileImageUrl: String + /// 나이 + let age: Int? + /// 키 + let height: Double + /// 몸무게 + let weight: Double + /// PT 목표 + let ptGoal: String + /// 주의 사항 + let cautionNote: String? +} diff --git a/TnT/Projects/Domain/Sources/Entity/ConnectedTraineeProfileEntity.swift b/TnT/Projects/Domain/Sources/Entity/ConnectedTraineeProfileEntity.swift new file mode 100644 index 00000000..67eb7329 --- /dev/null +++ b/TnT/Projects/Domain/Sources/Entity/ConnectedTraineeProfileEntity.swift @@ -0,0 +1,45 @@ +// +// ConnectedTraineeProfileEntity.swift +// Domain +// +// Created by 박민서 on 2/11/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import Foundation + +/// 연결 완료된 트레이니의 프로필 +public struct ConnectedTraineeProfileEntity: Equatable, Sendable { + /// 트레이니 이름 + public let traineeName: String + /// 트레이니 프로필 이미지 url + public let imageUrl: String + /// 나이 + public let age: Int? + /// 키 + public let height: Double + /// 몸무게 + public let weight: Double + /// PT 목표 + public let ptGoal: String + /// 주의 사항 + public let cautionNote: String? + + public init( + traineeName: String, + imageUrl: String, + age: Int?, + height: Double, + weight: Double, + ptGoal: String, + cautionNote: String? + ) { + self.traineeName = traineeName + self.imageUrl = imageUrl + self.age = age + self.height = height + self.weight = weight + self.ptGoal = ptGoal + self.cautionNote = cautionNote + } +} diff --git a/TnT/Projects/Domain/Sources/Entity/ConnectionInfoEntity.swift b/TnT/Projects/Domain/Sources/Entity/ConnectionInfoEntity.swift index 955c0c66..9196a690 100644 --- a/TnT/Projects/Domain/Sources/Entity/ConnectionInfoEntity.swift +++ b/TnT/Projects/Domain/Sources/Entity/ConnectionInfoEntity.swift @@ -9,7 +9,7 @@ import Foundation /// 트레이너와 트레이니가 연결된 정보 -public struct ConnectionInfoEntity { +public struct ConnectionInfoEntity: Equatable, Sendable { /// 트레이너 이름 public let trainerName: String /// 트레이니 이름 diff --git a/TnT/Projects/Domain/Sources/Mapper/TrainerMapper.swift b/TnT/Projects/Domain/Sources/Mapper/TrainerMapper.swift new file mode 100644 index 00000000..c16a9f20 --- /dev/null +++ b/TnT/Projects/Domain/Sources/Mapper/TrainerMapper.swift @@ -0,0 +1,34 @@ +// +// TrainerMapper.swift +// Domain +// +// Created by 박민서 on 2/11/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import Foundation + +public extension ConnectTraineeInfoDTO { + func toEntity() -> ConnectedTraineeProfileEntity { + return .init( + traineeName: self.traineeName, + imageUrl: self.traineeProfileImageUrl, + age: self.age, + height: self.height, + weight: self.weight, + ptGoal: self.ptGoal, + cautionNote: self.cautionNote + ) + } +} + +public extension GetConnectedTraineeInfoResponseDTO { + func toEntity() -> ConnectionInfoEntity { + return .init( + trainerName: self.trainer.trainerName, + traineeName: self.trainee.traineeName, + trainerProfileImageUrl: self.trainer.trainerProfileImageUrl, + traineeProfileImageUrl: self.trainee.traineeProfileImageUrl + ) + } +} diff --git a/TnT/Projects/Domain/Sources/Repository/TrainerRepository.swift b/TnT/Projects/Domain/Sources/Repository/TrainerRepository.swift index c6f27016..7dc76c4c 100644 --- a/TnT/Projects/Domain/Sources/Repository/TrainerRepository.swift +++ b/TnT/Projects/Domain/Sources/Repository/TrainerRepository.swift @@ -28,4 +28,7 @@ public protocol TrainerRepository { /// 회원 조희 func getMembersList() async throws -> GetMembersListDTO + + /// 연결 완료된 트레이니 정보 불러오기 + func getConnectedTraineeInfo(trainerId: Int, traineeId: Int) async throws -> GetConnectedTraineeInfoResponseDTO } diff --git a/TnT/Projects/Domain/Sources/UseCase/TrainerUseCase.swift b/TnT/Projects/Domain/Sources/UseCase/TrainerUseCase.swift index 2b906ff3..1f6e4096 100644 --- a/TnT/Projects/Domain/Sources/UseCase/TrainerUseCase.swift +++ b/TnT/Projects/Domain/Sources/UseCase/TrainerUseCase.swift @@ -36,4 +36,8 @@ public struct DefaultTrainerUseCase: TrainerRepository { public func getMembersList() async throws -> GetMembersListDTO { return try await trainerRepository.getMembersList() } + + public func getConnectedTraineeInfo(trainerId: Int, traineeId: Int) async throws -> GetConnectedTraineeInfoResponseDTO { + return try await trainerRepository.getConnectedTraineeInfo(trainerId: trainerId, traineeId: traineeId) + } } From a098445dcbf44dfd796fd17e56c8f4743aab75fe Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Tue, 11 Feb 2025 05:18:55 +0900 Subject: [PATCH 7/7] =?UTF-8?q?[Feat]=20ConnectionComplete,=20ConnectedTra?= =?UTF-8?q?ineeProfile=20API=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=ED=9D=90=EB=A6=84=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TrainerMainFlowFeature.swift | 8 ++ .../TrainerMainFlow/TrainerMainFlowView.swift | 4 + .../TraineeConnectionCompleteView.swift | 2 +- .../ConnectedTraineeProfileFeature.swift | 8 +- .../ConnectedTraineeProfileView.swift | 69 ++++++++++-- .../ConnectionCompleteFeature.swift | 62 ++++++++++- .../Connection/ConnectionCompleteView.swift | 104 ++++++++++++++---- 7 files changed, 213 insertions(+), 44 deletions(-) diff --git a/TnT/Projects/Presentation/Sources/Coordinator/TrainerMainFlow/TrainerMainFlowFeature.swift b/TnT/Projects/Presentation/Sources/Coordinator/TrainerMainFlow/TrainerMainFlowFeature.swift index ab8093cc..7300085f 100644 --- a/TnT/Projects/Presentation/Sources/Coordinator/TrainerMainFlow/TrainerMainFlowFeature.swift +++ b/TnT/Projects/Presentation/Sources/Coordinator/TrainerMainFlow/TrainerMainFlowFeature.swift @@ -71,6 +71,10 @@ public struct TrainerMainFlowFeature { state.path.removeLast() return .none + case .element(id: _, action: .connectionComplete(.setNavigating(let profile))): + state.path.append(.connectedTraineeProfile(.init(traineeProfile: profile))) + return .none + default: return .none } @@ -99,6 +103,10 @@ extension TrainerMainFlowFeature { case alarmCheck(AlarmCheckFeature) /// PT 일정 추가 case addPTSession(TrainerAddPTSessionFeature) + /// 연결 완료 + case connectionComplete(ConnectionCompleteFeature) + /// 연결된 트레이니 프로필 + case connectedTraineeProfile(ConnectedTraineeProfileFeature) // MARK: MyPage /// 초대코드 발급 diff --git a/TnT/Projects/Presentation/Sources/Coordinator/TrainerMainFlow/TrainerMainFlowView.swift b/TnT/Projects/Presentation/Sources/Coordinator/TrainerMainFlow/TrainerMainFlowView.swift index 7deb27bf..aea9ec95 100644 --- a/TnT/Projects/Presentation/Sources/Coordinator/TrainerMainFlow/TrainerMainFlowView.swift +++ b/TnT/Projects/Presentation/Sources/Coordinator/TrainerMainFlow/TrainerMainFlowView.swift @@ -32,6 +32,10 @@ public struct TrainerMainFlowView: View { // MARK: Home case .alarmCheck(let store): AlarmCheckView(store: store) + case .connectionComplete(let store): + ConnectionCompleteView(store: store) + case .connectedTraineeProfile(let store): + ConnectedTraineeProfileView(store: store) // MARK: MyPage case .trainerMakeInvitationCodePage(let store): diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeConnectionComplete/TraineeConnectionCompleteView.swift b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeConnectionComplete/TraineeConnectionCompleteView.swift index 9aaffee1..374e0e2c 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeConnectionComplete/TraineeConnectionCompleteView.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeConnectionComplete/TraineeConnectionCompleteView.swift @@ -59,7 +59,7 @@ public struct TraineeConnectionCompleteView: View { @ViewBuilder private func Header() -> some View { VStack(spacing: 40) { - Text("\(store.view_opponentUserName) \(store.view_opponentUserType.koreanName)와\n연결되었어요!") + Text("\(store.view_opponentUserName) \(store.view_opponentUserType.koreanName)와\n연결되었어요!") .typographyStyle(.heading1, with: .common0) .multilineTextAlignment(.center) diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Trainer/Login/Connection/ConnectedTraineeProfileFeature.swift b/TnT/Projects/Presentation/Sources/Onboarding/Trainer/Login/Connection/ConnectedTraineeProfileFeature.swift index 7f002cf7..160379a1 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Trainer/Login/Connection/ConnectedTraineeProfileFeature.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Trainer/Login/Connection/ConnectedTraineeProfileFeature.swift @@ -9,13 +9,17 @@ import SwiftUI import ComposableArchitecture +import Domain + @Reducer public struct ConnectedTraineeProfileFeature { @ObservableState public struct State: Equatable { - var view_trainer: Data? + var traineeProfile: ConnectedTraineeProfileEntity - public init() { } + public init(traineeProfile: ConnectedTraineeProfileEntity) { + self.traineeProfile = traineeProfile + } } public enum Action: Equatable, ViewAction { diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Trainer/Login/Connection/ConnectedTraineeProfileView.swift b/TnT/Projects/Presentation/Sources/Onboarding/Trainer/Login/Connection/ConnectedTraineeProfileView.swift index 99fd4ff6..9ac2b34c 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Trainer/Login/Connection/ConnectedTraineeProfileView.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Trainer/Login/Connection/ConnectedTraineeProfileView.swift @@ -32,11 +32,11 @@ public struct ConnectedTraineeProfileView: View { .ignoresSafeArea() .scaledToFill() - VStack { + VStack(spacing: 0) { Spacer() traineeView() - + Spacer() TBottomButton(title: "시작하기", isEnable: true) { @@ -62,13 +62,10 @@ public struct ConnectedTraineeProfileView: View { .overlay { VStack { VStack(spacing: 10) { - Image(.imgOnboardingTrainee) - .resizable() - .frame(width: 128, height: 128) - .clipShape(Circle()) + UserProfileView(imageURL: store.traineeProfile.imageUrl) HStack(spacing: 4) { - Text("김회원") + Text(store.traineeProfile.traineeName) .typographyStyle(.heading2, with: .neutral950) Text("트레이니") .typographyStyle(.heading4, with: .neutral950) @@ -79,13 +76,15 @@ public struct ConnectedTraineeProfileView: View { VStack(spacing: 24) { HStack(spacing: 21) { - traineeProfileView(title: "나이", content: "24세") - traineeProfileView(title: "키", content: "165") - traineeProfileView(title: "체중", content: "52kg") + if let age = store.traineeProfile.age { + traineeProfileView(title: "나이", content: "\(age)세") + } + traineeProfileView(title: "키", content: "\(store.traineeProfile.height)") + traineeProfileView(title: "체중", content: "\(store.traineeProfile.weight)") } - traineeInfoView(content: "contentententne", type: .goal) - traineeInfoView(content: "catoutoingldsjl", type: .caution) + traineeInfoView(content: store.traineeProfile.ptGoal, type: .goal) + traineeInfoView(content: store.traineeProfile.cautionNote ?? "", type: .caution) } } .padding(.init(top: 32, leading: 20, bottom: 32, trailing: 20)) @@ -122,3 +121,49 @@ public struct ConnectedTraineeProfileView: View { } } } + +private extension ConnectedTraineeProfileView { + struct UserProfileView: View { + let imageURL: String? + let defaultImage: ImageResource = .imgDefaultTraineeImage + + var body: some View { + Group { + if let urlString = imageURL, let url = URL(string: urlString) { + AsyncImage(url: url) { phase in + switch phase { + case .empty: + ProgressView() + .tint(.red500) + .frame(width: 128, height: 128) + + case .success(let image): + image + .resizable() + .frame(width: 128, height: 128) + .scaledToFill() + .clipShape(Circle()) + + case .failure: + Image(defaultImage) + .resizable() + .frame(width: 128, height: 128) + .scaledToFill() + .clipShape(Circle()) + + @unknown default: + EmptyView() + } + } + } else { + Image(defaultImage) + .resizable() + .frame(width: 128, height: 128) + .scaledToFill() + .clipShape(Circle()) + } + } + .frame(width: 128, height: 128) + } + } +} diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Trainer/Login/Connection/ConnectionCompleteFeature.swift b/TnT/Projects/Presentation/Sources/Onboarding/Trainer/Login/Connection/ConnectionCompleteFeature.swift index 35fbd0ce..9e1f3985 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Trainer/Login/Connection/ConnectionCompleteFeature.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Trainer/Login/Connection/ConnectionCompleteFeature.swift @@ -9,23 +9,48 @@ import Foundation import ComposableArchitecture +import Domain + @Reducer public struct ConnectionCompleteFeature { @ObservableState public struct State: Equatable { - var view_trainer: Data? - var view_trainee: Data? + var traineeId: Int? + var trainerId: Int? + var connectionInfo: ConnectionInfoEntity? + var traineeProfile: ConnectedTraineeProfileEntity? - public init() { } + public init( + traineeId: Int? = nil, + trainerId: Int? = nil, + connectionInfo: ConnectionInfoEntity? = nil, + traineeProfile: ConnectedTraineeProfileEntity? = nil + ) { + self.traineeId = traineeId + self.trainerId = trainerId + self.connectionInfo = connectionInfo + self.traineeProfile = traineeProfile + } } + @Dependency(\.trainerRepoUseCase) private var trainerRepoUseCase + public enum Action: Sendable, ViewAction { - case setNavigating case view(View) + case api(APIAction) + case setNavigating(ConnectedTraineeProfileEntity) + case setTraineeProfile(ConnectedTraineeProfileEntity) + case setConnectionInfo(ConnectionInfoEntity) @CasePathable public enum View: Sendable { case tappedNextButton + case onAppear + } + + @CasePathable + public enum APIAction: Sendable { + case getConnectedTraineeInfo } } @@ -37,9 +62,36 @@ public struct ConnectionCompleteFeature { case .view(let action): switch action { case .tappedNextButton: - return .send(.setNavigating) + guard let profile = state.traineeProfile else { return .none } + return .send(.setNavigating(profile)) + + case .onAppear: + return .send(.api(.getConnectedTraineeInfo)) } + case .api(let action): + switch action { + case .getConnectedTraineeInfo: + guard let trainerId = state.trainerId, let traineeId = state.traineeId else { return .none } + return .run { send in + let result = try await trainerRepoUseCase.getConnectedTraineeInfo(trainerId: trainerId, traineeId: traineeId) + + let profile: ConnectedTraineeProfileEntity = result.trainee.toEntity() + let connectionInfo: ConnectionInfoEntity = result.toEntity() + + await send(.setConnectionInfo(connectionInfo)) + await send(.setTraineeProfile(profile)) + } + } + + case .setTraineeProfile(let profile): + state.traineeProfile = profile + return .none + + case .setConnectionInfo(let connectionInfo): + state.connectionInfo = connectionInfo + return .none + case .setNavigating: return .none } diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Trainer/Login/Connection/ConnectionCompleteView.swift b/TnT/Projects/Presentation/Sources/Onboarding/Trainer/Login/Connection/ConnectionCompleteView.swift index ecf80d3b..81dba811 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Trainer/Login/Connection/ConnectionCompleteView.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Trainer/Login/Connection/ConnectionCompleteView.swift @@ -10,6 +10,7 @@ import SwiftUI import ComposableArchitecture import DesignSystem +import Domain @ViewAction(for: ConnectionCompleteFeature.self) public struct ConnectionCompleteView: View { @@ -28,18 +29,22 @@ public struct ConnectionCompleteView: View { .ignoresSafeArea() .scaledToFill() - VStack { + VStack(spacing: 0) { Spacer() Header() - - Spacer() + .padding(.bottom, 35) Image(.imgBoom) + .resizable() + .scaledToFit() + .frame(width: 320, height: 320) + TBottomButton(title: "다음", isEnable: true) { send(.tappedNextButton) } .padding(.bottom, 40) + .ignoresSafeArea(.all, edges: .bottom) } } .navigationBarBackButtonHidden() @@ -48,29 +53,80 @@ public struct ConnectionCompleteView: View { @ViewBuilder private func Header() -> some View { - Text("김회원 트레이니와\n연결되었어요!") - .typographyStyle(.heading1, with: .common0) - .multilineTextAlignment(.center) - - HStack(spacing: 16) { - userProfileView(imgae: .imgOnboardingTrainee, name: "김회원") - userProfileView(imgae: .imgOnboardingTrainer, name: "김피티") + VStack(spacing: 40) { + Text("\(store.traineeProfile?.traineeName ?? "") 트레이니와\n연결되었어요!") + .typographyStyle(.heading1, with: .common0) + .multilineTextAlignment(.center) + + HStack(spacing: 16) { + Spacer() + UserProfileView( + imageURL: store.connectionInfo?.traineeProfileImageUrl, + userType: .trainee, + name: store.connectionInfo?.traineeName ?? "" + ) + UserProfileView( + imageURL: store.connectionInfo?.trainerProfileImageUrl, + userType: .trainer, + name: store.connectionInfo?.trainerName ?? "" + ) + Spacer() + } } } - - @ViewBuilder - private func userProfileView(imgae: ImageResource, name: String) -> some View { - VStack(spacing: 12) { - Image(imgae) - .resizable() - .frame(width: 100, height: 100) - .scaledToFill() - .clipShape(Circle()) - - Text(name) - .typographyStyle(.body2Medium, with: .neutral300) - .frame(maxWidth: .infinity) +} + +private extension ConnectionCompleteView { + struct UserProfileView: View { + let imageURL: URL? + let userType: UserType + let name: String + + var defaultImage: ImageResource { + self.userType == .trainee ? .imgDefaultTraineeImage : .imgDefaultTrainerImage + } + + var body: some View { + VStack(spacing: 12) { + if let url = imageURL { + AsyncImage(url: url) { phase in + switch phase { + case .empty: + ProgressView() + .tint(.red500) + .frame(width: 100, height: 100) + + case .success(let image): + image + .resizable() + .frame(width: 100, height: 100) + .scaledToFill() + .clipShape(Circle()) + + case .failure: + Image(defaultImage) + .resizable() + .frame(width: 100, height: 100) + .scaledToFill() + .clipShape(Circle()) + + @unknown default: + EmptyView() + } + } + } else { + Image(defaultImage) + .resizable() + .frame(width: 100, height: 100) + .scaledToFill() + .clipShape(Circle()) + } + + Text(name) + .typographyStyle(.body2Medium, with: .neutral300) + .frame(maxWidth: .infinity) + } + .frame(width: 100) } - .frame(width: 100) } }