From 80b04ef0a92f720e63f810d1d760a6abc82d0afd Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Sat, 8 Feb 2025 03:02:22 +0900 Subject: [PATCH 1/9] =?UTF-8?q?[Feat]=20User=20Social=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20Req/Res=20DTO=20=EC=B5=9C=EC=8B=A0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/DTO/User/UserRequestDTO.swift | 4 -- .../Sources/DTO/User/UserResponseDTO.swift | 22 +++++++- .../Sources/Entity/SocailLoginEntity.swift | 27 ++++----- .../Sources/Mapper/PostSocialMapper.swift | 55 ++++++------------- .../OnboardingFlowFeature.swift | 12 +++- .../Onboarding/Common/LoginFeature.swift | 6 +- 6 files changed, 63 insertions(+), 63 deletions(-) diff --git a/TnT/Projects/Domain/Sources/DTO/User/UserRequestDTO.swift b/TnT/Projects/Domain/Sources/DTO/User/UserRequestDTO.swift index 1d09b036..61435a70 100644 --- a/TnT/Projects/Domain/Sources/DTO/User/UserRequestDTO.swift +++ b/TnT/Projects/Domain/Sources/DTO/User/UserRequestDTO.swift @@ -16,8 +16,6 @@ public struct PostSocialLoginReqDTO: Encodable { let fcmToken: String /// 소셜 액세스 토큰 let socialAccessToken: String? - /// 애플 인가 코드 (Apple 로그인 시 필요) - let authorizationCode: String? /// 애플 ID 토큰 (Apple 로그인 시 필요) let idToken: String? @@ -25,13 +23,11 @@ public struct PostSocialLoginReqDTO: Encodable { socialType: String, fcmToken: String, socialAccessToken: String?, - authorizationCode: String?, idToken: String? ) { self.socialType = socialType self.fcmToken = fcmToken self.socialAccessToken = socialAccessToken - self.authorizationCode = authorizationCode self.idToken = idToken } } diff --git a/TnT/Projects/Domain/Sources/DTO/User/UserResponseDTO.swift b/TnT/Projects/Domain/Sources/DTO/User/UserResponseDTO.swift index 0874661f..339ca6ac 100644 --- a/TnT/Projects/Domain/Sources/DTO/User/UserResponseDTO.swift +++ b/TnT/Projects/Domain/Sources/DTO/User/UserResponseDTO.swift @@ -13,13 +13,29 @@ public struct PostSocialLoginResDTO: Decodable { /// 세션 ID public let sessionId: String? /// 소셜 로그인 ID - public let socialId: String + public let socialId: String? /// 소셜 이메일 - public let socialEmail: String + public let socialEmail: String? /// 소셜 로그인 타입 (KAKAO, APPLE) - public let socialType: String + public let socialType: String? /// 가입 여부 (`true`: 이미 가입됨, `false`: 미가입) public let isSignUp: Bool + /// 회원 타입 (TRAINER, TRAINEE, UNREGISTERED) + public let memberType: MemberTypeResDTO +} + +/// Trainer, Trainee, Unregistered로 구분되는 MemberTypeDTO +public enum MemberTypeResDTO: String, Decodable { + case trainer = "TRAINER" + case trainee = "TRAINEE" + case unregistered = "UNREGISTERED" + case unknown = "" + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let rawValue = try container.decode(String.self) + self = MemberTypeResDTO(rawValue: rawValue) ?? .unknown + } } /// 회원 정보 응답 DTO diff --git a/TnT/Projects/Domain/Sources/Entity/SocailLoginEntity.swift b/TnT/Projects/Domain/Sources/Entity/SocailLoginEntity.swift index 732aa9df..2467babe 100644 --- a/TnT/Projects/Domain/Sources/Entity/SocailLoginEntity.swift +++ b/TnT/Projects/Domain/Sources/Entity/SocailLoginEntity.swift @@ -8,30 +8,31 @@ import Foundation +public enum SocialType: String { + case kakao = "KAKAO" + case apple = "APPLE" +} + /// 소셜 로그인 요청 DTO -public struct PostSocailEntity: Equatable { +public struct PostSocialEntity: Equatable { /// 소셜 로그인 타입 (KAKAO, APPLE) - let socialType: String + let socialType: SocialType /// FCM 토큰 let fcmToken: String /// 소셜 액세스 토큰 let socialAccessToken: String? - /// 애플 인가 코드 (Apple 로그인 시 필요) - let authorizationCode: String? - /// 애플 ID 토큰 (Apple 로그인 시 필요) + /// 애플 ID 토큰 (Apple z로그인 시 필요) let idToken: String? public init( - socialType: String, + socialType: SocialType, fcmToken: String, - socialAccessToken: String?, - authorizationCode: String?, - idToken: String? + socialAccessToken: String? = nil, + idToken: String? = nil ) { self.socialType = socialType self.fcmToken = fcmToken self.socialAccessToken = socialAccessToken - self.authorizationCode = authorizationCode self.idToken = idToken } } @@ -41,11 +42,11 @@ public struct PostSocialLoginResEntity: Equatable { /// 세션 ID public let sessionId: String? /// 소셜 로그인 ID - public let socialId: String + public let socialId: String? /// 소셜 이메일 - public let socialEmail: String + public let socialEmail: String? /// 소셜 로그인 타입 (KAKAO, APPLE) - public let socialType: String + public let socialType: String? /// 가입 여부 (`true`: 이미 가입됨, `false`: 미가입) public let isSignUp: Bool } diff --git a/TnT/Projects/Domain/Sources/Mapper/PostSocialMapper.swift b/TnT/Projects/Domain/Sources/Mapper/PostSocialMapper.swift index c299a03a..6e29b3fc 100644 --- a/TnT/Projects/Domain/Sources/Mapper/PostSocialMapper.swift +++ b/TnT/Projects/Domain/Sources/Mapper/PostSocialMapper.swift @@ -8,46 +8,25 @@ import Foundation -public struct PostSocialMapper { - public static func toDTO(from entity: PostSocailEntity) -> PostSocialLoginReqDTO { - return PostSocialLoginReqDTO( - socialType: entity.socialType, - fcmToken: entity.fcmToken, - socialAccessToken: entity.socialAccessToken, - authorizationCode: entity.authorizationCode, - idToken: entity.idToken +public extension PostSocialEntity { + func toDTO() -> PostSocialLoginReqDTO { + return .init( + socialType: self.socialType.rawValue, + fcmToken: self.fcmToken, + socialAccessToken: self.socialAccessToken, + idToken: self.idToken ) } - - public static func toEntity(from dto: PostSocialLoginReqDTO) -> PostSocailEntity { - return PostSocailEntity( - socialType: dto.socialType, - fcmToken: dto.fcmToken, - socialAccessToken: dto.socialAccessToken, - authorizationCode: dto.authorizationCode, - idToken: dto.idToken - ) - } - - /// `PostSocialLoginResDTO` → `PostSocialLoginResEntity` 변환 - public static func toResEntity(from dto: PostSocialLoginResDTO) -> PostSocialLoginResEntity { - return PostSocialLoginResEntity( - sessionId: dto.sessionId, - socialId: dto.socialId, - socialEmail: dto.socialEmail, - socialType: dto.socialType, - isSignUp: dto.isSignUp +} + +public extension PostSocialLoginResDTO { + func toEntity() -> PostSocialLoginResEntity { + return .init( + sessionId: self.sessionId, + socialId: self.socialId, + socialEmail: self.socialEmail, + socialType: self.socialType, + isSignUp: self.isSignUp ) } - - /// `PostSocialLoginResEntity` → `PostSocialLoginResDTO` 변환 - public static func toDTO(from entity: PostSocialLoginResEntity) -> PostSocialLoginResDTO { - return PostSocialLoginResDTO( - sessionId: entity.sessionId, - socialId: entity.socialId, - socialEmail: entity.socialEmail, - socialType: entity.socialType, - isSignUp: entity.isSignUp - ) - } } diff --git a/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift b/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift index 3cd36667..76354b00 100644 --- a/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift +++ b/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift @@ -36,8 +36,16 @@ public struct OnboardingFlowFeature { case let .path(action): switch action { - case .element(id: _, action: .snsLogin(.view(.tappedAppleLogin))): - state.path.append(.trainerSignUpComplete(.init())) + case .element(id: _, action: .snsLogin(.setNavigating(let screen))): + switch screen { + case .traineeHome: + return .send(.switchFlow(.traineeMainFlow)) + case .trainerHome: + return .send(.switchFlow(.trainerMainFlow)) + case .term: + state.path.append(.term(.init())) + } + return .none /// 트레이너 프로필 생성 완료 -> 다음 버튼 tapped diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Common/LoginFeature.swift b/TnT/Projects/Presentation/Sources/Onboarding/Common/LoginFeature.swift index 0b96ceff..813a5440 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Common/LoginFeature.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Common/LoginFeature.swift @@ -21,7 +21,7 @@ public struct LoginFeature { public var socialType: LoginType? public var termAgree: Bool public var socialEmail: String? - public var postUserEntity: PostSocailEntity? + public var postUserEntity: PostSocialEntity? public init( userType: UserType? = nil, @@ -29,7 +29,7 @@ public struct LoginFeature { socialType: LoginType? = nil, termAgree: Bool = false, socialEmail: String? = nil, - postUserEntity: PostSocailEntity? = nil + postUserEntity: PostSocialEntity? = nil ) { self.userType = userType self.nickname = nickname @@ -50,7 +50,7 @@ public struct LoginFeature { /// 네비게이션 여부 설정 case setNavigating(RoutingScreen) /// 소셜 로그인 post 요청 - case postSocialLogin(entity: PostSocailEntity) + case postSocialLogin(entity: PostSocialEntity) /// 소셜 로그인 실패 case socialLoginFail From dcd11f0607e4a1ee382275b7cf68fe48bfc26fc0 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Sat, 8 Feb 2025 04:26:52 +0900 Subject: [PATCH 2/9] =?UTF-8?q?[Feat]=20=EC=95=B1=20=EC=B4=88=EA=B8=B0=20?= =?UTF-8?q?=EC=A7=84=EC=9E=85=20-=20Login/Term=20-=20UserTypeSelection=20?= =?UTF-8?q?=ED=9D=90=EB=A6=84=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Network/Service/User/UserTargetType.swift | 5 +- .../AppFlow/AppFlowCoordinatorFeature.swift | 32 +++++++- .../OnboardingFlowFeature.swift | 11 ++- .../OnboardingFlow/OnboardingFlowView.swift | 2 - .../Sources/Extension/Color+.swift | 38 ++++++++++ .../Onboarding/Common/LoginFeature.swift | 73 +++++++++++++------ .../Sources/Onboarding/Common/LoginView.swift | 24 +++--- .../Sources/Onboarding/Common/TermView.swift | 10 ++- .../UserTypeSelectionView.swift | 42 ++++------- TnT/Projects/TnTApp/Sources/TnTApp.swift | 10 ++- 10 files changed, 168 insertions(+), 79 deletions(-) create mode 100644 TnT/Projects/Presentation/Sources/Extension/Color+.swift diff --git a/TnT/Projects/Data/Sources/Network/Service/User/UserTargetType.swift b/TnT/Projects/Data/Sources/Network/Service/User/UserTargetType.swift index 84b39c06..0c7bfe8c 100644 --- a/TnT/Projects/Data/Sources/Network/Service/User/UserTargetType.swift +++ b/TnT/Projects/Data/Sources/Network/Service/User/UserTargetType.swift @@ -63,10 +63,7 @@ extension UserTargetType: TargetType { return ["Content-Type": "application/json"] case .postSignUp: - return [ - "Content-Type": "multipart/form-data", - "Authorization": "SESSION-ID 1111" - ] + return ["Content-Type": "multipart/form-data"] } } diff --git a/TnT/Projects/Presentation/Sources/Coordinator/AppFlow/AppFlowCoordinatorFeature.swift b/TnT/Projects/Presentation/Sources/Coordinator/AppFlow/AppFlowCoordinatorFeature.swift index ea16a854..62231824 100644 --- a/TnT/Projects/Presentation/Sources/Coordinator/AppFlow/AppFlowCoordinatorFeature.swift +++ b/TnT/Projects/Presentation/Sources/Coordinator/AppFlow/AppFlowCoordinatorFeature.swift @@ -11,6 +11,12 @@ import ComposableArchitecture import Domain +public enum AppFlow: Sendable { + case onboardingFlow + case traineeMainFlow + case trainerMainFlow +} + @Reducer public struct AppFlowCoordinatorFeature { @ObservableState @@ -58,14 +64,17 @@ public struct AppFlowCoordinatorFeature { switch action { case .subFeature(let internalAction): switch internalAction { - case .onboardingFlow: - return .none + case .onboardingFlow(.switchFlow(let flow)): + return self.setFlow(flow, state: &state) case .trainerMainFlow: return .none case .traineeMainFlow: return .none + + default: + return .none } case .onAppear: @@ -77,3 +86,22 @@ public struct AppFlowCoordinatorFeature { .ifLet(\.traineeMainState, action: \.subFeature.traineeMainFlow) { TraineeMainFlowFeature() } } } + +extension AppFlowCoordinatorFeature { + private func setFlow(_ flow: AppFlow, state: inout State) -> Effect { + state.onboardingState = nil + state.traineeMainState = nil + state.trainerMainState = nil + + switch flow { + case .onboardingFlow: + state.onboardingState = .init() + case .traineeMainFlow: + state.traineeMainState = .init() + case .trainerMainFlow: + state.trainerMainState = .init() + } + + return .none + } +} diff --git a/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift b/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift index 76354b00..5faee873 100644 --- a/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift +++ b/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift @@ -25,6 +25,8 @@ public struct OnboardingFlowFeature { public enum Action: Sendable { /// 현재 표시되고 있는 path 화면 내부에서 일어나는 액션을 처리합니다. case path(StackActionOf) + /// Flow 변경을 AppCoordinator로 전달합니다 + case switchFlow(AppFlow) case onAppear } @@ -42,8 +44,8 @@ public struct OnboardingFlowFeature { return .send(.switchFlow(.traineeMainFlow)) case .trainerHome: return .send(.switchFlow(.trainerMainFlow)) - case .term: - state.path.append(.term(.init())) + case .userTypeSelection: + state.path.append(.userTypeSelection(.init())) } return .none @@ -66,6 +68,9 @@ public struct OnboardingFlowFeature { return .none } + case .switchFlow: + return .none + case .onAppear: return .none } @@ -80,8 +85,6 @@ extension OnboardingFlowFeature { // MARK: Common /// SNS 로그인 뷰 case snsLogin(LoginFeature) - /// 약관동의뷰 - case term(TermFeature) /// 트레이너/트레이니 선택 뷰 case userTypeSelection(UserTypeSelectionFeature) /// 트레이너/트레이니의 이름 입력 뷰 diff --git a/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowView.swift b/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowView.swift index aa98e0ad..ad78d844 100644 --- a/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowView.swift +++ b/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowView.swift @@ -26,8 +26,6 @@ public struct OnboardingFlowView: View { // 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): diff --git a/TnT/Projects/Presentation/Sources/Extension/Color+.swift b/TnT/Projects/Presentation/Sources/Extension/Color+.swift new file mode 100644 index 00000000..ee988003 --- /dev/null +++ b/TnT/Projects/Presentation/Sources/Extension/Color+.swift @@ -0,0 +1,38 @@ +// +// Color+.swift +// Presentation +// +// Created by 박민서 on 2/8/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import SwiftUI + +public extension Color { + /// 헥스 문자열(#RRGGBB 또는 #RRGGBBAA)을 사용하여 Color를 초기화합니다. + init(hex: String) { + let hex = hex.trimmingCharacters(in: .whitespacesAndNewlines) + let hexString = hex.hasPrefix("#") ? String(hex.dropFirst()) : hex + var rgbValue: UInt64 = 0 + Scanner(string: hexString).scanHexInt64(&rgbValue) + + let r, g, b, a: Double + + switch hexString.count { + case 8: // RRGGBBAA + r = Double((rgbValue & 0xFF000000) >> 24) / 255.0 + g = Double((rgbValue & 0x00FF0000) >> 16) / 255.0 + b = Double((rgbValue & 0x0000FF00) >> 8) / 255.0 + a = Double(rgbValue & 0x000000FF) / 255.0 + case 6: // RRGGBB + r = Double((rgbValue & 0xFF0000) >> 16) / 255.0 + g = Double((rgbValue & 0x00FF00) >> 8) / 255.0 + b = Double(rgbValue & 0x0000FF) / 255.0 + a = 1.0 + default: + r = 1.0; g = 1.0; b = 1.0; a = 1.0 + } + + self.init(.sRGB, red: r, green: g, blue: b, opacity: a) + } +} diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Common/LoginFeature.swift b/TnT/Projects/Presentation/Sources/Onboarding/Common/LoginFeature.swift index 813a5440..e974b189 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Common/LoginFeature.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Common/LoginFeature.swift @@ -22,6 +22,7 @@ public struct LoginFeature { public var termAgree: Bool public var socialEmail: String? public var postUserEntity: PostSocialEntity? + @Presents var termFeature: TermFeature.State? public init( userType: UserType? = nil, @@ -47,18 +48,28 @@ public struct LoginFeature { public enum Action: ViewAction { /// 뷰에서 일어나는 액션을 처리합니다.(카카오,애플로그인 실행) case view(View) + /// 하위 화면에서 일어나는 액션을 처리합니다 + case subFeature(SubFeatureAction) /// 네비게이션 여부 설정 case setNavigating(RoutingScreen) /// 소셜 로그인 post 요청 case postSocialLogin(entity: PostSocialEntity) /// 소셜 로그인 실패 case socialLoginFail + /// 약관 동의 화면 표시 + case showTermView @CasePathable public enum View: Equatable { case tappedAppleLogin case tappedKakaoLogin } + + @CasePathable + public enum SubFeatureAction: Equatable { + /// 역관 동의 화면에서 발생하는 액션 처리 + case termAction(PresentationAction) + } } public init() {} @@ -72,11 +83,10 @@ public struct LoginFeature { return .run { @Sendable send in let result = await socialLoginUseCase.appleLogin() guard let result else { return } - let entity = PostSocailEntity( - socialType: "APPLE", - fcmToken: "", - socialAccessToken: "", - authorizationCode: result.authorizationCode, + + let entity = PostSocialEntity( + socialType: .apple, + fcmToken: "asdfg", // TODO: FCM 로직 나오면 추후 수정 idToken: result.identityToken ) @@ -88,34 +98,42 @@ public struct LoginFeature { let result = await socialLoginUseCase.kakaoLogin() guard let result else { return } - let entity = PostSocailEntity( - socialType: "KAKAO", - fcmToken: "", - socialAccessToken: result.accessToken, - authorizationCode: "", - idToken: "" + let entity = PostSocialEntity( + socialType: .kakao, + fcmToken: "asdfg", // TODO: FCM 로직 나오면 추후 수정 + socialAccessToken: result.accessToken ) await send(.postSocialLogin(entity: entity)) } } + case .subFeature(.termAction(.presented(.setNavigating))): + state.termFeature = nil + return .send(.setNavigating(.userTypeSelection)) + + case .subFeature(.termAction(.dismiss)): + state.termFeature = nil + return .none + + case .subFeature: + return .none + case .postSocialLogin(let entity): - let post = PostSocialMapper.toDTO(from: entity) + let post = entity.toDTO() return .run { send in do { let result = try await userUseCaseRepo.postSocialLogin(post) - // TODO: res에 MemberType 추가해주세요! -// switch result.memberType { -// case .trainer: -// await send(.setNavigating(.trainerHome)) -// case .trainee: -// await send(.setNavigating(.traineeHome)) -// case .unregistered: -// await send(.setNavigating(.term)) -// } - // For test - await send(.setNavigating(.term)) + switch result.memberType { + case .trainer: + await send(.setNavigating(.trainerHome)) + case .trainee: + await send(.setNavigating(.traineeHome)) + case .unregistered: + await send(.showTermView) + case .unknown: + print("unknown 타입이에요 토스트해줏요") + } } catch { await send(.socialLoginFail) } @@ -125,10 +143,17 @@ public struct LoginFeature { print("네트워크 에러 발생") return .none + case .showTermView: + state.termFeature = .init() + return .none + case .setNavigating: return .none } } + .ifLet(\.termFeature, action: \.subFeature.termAction.presented) { + TermFeature() + } } } @@ -137,6 +162,6 @@ extension LoginFeature { public enum RoutingScreen { case traineeHome case trainerHome - case term + case userTypeSelection } } diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Common/LoginView.swift b/TnT/Projects/Presentation/Sources/Onboarding/Common/LoginView.swift index faf51e30..8fe07c64 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Common/LoginView.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Common/LoginView.swift @@ -23,20 +23,24 @@ public struct LoginView: View { VStack { Header() - Spacer() - - Image(.imgOnboardingLogin) - .resizable() - .scaledToFit() - .frame(width: 310, height: 310) + VStack { + Spacer() + Image(.imgOnboardingLogin) + .resizable() + .scaledToFit() + .frame(width: 310, height: 310) + Spacer() + } Bottom() - - Spacer() - } .padding(.horizontal, 28) .navigationBarBackButtonHidden() + .sheet(item: $store.scope(state: \.termFeature, action: \.subFeature.termAction)) { store in + TermView(store: store) + .padding(.top, 10) + .presentationDetents([.height(512)]) + } } @ViewBuilder @@ -106,7 +110,7 @@ public enum LoginType: String, CaseIterable { var background: Color { switch self { case .kakao: - return .yellow + return Color(hex: "FDE500") case .apple: return .neutral900 } diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Common/TermView.swift b/TnT/Projects/Presentation/Sources/Onboarding/Common/TermView.swift index ea24ce83..f90a7439 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Common/TermView.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Common/TermView.swift @@ -29,12 +29,16 @@ public struct TermView: View { .padding(.horizontal, 20) Spacer() - - TBottomButton(title: "다음", isEnable: store.view_isAllAgreed) { + } + .navigationBarBackButtonHidden() + .safeAreaInset(edge: .bottom) { + TBottomButton( + title: "다음", + isEnable: store.view_isAllAgreed + ) { send(.nextButtonTapped) } } - .navigationBarBackButtonHidden() } @ViewBuilder diff --git a/TnT/Projects/Presentation/Sources/Onboarding/UserTypeSelection/UserTypeSelectionView.swift b/TnT/Projects/Presentation/Sources/Onboarding/UserTypeSelection/UserTypeSelectionView.swift index b06885ae..d5bbc791 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/UserTypeSelection/UserTypeSelectionView.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/UserTypeSelection/UserTypeSelectionView.swift @@ -25,35 +25,25 @@ public struct UserTypeSelectionView: View { } public var body: some View { - NavigationStack { - VStack(spacing: 0) { - VStack(spacing: 48) { - Header() - - ImageSection() - - ButtonSection() - } - .padding(.top, 60) + VStack(spacing: 0) { + VStack(spacing: 48) { + Header() - Spacer() - } - .safeAreaInset(edge: .bottom) { - TBottomButton( - title: "다음", - isEnable: true - ) { - send(.tapNextButton) - } + ImageSection() + + ButtonSection() } - .ignoresSafeArea(.container, edges: .bottom) - .navigationDestination( - isPresented: $store.view_isNavigating + .padding(.top, 60) + + Spacer() + } + .navigationBarBackButtonHidden() + .safeAreaInset(edge: .bottom) { + TBottomButton( + title: "다음", + isEnable: true ) { - CreateProfileView( - store: .init(initialState: CreateProfileFeature.State(userType: .trainee)) { - CreateProfileFeature() - }) + send(.tapNextButton) } } } diff --git a/TnT/Projects/TnTApp/Sources/TnTApp.swift b/TnT/Projects/TnTApp/Sources/TnTApp.swift index 4f0825b3..249677b6 100644 --- a/TnT/Projects/TnTApp/Sources/TnTApp.swift +++ b/TnT/Projects/TnTApp/Sources/TnTApp.swift @@ -21,10 +21,12 @@ struct ToyProjectApp: App { var body: some Scene { WindowGroup { - OnboardingView(store: Store(initialState: OnboardingFeature.State(), reducer: { - OnboardingFeature() - })) -// ContentView() + AppFlowCoordinatorView( + store: .init( + initialState: AppFlowCoordinatorFeature.State(), + reducer: { AppFlowCoordinatorFeature() + }) + ) } } } From 18223dadb70142c48fd6af3a62c44b6056818920 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Sat, 8 Feb 2025 04:36:37 +0900 Subject: [PATCH 3/9] =?UTF-8?q?[Feat]=20UserTypeSelection=20->=20Trainee/T?= =?UTF-8?q?rainer=20CreateProfile=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OnboardingFlowFeature.swift | 13 +++++++++-- .../Sources/Onboarding/Common/LoginView.swift | 1 + .../UserTypeSelectionFeature.swift | 23 +++++++++++++++++-- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift b/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift index 5faee873..8f89cd8b 100644 --- a/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift +++ b/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift @@ -37,7 +37,7 @@ public struct OnboardingFlowFeature { switch action { case let .path(action): switch action { - + /// SNS 로그인 화면 로그인 완료 -> 트레이너/트레이니/회원가입 case .element(id: _, action: .snsLogin(.setNavigating(let screen))): switch screen { case .traineeHome: @@ -49,7 +49,16 @@ public struct OnboardingFlowFeature { } return .none - + + case .element(id: _, action: .userTypeSelection(.setNavigating(let screen))): + switch screen { + case .createProfileTrainee: + state.path.append(.createProfile(.init(userType: .trainee))) + case .createProfileTrainer: + state.path.append(.createProfile(.init(userType: .trainer))) + } + return .none + /// 트레이너 프로필 생성 완료 -> 다음 버튼 tapped case .element(id: _, action: .trainerSignUpComplete(.setNavigating)): state.path.append(.trainerMakeInvitationCode(MakeInvitationCodeFeature.State())) diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Common/LoginView.swift b/TnT/Projects/Presentation/Sources/Onboarding/Common/LoginView.swift index 8fe07c64..2a3b3f56 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Common/LoginView.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Common/LoginView.swift @@ -40,6 +40,7 @@ public struct LoginView: View { TermView(store: store) .padding(.top, 10) .presentationDetents([.height(512)]) + .presentationDragIndicator(.visible) } } diff --git a/TnT/Projects/Presentation/Sources/Onboarding/UserTypeSelection/UserTypeSelectionFeature.swift b/TnT/Projects/Presentation/Sources/Onboarding/UserTypeSelection/UserTypeSelectionFeature.swift index 78a4e18c..8b9d7eb9 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/UserTypeSelection/UserTypeSelectionFeature.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/UserTypeSelection/UserTypeSelectionFeature.swift @@ -37,6 +37,8 @@ public struct UserTypeSelectionFeature { public enum Action: Sendable, ViewAction { /// 뷰에서 발생한 액션을 처리합니다. case view(ViewAction) + /// 네비게이션 설정 + case setNavigating(RoutingScreen) public enum ViewAction: Sendable, BindableAction { /// 바인딩할 액션을 처리합니다 @@ -65,10 +67,27 @@ public struct UserTypeSelectionFeature { return .none case .tapNextButton: - state.view_isNavigating = true - return .none + switch state.userType { + case .trainer: + return .send(.setNavigating(.createProfileTrainer)) + case .trainee: + return .send(.setNavigating(.createProfileTrainee)) + } } + + case .setNavigating: + return .none } } } } + +extension UserTypeSelectionFeature { + /// 하위 화면에서 파생되는 라우팅을 전달합니다 + public enum RoutingScreen: Sendable { + /// 트레이니 회원가입 + case createProfileTrainee + /// 트레이너 회원가입 + case createProfileTrainer + } +} From 0b04aee975cd2ec563f2da5c238ef25befea7978 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Sat, 8 Feb 2025 06:11:50 +0900 Subject: [PATCH 4/9] =?UTF-8?q?[Feat]=20OnboardingFlow=20Shared=20PostSign?= =?UTF-8?q?UpEntity=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/DTO/User/UserRequestDTO.swift | 4 -- .../Sources/Entity/SocailLoginEntity.swift | 62 +++++++++---------- .../OnboardingFlowFeature.swift | 10 ++- .../Onboarding/Common/LoginFeature.swift | 33 +++++----- .../UserTypeSelectionFeature.swift | 10 ++- 5 files changed, 63 insertions(+), 56 deletions(-) diff --git a/TnT/Projects/Domain/Sources/DTO/User/UserRequestDTO.swift b/TnT/Projects/Domain/Sources/DTO/User/UserRequestDTO.swift index 61435a70..29c809a7 100644 --- a/TnT/Projects/Domain/Sources/DTO/User/UserRequestDTO.swift +++ b/TnT/Projects/Domain/Sources/DTO/User/UserRequestDTO.swift @@ -50,8 +50,6 @@ public struct PostSignUpReqDTO: Encodable { let collectionAgreement: Bool /// 광고성 알림 수신 동의 여부 let advertisementAgreement: Bool - /// 푸시 알림 수신 동의 여부 - let pushAgreement: Bool /// 회원 이름 let name: String /// 생년월일 (yyyy-MM-dd) @@ -74,7 +72,6 @@ public struct PostSignUpReqDTO: Encodable { serviceAgreement: Bool, collectionAgreement: Bool, advertisementAgreement: Bool, - pushAgreement: Bool, name: String, birthday: String?, height: Double?, @@ -90,7 +87,6 @@ public struct PostSignUpReqDTO: Encodable { self.serviceAgreement = serviceAgreement self.collectionAgreement = collectionAgreement self.advertisementAgreement = advertisementAgreement - self.pushAgreement = pushAgreement self.name = name self.birthday = birthday self.height = height diff --git a/TnT/Projects/Domain/Sources/Entity/SocailLoginEntity.swift b/TnT/Projects/Domain/Sources/Entity/SocailLoginEntity.swift index 2467babe..19695722 100644 --- a/TnT/Projects/Domain/Sources/Entity/SocailLoginEntity.swift +++ b/TnT/Projects/Domain/Sources/Entity/SocailLoginEntity.swift @@ -52,54 +52,51 @@ public struct PostSocialLoginResEntity: Equatable { } /// 회원가입 요청 DTO -public struct PostSignUpReqEntity { +public struct PostSignUpEntity: Equatable { /// FCM 토큰 - let fcmToken: String + public var fcmToken: String? /// 회원 타입 (trainer, trainee) - let memberType: String + public var memberType: UserType? /// 소셜 로그인 타입 (KAKAO, APPLE) - let socialType: String + public var socialType: SocialType? /// 소셜 로그인 ID - let socialId: String + public var socialId: String? /// 소셜 로그인 이메일 - let socialEmail: String + public var socialEmail: String? /// 서비스 이용 약관 동의 여부 - let serviceAgreement: Bool + public var serviceAgreement: Bool /// 개인정보 수집 동의 여부 - let collectionAgreement: Bool + public var collectionAgreement: Bool /// 광고성 알림 수신 동의 여부 - let advertisementAgreement: Bool - /// 푸시 알림 수신 동의 여부 - let pushAgreement: Bool + public var advertisementAgreement: Bool /// 회원 이름 - let name: String + public var name: String? /// 생년월일 (yyyy-MM-dd) - let birthday: String? + public var birthday: String? /// 키 (cm) - let height: Double? + public var height: Double? /// 몸무게 (kg, 소수점 1자리까지 가능) - let weight: Double? + public var weight: Double? /// 트레이너에게 전달할 주의사항 - let cautionNote: String? + public var cautionNote: String? /// PT 목적 (체중 감량, 근력 향상 등) - let goalContents: [String]? + public var goalContents: [String]? public init( - fcmToken: String, - memberType: String, - socialType: String, - socialId: String, - socialEmail: String, - serviceAgreement: Bool, - collectionAgreement: Bool, - advertisementAgreement: Bool, - pushAgreement: Bool, - name: String, - birthday: String?, - height: Double?, - weight: Double?, - cautionNote: String?, - goalContents: [String]? + fcmToken: String? = nil, + memberType: UserType? = nil, + socialType: SocialType? = nil, + socialId: String? = nil, + socialEmail: String? = nil, + serviceAgreement: Bool = false, + collectionAgreement: Bool = false, + advertisementAgreement: Bool = false, + name: String? = nil, + birthday: String? = nil, + height: Double? = nil, + weight: Double? = nil, + cautionNote: String? = nil, + goalContents: [String]? = nil ) { self.fcmToken = fcmToken self.memberType = memberType @@ -109,7 +106,6 @@ public struct PostSignUpReqEntity { self.serviceAgreement = serviceAgreement self.collectionAgreement = collectionAgreement self.advertisementAgreement = advertisementAgreement - self.pushAgreement = pushAgreement self.name = name self.birthday = birthday self.height = height diff --git a/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift b/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift index 8f89cd8b..3a3f554a 100644 --- a/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift +++ b/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift @@ -16,9 +16,15 @@ public struct OnboardingFlowFeature { @ObservableState public struct State: Equatable { public var path: StackState + @Shared var signUpEntity: PostSignUpEntity - public init(path: StackState = .init([.snsLogin(.init())])) { - self.path = path + public init( + signUpEntity: PostSignUpEntity = .init(), + path: StackState? = nil + ) { + let shared = Shared(value: signUpEntity) + self._signUpEntity = shared + self.path = path ?? .init([.snsLogin(.init(signUpEntity: shared))]) } } diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Common/LoginFeature.swift b/TnT/Projects/Presentation/Sources/Onboarding/Common/LoginFeature.swift index e974b189..6c3ae385 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Common/LoginFeature.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Common/LoginFeature.swift @@ -16,27 +16,15 @@ import DIContainer public struct LoginFeature { @ObservableState public struct State: Equatable { - public var userType: UserType? - public var nickname: String? - public var socialType: LoginType? - public var termAgree: Bool - public var socialEmail: String? + @Shared var signUpEntity: PostSignUpEntity public var postUserEntity: PostSocialEntity? @Presents var termFeature: TermFeature.State? public init( - userType: UserType? = nil, - nickname: String? = nil, - socialType: LoginType? = nil, - termAgree: Bool = false, - socialEmail: String? = nil, + signUpEntity: Shared, postUserEntity: PostSocialEntity? = nil ) { - self.userType = userType - self.nickname = nickname - self.socialType = socialType - self.termAgree = termAgree - self.socialEmail = socialEmail + self._signUpEntity = signUpEntity self.postUserEntity = postUserEntity } } @@ -56,6 +44,8 @@ public struct LoginFeature { case postSocialLogin(entity: PostSocialEntity) /// 소셜 로그인 실패 case socialLoginFail + /// signUpEntity를 소셜로그인 정보로 업데이트 + case updateSignUpEntityWithSocialInfo(res: PostSocialLoginResDTO) /// 약관 동의 화면 표시 case showTermView @@ -110,6 +100,9 @@ public struct LoginFeature { case .subFeature(.termAction(.presented(.setNavigating))): state.termFeature = nil + state.$signUpEntity.withLock { $0.collectionAgreement = true } + state.$signUpEntity.withLock { $0.serviceAgreement = true } + state.$signUpEntity.withLock { $0.advertisementAgreement = true } return .send(.setNavigating(.userTypeSelection)) case .subFeature(.termAction(.dismiss)): @@ -124,13 +117,14 @@ public struct LoginFeature { return .run { send in do { let result = try await userUseCaseRepo.postSocialLogin(post) + switch result.memberType { case .trainer: await send(.setNavigating(.trainerHome)) case .trainee: await send(.setNavigating(.traineeHome)) case .unregistered: - await send(.showTermView) + await send(.updateSignUpEntityWithSocialInfo(res: result)) case .unknown: print("unknown 타입이에요 토스트해줏요") } @@ -143,6 +137,13 @@ public struct LoginFeature { print("네트워크 에러 발생") return .none + case .updateSignUpEntityWithSocialInfo(let res): + guard let socialType = SocialType(rawValue: res.socialType ?? "") else { return .none } + state.$signUpEntity.withLock { $0.socialId = res.socialId } + state.$signUpEntity.withLock { $0.socialEmail = res.socialEmail } + state.$signUpEntity.withLock { $0.socialType = socialType } + return .send(.showTermView) + case .showTermView: state.termFeature = .init() return .none diff --git a/TnT/Projects/Presentation/Sources/Onboarding/UserTypeSelection/UserTypeSelectionFeature.swift b/TnT/Projects/Presentation/Sources/Onboarding/UserTypeSelection/UserTypeSelectionFeature.swift index 8b9d7eb9..4c5d4f8d 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/UserTypeSelection/UserTypeSelectionFeature.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/UserTypeSelection/UserTypeSelectionFeature.swift @@ -17,6 +17,8 @@ public struct UserTypeSelectionFeature { @ObservableState public struct State: Equatable { // MARK: Data related state + /// 현재 회원가입 정보 + @Shared var signUpEntity: PostSignUpEntity /// 현재 선택된 유저 타입 (트레이너/트레이니) var userType: UserType @@ -28,9 +30,14 @@ public struct UserTypeSelectionFeature { /// - Parameters: /// - userType: 현재 선택된 유저 타입 (기본값: `.trainer`) /// - isNavigating: 네비게이션 여부 (기본값: `false`) - public init(userType: UserType = .trainer, view_isNavigating: Bool = false) { + public init( + userType: UserType = .trainer, + view_isNavigating: Bool = false, + signUpEntity: Shared + ) { self.userType = userType self.view_isNavigating = view_isNavigating + self._signUpEntity = signUpEntity } } @@ -67,6 +74,7 @@ public struct UserTypeSelectionFeature { return .none case .tapNextButton: + state.$signUpEntity.withLock { $0.memberType = state.userType } switch state.userType { case .trainer: return .send(.setNavigating(.createProfileTrainer)) From 076ecada481246ec43f59194306de21ad4fe2d3a Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Sat, 8 Feb 2025 06:36:43 +0900 Subject: [PATCH 5/9] =?UTF-8?q?[Feat]=20CreateProfile=20->=20=EB=B6=84?= =?UTF-8?q?=EA=B8=B0=EC=B2=98=EB=A6=AC=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Entity/SocailLoginEntity.swift | 4 +++ .../AppFlow/AppFlowCoordinatorFeature.swift | 2 +- .../OnboardingFlowFeature.swift | 27 +++++++++++------ .../CreateProfile/CreateProfileFeature.swift | 30 ++++++++++++++++--- .../CreateProfile/CreateProfileView.swift | 1 + .../UserTypeSelectionFeature.swift | 3 +- 6 files changed, 52 insertions(+), 15 deletions(-) diff --git a/TnT/Projects/Domain/Sources/Entity/SocailLoginEntity.swift b/TnT/Projects/Domain/Sources/Entity/SocailLoginEntity.swift index 19695722..22300e78 100644 --- a/TnT/Projects/Domain/Sources/Entity/SocailLoginEntity.swift +++ b/TnT/Projects/Domain/Sources/Entity/SocailLoginEntity.swift @@ -71,6 +71,8 @@ public struct PostSignUpEntity: Equatable { public var advertisementAgreement: Bool /// 회원 이름 public var name: String? + /// 프로필 이미지 데이터 + public var imageData: Data? /// 생년월일 (yyyy-MM-dd) public var birthday: String? /// 키 (cm) @@ -92,6 +94,7 @@ public struct PostSignUpEntity: Equatable { collectionAgreement: Bool = false, advertisementAgreement: Bool = false, name: String? = nil, + imageData: Data? = nil, birthday: String? = nil, height: Double? = nil, weight: Double? = nil, @@ -107,6 +110,7 @@ public struct PostSignUpEntity: Equatable { self.collectionAgreement = collectionAgreement self.advertisementAgreement = advertisementAgreement self.name = name + self.imageData = imageData self.birthday = birthday self.height = height self.weight = weight diff --git a/TnT/Projects/Presentation/Sources/Coordinator/AppFlow/AppFlowCoordinatorFeature.swift b/TnT/Projects/Presentation/Sources/Coordinator/AppFlow/AppFlowCoordinatorFeature.swift index 62231824..5a9a066b 100644 --- a/TnT/Projects/Presentation/Sources/Coordinator/AppFlow/AppFlowCoordinatorFeature.swift +++ b/TnT/Projects/Presentation/Sources/Coordinator/AppFlow/AppFlowCoordinatorFeature.swift @@ -81,7 +81,7 @@ public struct AppFlowCoordinatorFeature { return .none } } - .ifLet(\.onboardingState, action: \.subFeature.onboardingFlow) { OnboardingFlowFeature() } + .ifLet(\.onboardingState, action: \.subFeature.onboardingFlow) { OnboardingFlowFeature()._printChanges() } .ifLet(\.trainerMainState, action: \.subFeature.trainerMainFlow) { TrainerMainFlowFeature() } .ifLet(\.traineeMainState, action: \.subFeature.traineeMainFlow) { TraineeMainFlowFeature() } } diff --git a/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift b/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift index 3a3f554a..03aa639e 100644 --- a/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift +++ b/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift @@ -43,7 +43,7 @@ public struct OnboardingFlowFeature { switch action { case let .path(action): switch action { - /// SNS 로그인 화면 로그인 완료 -> 트레이너/트레이니/회원가입 + /// SNS 로그인 화면 로그인 완료 -> 트레이너/트레이니/회원가입 case .element(id: _, action: .snsLogin(.setNavigating(let screen))): switch screen { case .traineeHome: @@ -51,18 +51,31 @@ public struct OnboardingFlowFeature { case .trainerHome: return .send(.switchFlow(.trainerMainFlow)) case .userTypeSelection: - state.path.append(.userTypeSelection(.init())) + state.path.append(.userTypeSelection(.init(signUpEntity: state.$signUpEntity))) } return .none - + + /// 유저 타입 선택 완료 -> 트레이니/트레이너 프로필 입력 case .element(id: _, action: .userTypeSelection(.setNavigating(let screen))): switch screen { case .createProfileTrainee: - state.path.append(.createProfile(.init(userType: .trainee))) + state.path.append(.createProfile(.init(signUpEntity: state.$signUpEntity, userType: .trainee))) case .createProfileTrainer: - state.path.append(.createProfile(.init(userType: .trainer))) + state.path.append(.createProfile(.init(signUpEntity: state.$signUpEntity, userType: .trainer))) + } + + return .none + + /// 약관 화면 -> 트레이너/트레이니 선택 화면 이동 + case .element(id: _, action: .createProfile(.setNavigating(let screen))): + switch screen { + case .traineeBasicInfoInput: + state.path.append(.traineeBasicInfoInput(.init())) + case .trainerSignUpComplete: + state.path.append(.trainerSignUpComplete(.init())) } + return .none /// 트레이너 프로필 생성 완료 -> 다음 버튼 tapped @@ -75,10 +88,6 @@ public struct OnboardingFlowFeature { // 추후에 홈과 연결 return .none - /// 약관 화면 -> 트레이너/트레이니 선택 화면 이동 - case .element(id: _, action: .userTypeSelection): - return .none - default: return .none } diff --git a/TnT/Projects/Presentation/Sources/Onboarding/CreateProfile/CreateProfileFeature.swift b/TnT/Projects/Presentation/Sources/Onboarding/CreateProfile/CreateProfileFeature.swift index 95d78a46..c783bf3e 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/CreateProfile/CreateProfileFeature.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/CreateProfile/CreateProfileFeature.swift @@ -20,6 +20,8 @@ public struct CreateProfileFeature { @ObservableState public struct State: Equatable { // MARK: Data related state + /// 현재 회원가입 정보 + @Shared var signUpEntity: PostSignUpEntity /// 현재 선택된 유저 타입 (트레이너/트레이니) var userType: UserType /// 현재 입력된 사용자 이름 @@ -49,6 +51,7 @@ public struct CreateProfileFeature { /// `CreateProfileFeature.State`의 생성자 /// - Parameters: + /// - signUpEntity: 현재 회원가입 정보 @Shared /// - userType: 현재 선택된 유저 타입 (기본값: `.trainer`) /// - userName: 입력된 유저 이름 (기본값: 공백) /// - userImageData: 선택된 이미지 데이터 (기본값: `nil`) @@ -61,6 +64,7 @@ public struct CreateProfileFeature { /// - view_photoPickerItem: 현재 선택된 이미지 아이템 (기본값: `nil`) /// - view_nameMaxLength:유저의 최대 이름 길이 (기본값: `nil`) public init( + signUpEntity: Shared, userType: UserType, userImageData: Data? = nil, userName: String = "", @@ -72,6 +76,7 @@ public struct CreateProfileFeature { view_photoPickerItem: PhotosPickerItem? = nil, view_nameMaxLength: Int? = nil ) { + self._signUpEntity = signUpEntity self.userType = userType self.userImageData = userImageData self.userName = userName @@ -89,7 +94,7 @@ public struct CreateProfileFeature { public enum Action: Sendable, ViewAction { /// 네비게이션 여부 설정 - case setNavigating(Bool) + case setNavigating(RoutingScreen) /// 선택된 이미지 데이터 저장 case imagePicked(Data?) /// 뷰에서 발생한 액션을 처리합니다. @@ -134,11 +139,17 @@ public struct CreateProfileFeature { return .none case .tapNextButton: - return .send(.setNavigating(true)) + state.$signUpEntity.withLock { $0.name = state.userName } + state.$signUpEntity.withLock { $0.imageData = state.userImageData } + switch state.userType { + case .trainee: + return .send(.setNavigating(.traineeBasicInfoInput)) + case .trainer: + return .send(.setNavigating(.trainerSignUpComplete)) + } } - case .setNavigating(let isNavigating): - state.view_isNavigating = isNavigating + case .setNavigating: return .none case .imagePicked(let imgData): @@ -164,3 +175,14 @@ private extension CreateProfileFeature { return .none } } + +extension CreateProfileFeature { + /// 본 화면에서 라우팅(파생)되는 화면 + public enum RoutingScreen: Sendable { + /// 트레이니 회원가입 + case traineeBasicInfoInput + /// 트레이너 프로필 생성 완료 + case trainerSignUpComplete + + } +} diff --git a/TnT/Projects/Presentation/Sources/Onboarding/CreateProfile/CreateProfileView.swift b/TnT/Projects/Presentation/Sources/Onboarding/CreateProfile/CreateProfileView.swift index bb3d977c..d4a950c9 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/CreateProfile/CreateProfileView.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/CreateProfile/CreateProfileView.swift @@ -43,6 +43,7 @@ public struct CreateProfileView: View { Spacer() } + .navigationBarBackButtonHidden() .keyboardDismissOnTap() .safeAreaInset(edge: .bottom) { TBottomButton( diff --git a/TnT/Projects/Presentation/Sources/Onboarding/UserTypeSelection/UserTypeSelectionFeature.swift b/TnT/Projects/Presentation/Sources/Onboarding/UserTypeSelection/UserTypeSelectionFeature.swift index 4c5d4f8d..ff8d3701 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/UserTypeSelection/UserTypeSelectionFeature.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/UserTypeSelection/UserTypeSelectionFeature.swift @@ -30,6 +30,7 @@ public struct UserTypeSelectionFeature { /// - Parameters: /// - userType: 현재 선택된 유저 타입 (기본값: `.trainer`) /// - isNavigating: 네비게이션 여부 (기본값: `false`) + /// - signUpEntity: 현재 회원가입 정보 @Shared public init( userType: UserType = .trainer, view_isNavigating: Bool = false, @@ -91,7 +92,7 @@ public struct UserTypeSelectionFeature { } extension UserTypeSelectionFeature { - /// 하위 화면에서 파생되는 라우팅을 전달합니다 + /// 본 화면에서 라우팅(파생)되는 화면 public enum RoutingScreen: Sendable { /// 트레이니 회원가입 case createProfileTrainee From 0060e5b57f6330104f2a70301324d992afae4241 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Sat, 8 Feb 2025 06:49:44 +0900 Subject: [PATCH 6/9] =?UTF-8?q?[Feat]=20TraineeBasicInfoInput=20->=20Train?= =?UTF-8?q?ingPurpose=20=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 --- .../OnboardingFlow/OnboardingFlowFeature.swift | 14 +++++++++----- .../TraineeBasicInfoInputFeature.swift | 10 ++++++++++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift b/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift index 03aa639e..4c6beaf9 100644 --- a/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift +++ b/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift @@ -53,7 +53,6 @@ public struct OnboardingFlowFeature { case .userTypeSelection: state.path.append(.userTypeSelection(.init(signUpEntity: state.$signUpEntity))) } - return .none /// 유저 타입 선택 완료 -> 트레이니/트레이너 프로필 입력 @@ -64,20 +63,19 @@ public struct OnboardingFlowFeature { case .createProfileTrainer: state.path.append(.createProfile(.init(signUpEntity: state.$signUpEntity, userType: .trainer))) } - return .none /// 약관 화면 -> 트레이너/트레이니 선택 화면 이동 case .element(id: _, action: .createProfile(.setNavigating(let screen))): switch screen { case .traineeBasicInfoInput: - state.path.append(.traineeBasicInfoInput(.init())) + state.path.append(.traineeBasicInfoInput(.init(signUpEntity: state.$signUpEntity))) case .trainerSignUpComplete: state.path.append(.trainerSignUpComplete(.init())) } - return .none - + + // MARK: Trainer /// 트레이너 프로필 생성 완료 -> 다음 버튼 tapped case .element(id: _, action: .trainerSignUpComplete(.setNavigating)): state.path.append(.trainerMakeInvitationCode(MakeInvitationCodeFeature.State())) @@ -88,6 +86,12 @@ public struct OnboardingFlowFeature { // 추후에 홈과 연결 return .none + // MARK: Trainee + /// 트레이니 기본 정보 입력 -> PT 목적 설정 화면 이동 + case .element(id: _, action: .traineeBasicInfoInput(.setNavigating)): + state.path.append(.traineeTrainingPurpose(.init())) + return .none + default: return .none } diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeBasicInfoInput/TraineeBasicInfoInputFeature.swift b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeBasicInfoInput/TraineeBasicInfoInputFeature.swift index a26d8a4d..f0adda39 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeBasicInfoInput/TraineeBasicInfoInputFeature.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeBasicInfoInput/TraineeBasicInfoInputFeature.swift @@ -20,6 +20,8 @@ public struct TraineeBasicInfoInputFeature { @ObservableState public struct State: Equatable { // MARK: Data related state + /// 현재 회원가입 정보 + @Shared var signUpEntity: PostSignUpEntity /// 입력된 생년월일 var birthDate: String /// 입력된 키 @@ -41,6 +43,7 @@ public struct TraineeBasicInfoInputFeature { /// `TraineeBasicInfoInputFeature.State`의 생성자 /// - Parameters: + /// - signUpEntity: 현재 회원가입 정보 @Shared /// - birthDate: 입력된 생년월일 (기본값: `""`) /// - height: 입력된 키 (기본값: `""`) /// - weight: 입력된 몸무게 (기본값: `""`) @@ -50,6 +53,7 @@ public struct TraineeBasicInfoInputFeature { /// - view_isDatePickerPresented: DatePicker 표시 여부 (기본값: `false`) /// - view_isNextButtonEnabled: "다음" 버튼 활성화 여부 (기본값: `false`) public init( + signUpEntity: Shared, birthDate: String = "", height: String = "", weight: String = "", @@ -59,6 +63,7 @@ public struct TraineeBasicInfoInputFeature { view_isDatePickerPresented: Bool = false, view_isNextButtonEnabled: Bool = false ) { + self._signUpEntity = signUpEntity self.birthDate = birthDate self.height = height self.weight = weight @@ -122,6 +127,11 @@ public struct TraineeBasicInfoInputFeature { : .none case .tapNextButton: + if !state.birthDate.isEmpty { + state.$signUpEntity.withLock { $0.birthday = state.birthDate } + } + state.$signUpEntity.withLock { $0.height = Double(state.height) } + state.$signUpEntity.withLock { $0.weight = Double(state.weight) } return .send(.setNavigating) } From 9e8a2965d10ee93b55708a42978447e736597f6d Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Sat, 8 Feb 2025 07:03:50 +0900 Subject: [PATCH 7/9] =?UTF-8?q?[Feat]=20TrainingPurpose=20->=20Precaution?= =?UTF-8?q?=20->=20Completion=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OnboardingFlow/OnboardingFlowFeature.swift | 12 +++++++++++- .../TraineePrecautionInputFeature.swift | 6 ++++++ .../TraineeTrainingPurposeFeature.swift | 9 +++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift b/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift index 4c6beaf9..e3e82f11 100644 --- a/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift +++ b/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift @@ -89,7 +89,17 @@ public struct OnboardingFlowFeature { // MARK: Trainee /// 트레이니 기본 정보 입력 -> PT 목적 설정 화면 이동 case .element(id: _, action: .traineeBasicInfoInput(.setNavigating)): - state.path.append(.traineeTrainingPurpose(.init())) + state.path.append(.traineeTrainingPurpose(.init(signUpEntity: state.$signUpEntity))) + return .none + + /// 트레이니 PT 목표 입력 -> 주의사항 입력 화면 이동 + case .element(id: _, action: .traineeTrainingPurpose(.setNavigating)): + state.path.append(.traineePrecautionInput(.init(signUpEntity: state.$signUpEntity))) + return .none + + /// 트레이니 주의사항 입력 -> 트레이니 회원 가입 완료 화면 이동 + case .element(id: _, action: .traineePrecautionInput(.setNavigating)): + state.path.append(.traineeProfileCompletion(.init())) return .none default: diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineePrecautionInput/TraineePrecautionInputFeature.swift b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineePrecautionInput/TraineePrecautionInputFeature.swift index 3538d8ff..32825a05 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineePrecautionInput/TraineePrecautionInputFeature.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineePrecautionInput/TraineePrecautionInputFeature.swift @@ -20,6 +20,8 @@ public struct TraineePrecautionInputFeature { @ObservableState public struct State: Equatable { // MARK: Data related state + /// 현재 회원가입 정보 + @Shared var signUpEntity: PostSignUpEntity /// 사용자가 입력한 주의사항 var precaution: String @@ -35,18 +37,21 @@ public struct TraineePrecautionInputFeature { /// `TraineePrecautionInputFeature.State`의 생성자 /// - Parameters: + /// - signUpEntity: 현재 회원가입 정보 @Shared /// - precaution: 입력된 주의사항 (기본값: `""`) /// - view_editorStatus: 텍스트 에디터 상태 (기본값: `.empty`) /// - view_editorMaxCount: 텍스트 에디터 최대 길이 제한 (기본값: `nil`) /// - view_focusField: 텍스트 에디터 포커스 여부 (기본값: `false`) /// - view_isNextButtonEnabled: "다음" 버튼 활성화 여부 (기본값: `true`) public init( + signUpEntity: Shared, precaution: String = "", view_editorStatus: TTextEditor.Status = .empty, view_editorMaxCount: Int? = nil, view_focusField: Bool = false, view_isNextButtonEnabled: Bool = true ) { + self._signUpEntity = signUpEntity self.precaution = precaution self.view_editorStatus = view_editorStatus self.view_editorMaxCount = view_editorMaxCount @@ -94,6 +99,7 @@ public struct TraineePrecautionInputFeature { return .none case .tapNextButton: + state.$signUpEntity.withLock { $0.cautionNote = state.precaution } return .send(.setNavigating) } diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeTrainingPurpose/TraineeTrainingPurposeFeature.swift b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeTrainingPurpose/TraineeTrainingPurposeFeature.swift index 6012ce08..7c85f9cd 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeTrainingPurpose/TraineeTrainingPurposeFeature.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeTrainingPurpose/TraineeTrainingPurposeFeature.swift @@ -18,6 +18,8 @@ public struct TraineeTrainingPurposeFeature { @ObservableState public struct State: Equatable { // MARK: Data related state + /// 현재 회원가입 정보 + @Shared var signUpEntity: PostSignUpEntity /// 사용자가 선택한 트레이닝 목적 목록 (다중 선택 가능) var selectedPurposes: Set @@ -27,12 +29,15 @@ public struct TraineeTrainingPurposeFeature { /// `TraineeTrainingPurposeFeature.State`의 생성자 /// - Parameters: + /// - signUpEntity: 현재 회원가입 정보 @Shared /// - selectedPurposes: 사용자가 선택한 트레이닝 목적 목록 (기본값: 빈 `Set`) /// - view_isNextButtonEnabled: "다음" 버튼 활성화 여부 (기본값: `false`) public init( + signUpEntity: Shared, selectedPurposes: Set = [], view_isNextButtonEnabled: Bool = false ) { + self._signUpEntity = signUpEntity self.selectedPurposes = selectedPurposes self.view_isNextButtonEnabled = view_isNextButtonEnabled } @@ -74,6 +79,10 @@ public struct TraineeTrainingPurposeFeature { return self.validate(&state) case .tapNextButton: + let purposes = state.selectedPurposes.map { + $0.koreanName + } + state.$signUpEntity.withLock { $0.goalContents = purposes } return .send(.setNavigating) } From b543542b626a9264e65d00aeaf95b47da3775b4f Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Sat, 8 Feb 2025 07:42:41 +0900 Subject: [PATCH 8/9] =?UTF-8?q?[Feat]=20PostSignUp=20API=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Network/Service/User/UserTargetType.swift | 5 +- .../Sources/DTO/User/UserResponseDTO.swift | 2 +- .../Domain/Sources/Entity/SignUpEntity.swift | 89 +++++++++++++++++++ .../Sources/Entity/SocailLoginEntity.swift | 68 -------------- .../Domain/Sources/Entity/UserType.swift | 9 ++ .../Sources/Mapper/PostSocialMapper.swift | 34 +++++++ .../OnboardingFlowFeature.swift | 5 +- .../TraineePrecautionInputFeature.swift | 19 +++- .../TraineeProfileCompletionFeature.swift | 8 +- .../TraineeProfileCompletionView.swift | 37 ++++++++ 10 files changed, 198 insertions(+), 78 deletions(-) create mode 100644 TnT/Projects/Domain/Sources/Entity/SignUpEntity.swift diff --git a/TnT/Projects/Data/Sources/Network/Service/User/UserTargetType.swift b/TnT/Projects/Data/Sources/Network/Service/User/UserTargetType.swift index 0c7bfe8c..84b39c06 100644 --- a/TnT/Projects/Data/Sources/Network/Service/User/UserTargetType.swift +++ b/TnT/Projects/Data/Sources/Network/Service/User/UserTargetType.swift @@ -63,7 +63,10 @@ extension UserTargetType: TargetType { return ["Content-Type": "application/json"] case .postSignUp: - return ["Content-Type": "multipart/form-data"] + return [ + "Content-Type": "multipart/form-data", + "Authorization": "SESSION-ID 1111" + ] } } diff --git a/TnT/Projects/Domain/Sources/DTO/User/UserResponseDTO.swift b/TnT/Projects/Domain/Sources/DTO/User/UserResponseDTO.swift index 339ca6ac..a686e7b8 100644 --- a/TnT/Projects/Domain/Sources/DTO/User/UserResponseDTO.swift +++ b/TnT/Projects/Domain/Sources/DTO/User/UserResponseDTO.swift @@ -47,5 +47,5 @@ public struct PostSignUpResDTO: Decodable { /// 회원 이름 let name: String /// 프로필 이미지 URL - let profileImageUrl: String + let profileImageUrl: String? } diff --git a/TnT/Projects/Domain/Sources/Entity/SignUpEntity.swift b/TnT/Projects/Domain/Sources/Entity/SignUpEntity.swift new file mode 100644 index 00000000..98222dc2 --- /dev/null +++ b/TnT/Projects/Domain/Sources/Entity/SignUpEntity.swift @@ -0,0 +1,89 @@ +// +// SignUpEntity.swift +// Domain +// +// Created by 박민서 on 2/8/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import Foundation + +/// 회원가입 요청 DTO +public struct PostSignUpEntity: Equatable { + /// FCM 토큰 + public var fcmToken: String? + /// 회원 타입 (trainer, trainee) + public var memberType: UserType? + /// 소셜 로그인 타입 (KAKAO, APPLE) + public var socialType: SocialType? + /// 소셜 로그인 ID + public var socialId: String? + /// 소셜 로그인 이메일 + public var socialEmail: String? + /// 서비스 이용 약관 동의 여부 + public var serviceAgreement: Bool + /// 개인정보 수집 동의 여부 + public var collectionAgreement: Bool + /// 광고성 알림 수신 동의 여부 + public var advertisementAgreement: Bool + /// 회원 이름 + public var name: String? + /// 프로필 이미지 데이터 + public var imageData: Data? + /// 생년월일 (yyyy-MM-dd) + public var birthday: String? + /// 키 (cm) + public var height: Double? + /// 몸무게 (kg, 소수점 1자리까지 가능) + public var weight: Double? + /// 트레이너에게 전달할 주의사항 + public var cautionNote: String? + /// PT 목적 (체중 감량, 근력 향상 등) + public var goalContents: [String]? + + public init( + fcmToken: String? = nil, + memberType: UserType? = nil, + socialType: SocialType? = nil, + socialId: String? = nil, + socialEmail: String? = nil, + serviceAgreement: Bool = false, + collectionAgreement: Bool = false, + advertisementAgreement: Bool = false, + name: String? = nil, + imageData: Data? = nil, + birthday: String? = nil, + height: Double? = nil, + weight: Double? = nil, + cautionNote: String? = nil, + goalContents: [String]? = nil + ) { + self.fcmToken = fcmToken + self.memberType = memberType + self.socialType = socialType + self.socialId = socialId + self.socialEmail = socialEmail + self.serviceAgreement = serviceAgreement + self.collectionAgreement = collectionAgreement + self.advertisementAgreement = advertisementAgreement + self.name = name + self.imageData = imageData + self.birthday = birthday + self.height = height + self.weight = weight + self.cautionNote = cautionNote + self.goalContents = goalContents + } +} + +/// 회원가입 응답 DTO +public struct PostSignUpResEntity: Equatable, Sendable { + /// 회원 타입 (trainer, trainee) + public let memberType: String + /// 세션 id + public let sessionId: String + /// 회원 이름 + public let name: String + /// 프로필 이미지 URL + public let profileImageUrl: String? +} diff --git a/TnT/Projects/Domain/Sources/Entity/SocailLoginEntity.swift b/TnT/Projects/Domain/Sources/Entity/SocailLoginEntity.swift index 22300e78..4dbb1792 100644 --- a/TnT/Projects/Domain/Sources/Entity/SocailLoginEntity.swift +++ b/TnT/Projects/Domain/Sources/Entity/SocailLoginEntity.swift @@ -50,71 +50,3 @@ public struct PostSocialLoginResEntity: Equatable { /// 가입 여부 (`true`: 이미 가입됨, `false`: 미가입) public let isSignUp: Bool } - -/// 회원가입 요청 DTO -public struct PostSignUpEntity: Equatable { - /// FCM 토큰 - public var fcmToken: String? - /// 회원 타입 (trainer, trainee) - public var memberType: UserType? - /// 소셜 로그인 타입 (KAKAO, APPLE) - public var socialType: SocialType? - /// 소셜 로그인 ID - public var socialId: String? - /// 소셜 로그인 이메일 - public var socialEmail: String? - /// 서비스 이용 약관 동의 여부 - public var serviceAgreement: Bool - /// 개인정보 수집 동의 여부 - public var collectionAgreement: Bool - /// 광고성 알림 수신 동의 여부 - public var advertisementAgreement: Bool - /// 회원 이름 - public var name: String? - /// 프로필 이미지 데이터 - public var imageData: Data? - /// 생년월일 (yyyy-MM-dd) - public var birthday: String? - /// 키 (cm) - public var height: Double? - /// 몸무게 (kg, 소수점 1자리까지 가능) - public var weight: Double? - /// 트레이너에게 전달할 주의사항 - public var cautionNote: String? - /// PT 목적 (체중 감량, 근력 향상 등) - public var goalContents: [String]? - - public init( - fcmToken: String? = nil, - memberType: UserType? = nil, - socialType: SocialType? = nil, - socialId: String? = nil, - socialEmail: String? = nil, - serviceAgreement: Bool = false, - collectionAgreement: Bool = false, - advertisementAgreement: Bool = false, - name: String? = nil, - imageData: Data? = nil, - birthday: String? = nil, - height: Double? = nil, - weight: Double? = nil, - cautionNote: String? = nil, - goalContents: [String]? = nil - ) { - self.fcmToken = fcmToken - self.memberType = memberType - self.socialType = socialType - self.socialId = socialId - self.socialEmail = socialEmail - self.serviceAgreement = serviceAgreement - self.collectionAgreement = collectionAgreement - self.advertisementAgreement = advertisementAgreement - self.name = name - self.imageData = imageData - self.birthday = birthday - self.height = height - self.weight = weight - self.cautionNote = cautionNote - self.goalContents = goalContents - } -} diff --git a/TnT/Projects/Domain/Sources/Entity/UserType.swift b/TnT/Projects/Domain/Sources/Entity/UserType.swift index 7d53dae8..58f3f30f 100644 --- a/TnT/Projects/Domain/Sources/Entity/UserType.swift +++ b/TnT/Projects/Domain/Sources/Entity/UserType.swift @@ -24,4 +24,13 @@ public extension UserType { return "트레이니" } } + + var englishName: String { + switch self { + case .trainer: + return "trainer" + case .trainee: + return "trainee" + } + } } diff --git a/TnT/Projects/Domain/Sources/Mapper/PostSocialMapper.swift b/TnT/Projects/Domain/Sources/Mapper/PostSocialMapper.swift index 6e29b3fc..9efc3cd2 100644 --- a/TnT/Projects/Domain/Sources/Mapper/PostSocialMapper.swift +++ b/TnT/Projects/Domain/Sources/Mapper/PostSocialMapper.swift @@ -30,3 +30,37 @@ public extension PostSocialLoginResDTO { ) } } + +public extension PostSignUpEntity { + func toDTO() -> PostSignUpReqDTO? { + guard let memberType, let socialType, let socialId, let socialEmail, let name, let goalContents else { return nil } + return .init( + // TODO: FCM 서버 로직 나오면 수정 + fcmToken: self.fcmToken ?? "temp", + memberType: memberType.englishName, + socialType: socialType.rawValue, + socialId: socialId, + socialEmail: socialEmail, + serviceAgreement: self.serviceAgreement, + collectionAgreement: self.collectionAgreement, + advertisementAgreement: self.advertisementAgreement, + name: name, + birthday: self.birthday, + height: self.height, + weight: self.weight, + cautionNote: self.cautionNote, + goalContents: goalContents + ) + } +} + +public extension PostSignUpResDTO { + func toEntity() -> PostSignUpResEntity { + return .init( + memberType: self.memberType, + sessionId: self.sessionId, + name: self.name, + profileImageUrl: self.profileImageUrl + ) + } +} diff --git a/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift b/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift index e3e82f11..f46b7538 100644 --- a/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift +++ b/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift @@ -98,8 +98,9 @@ public struct OnboardingFlowFeature { return .none /// 트레이니 주의사항 입력 -> 트레이니 회원 가입 완료 화면 이동 - case .element(id: _, action: .traineePrecautionInput(.setNavigating)): - state.path.append(.traineeProfileCompletion(.init())) + case .element(id: _, action: .traineePrecautionInput(.setNavigating(let info))): + state.path.append(.traineeProfileCompletion(.init(userName: info.name, profileImage: info.profileImageUrl))) + return .none default: diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineePrecautionInput/TraineePrecautionInputFeature.swift b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineePrecautionInput/TraineePrecautionInputFeature.swift index 32825a05..5cbe6e06 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineePrecautionInput/TraineePrecautionInputFeature.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineePrecautionInput/TraineePrecautionInputFeature.swift @@ -61,12 +61,15 @@ public struct TraineePrecautionInputFeature { } @Dependency(\.userUseCase) private var userUseCase: UserUseCase + @Dependency(\.userUseRepoCase) private var userUseRepoCase: UserRepository public enum Action: Sendable, ViewAction { /// 뷰에서 발생한 액션을 처리합니다. case view(View) /// 네비게이션 여부 설정 - case setNavigating + case setNavigating(PostSignUpResEntity) + /// 회원가입 POST + case postSignUp @CasePathable public enum View: Sendable, BindableAction { @@ -100,9 +103,21 @@ public struct TraineePrecautionInputFeature { case .tapNextButton: state.$signUpEntity.withLock { $0.cautionNote = state.precaution } - return .send(.setNavigating) + return .send(.postSignUp) } + case .postSignUp: + guard let reqDTO = state.signUpEntity.toDTO() else { + return .none + } + let imgData = state.signUpEntity.imageData + + return .run { send in + let result = try await userUseRepoCase.postSignUp(reqDTO, profileImage: imgData).toEntity() + // TODO: 세션, 유저타입 정보 키체인 저장 + await send(.setNavigating(result)) + } + case .setNavigating: return .none } diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeProfileCompletion/TraineeProfileCompletionFeature.swift b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeProfileCompletion/TraineeProfileCompletionFeature.swift index 5fe7f584..f14e4878 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeProfileCompletion/TraineeProfileCompletionFeature.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeProfileCompletion/TraineeProfileCompletionFeature.swift @@ -22,8 +22,8 @@ public struct TraineeProfileCompletionFeature { var userType: UserType /// 현재 사용자 이름 var userName: String - /// 등록한 프로필 이미지 데이터 - var profileImage: Data? + /// 등록한 프로필 이미지 링크 + var profileImage: String? // MARK: UI related state /// 상대방 유저 타입 (사용자가 트레이너면 트레이니, 트레이니면 트레이너) @@ -35,11 +35,11 @@ public struct TraineeProfileCompletionFeature { /// - Parameters: /// - userType: 현재 선택된 유저 타입 (기본값: `.trainee`) /// - userName: 입력된 사용자 이름 (기본값: `""`) - /// - profileImage: 등록한 프로필 이미지 데이터 (기본값: `nil`) + /// - profileImage: 등록한 프로필 이미지 링크 (기본값: `nil`) public init( userType: UserType = .trainee, userName: String = "", - profileImage: Data? = nil + profileImage: String? = nil ) { self.userType = userType self.userName = userName diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeProfileCompletion/TraineeProfileCompletionView.swift b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeProfileCompletion/TraineeProfileCompletionView.swift index fe9c0a19..dde0e408 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeProfileCompletion/TraineeProfileCompletionView.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeProfileCompletion/TraineeProfileCompletionView.swift @@ -72,4 +72,41 @@ public struct TraineeProfileCompletionView: View { .frame(width: 200, height: 200) .clipShape(Circle()) } + + @ViewBuilder + public func ImageSection(imgURL: URL) -> some View { + if let urlString = store.profileImage, let url = URL(string: urlString) { + AsyncImage(url: url) { phase in + switch phase { + case .empty: + ProgressView() + .tint(.red500) + .frame(width: 132, height: 132) + + case .success(let image): + image + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 132, height: 132) + .clipShape(Circle()) + + case .failure: + Image(store.userType == .trainee ? .imgDefaultTraineeImage : .imgDefaultTrainerImage) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 132, height: 132) + .clipShape(Circle()) + + @unknown default: + EmptyView() + } + } + } else { + Image(store.userType == .trainee ? .imgDefaultTraineeImage : .imgDefaultTrainerImage) + .resizable() + .scaledToFill() + .frame(width: 132, height: 132) + .clipShape(Circle()) + } + } } From 49f36c5306ce0b0dbf72e5de1c7e243933ca4fe7 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Sat, 8 Feb 2025 08:14:35 +0900 Subject: [PATCH 9/9] =?UTF-8?q?[Feat]=20Trainer=20PostSignUp=20API=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Mapper/PostSocialMapper.swift | 6 +++--- .../OnboardingFlowFeature.swift | 8 ++++---- .../OnboardingFlow/OnboardingFlowView.swift | 4 ++-- .../CreateProfile/CreateProfileFeature.swift | 20 ++++++++++++++++--- ...e.swift => ProfileCompletionFeature.swift} | 2 +- ...View.swift => ProfileCompletionView.swift} | 8 ++++---- 6 files changed, 31 insertions(+), 17 deletions(-) rename TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeProfileCompletion/{TraineeProfileCompletionFeature.swift => ProfileCompletionFeature.swift} (98%) rename TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeProfileCompletion/{TraineeProfileCompletionView.swift => ProfileCompletionView.swift} (93%) diff --git a/TnT/Projects/Domain/Sources/Mapper/PostSocialMapper.swift b/TnT/Projects/Domain/Sources/Mapper/PostSocialMapper.swift index 9efc3cd2..417d1e81 100644 --- a/TnT/Projects/Domain/Sources/Mapper/PostSocialMapper.swift +++ b/TnT/Projects/Domain/Sources/Mapper/PostSocialMapper.swift @@ -33,7 +33,7 @@ public extension PostSocialLoginResDTO { public extension PostSignUpEntity { func toDTO() -> PostSignUpReqDTO? { - guard let memberType, let socialType, let socialId, let socialEmail, let name, let goalContents else { return nil } + guard let memberType, let socialType, let socialId, let socialEmail, let name else { return nil } return .init( // TODO: FCM 서버 로직 나오면 수정 fcmToken: self.fcmToken ?? "temp", @@ -45,11 +45,11 @@ public extension PostSignUpEntity { collectionAgreement: self.collectionAgreement, advertisementAgreement: self.advertisementAgreement, name: name, - birthday: self.birthday, + birthday: self.birthday?.replacingOccurrences(of: "/", with: "-"), height: self.height, weight: self.weight, cautionNote: self.cautionNote, - goalContents: goalContents + goalContents: self.goalContents ?? [] ) } } diff --git a/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift b/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift index f46b7538..38e002b4 100644 --- a/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift +++ b/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowFeature.swift @@ -70,8 +70,8 @@ public struct OnboardingFlowFeature { switch screen { case .traineeBasicInfoInput: state.path.append(.traineeBasicInfoInput(.init(signUpEntity: state.$signUpEntity))) - case .trainerSignUpComplete: - state.path.append(.trainerSignUpComplete(.init())) + case .trainerSignUpComplete(let info): + state.path.append(.trainerSignUpComplete(.init(userType: .trainer, userName: info.name, profileImage: info.profileImageUrl))) } return .none @@ -132,7 +132,7 @@ extension OnboardingFlowFeature { // MARK: Trainer /// 트레이너 회원 가입 완료 뷰 /// TODO: 트레이너/트레이니 회원 가입 완료 화면으로 통합 필요 - case trainerSignUpComplete(TrainerSignUpCompleteFeature) + case trainerSignUpComplete(ProfileCompletionFeature) /// 트레이너의 초대코드 발급 뷰 case trainerMakeInvitationCode(MakeInvitationCodeFeature) /// 트레이너의 트레이니 프로필 확인 뷰 @@ -147,7 +147,7 @@ extension OnboardingFlowFeature { case traineePrecautionInput(TraineePrecautionInputFeature) /// 트레이니 프로필 생성 완료 /// TODO: 트레이너/트레이니 회원 가입 완료 화면으로 통합 필요 - case traineeProfileCompletion(TraineeProfileCompletionFeature) + case traineeProfileCompletion(ProfileCompletionFeature) /// 트레이니 초대 코드입력 case traineeInvitationCodeInput(TraineeInvitationCodeInputFeature) /// 트레이니 수업 정보 입력 diff --git a/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowView.swift b/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowView.swift index ad78d844..86672fb2 100644 --- a/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowView.swift +++ b/TnT/Projects/Presentation/Sources/Coordinator/OnboardingFlow/OnboardingFlowView.swift @@ -33,7 +33,7 @@ public struct OnboardingFlowView: View { // MARK: Trainer case .trainerSignUpComplete(let store): - TrainerSignUpCompleteView(store: store) + ProfileCompletionView(store: store) case .trainerMakeInvitationCode(let store): MakeInvitationCodeView(store: store) case .trainerConnectedTraineeProfile(let store): @@ -47,7 +47,7 @@ public struct OnboardingFlowView: View { case .traineePrecautionInput(let store): TraineePrecautionInputView(store: store) case .traineeProfileCompletion(let store): - TraineeProfileCompletionView(store: store) + ProfileCompletionView(store: store) case .traineeInvitationCodeInput(let store): TraineeInvitationCodeInputView(store: store) case .traineeTrainingInfoInput(let store): diff --git a/TnT/Projects/Presentation/Sources/Onboarding/CreateProfile/CreateProfileFeature.swift b/TnT/Projects/Presentation/Sources/Onboarding/CreateProfile/CreateProfileFeature.swift index c783bf3e..af216de0 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/CreateProfile/CreateProfileFeature.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/CreateProfile/CreateProfileFeature.swift @@ -91,6 +91,7 @@ public struct CreateProfileFeature { } @Dependency(\.userUseCase) private var userUseCase: UserUseCase + @Dependency(\.userUseRepoCase) private var userUseRepoCase: UserRepository public enum Action: Sendable, ViewAction { /// 네비게이션 여부 설정 @@ -99,6 +100,8 @@ public struct CreateProfileFeature { case imagePicked(Data?) /// 뷰에서 발생한 액션을 처리합니다. case view(View) + /// 회원가입 POST + case postSignUp @CasePathable public enum View: Sendable, BindableAction { @@ -145,10 +148,22 @@ public struct CreateProfileFeature { case .trainee: return .send(.setNavigating(.traineeBasicInfoInput)) case .trainer: - return .send(.setNavigating(.trainerSignUpComplete)) + return .send(.postSignUp) } } + case .postSignUp: + guard let reqDTO = state.signUpEntity.toDTO() else { + return .none + } + let imgData = state.signUpEntity.imageData + + return .run { send in + let result = try await userUseRepoCase.postSignUp(reqDTO, profileImage: imgData).toEntity() + // TODO: 세션, 유저타입 정보 키체인 저장 + await send(.setNavigating(.trainerSignUpComplete(result))) + } + case .setNavigating: return .none @@ -182,7 +197,6 @@ extension CreateProfileFeature { /// 트레이니 회원가입 case traineeBasicInfoInput /// 트레이너 프로필 생성 완료 - case trainerSignUpComplete - + case trainerSignUpComplete(PostSignUpResEntity) } } diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeProfileCompletion/TraineeProfileCompletionFeature.swift b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeProfileCompletion/ProfileCompletionFeature.swift similarity index 98% rename from TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeProfileCompletion/TraineeProfileCompletionFeature.swift rename to TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeProfileCompletion/ProfileCompletionFeature.swift index f14e4878..dd52e8c1 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeProfileCompletion/TraineeProfileCompletionFeature.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeProfileCompletion/ProfileCompletionFeature.swift @@ -13,7 +13,7 @@ import Domain /// 회원가입 완료 후 프로필 정보를 표시하는 리듀서 @Reducer -public struct TraineeProfileCompletionFeature { +public struct ProfileCompletionFeature { @ObservableState public struct State: Equatable { diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeProfileCompletion/TraineeProfileCompletionView.swift b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeProfileCompletion/ProfileCompletionView.swift similarity index 93% rename from TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeProfileCompletion/TraineeProfileCompletionView.swift rename to TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeProfileCompletion/ProfileCompletionView.swift index dde0e408..86060a26 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeProfileCompletion/TraineeProfileCompletionView.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeProfileCompletion/ProfileCompletionView.swift @@ -16,14 +16,14 @@ import DesignSystem /// - 사용자 타입 및 이름을 표시 /// - 상대 유저 타입을 안내하는 메시지 제공 /// - "시작하기" 버튼을 통해 다음 화면으로 이동 -@ViewAction(for: TraineeProfileCompletionFeature.self) -public struct TraineeProfileCompletionView: View { +@ViewAction(for: ProfileCompletionFeature.self) +public struct ProfileCompletionView: View { - @Bindable public var store: StoreOf + @Bindable public var store: StoreOf /// `ProfileCompletion` 생성자 /// - Parameter store: `TraineeProfileCompletionFeature`와 연결된 Store - public init(store: StoreOf) { + public init(store: StoreOf) { self.store = store }