From 89a931c0303ebe2cdc74fa46b85c9c9bc2e96460 Mon Sep 17 00:00:00 2001 From: Park Seo Yeon Date: Sat, 15 Feb 2025 15:06:32 +0900 Subject: [PATCH 01/32] =?UTF-8?q?[Fix]=20=ED=83=AD=EB=B0=94=20store=20init?= =?UTF-8?q?=20=EC=97=90=EB=9F=AC=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 마이페이지 더블 탭시 발생하는 에러 처리 - 에러 이유 : 마이페이지의 알림 설정여부를 비동기로 가져오는 부분이 tca와 충돌 발생(타이밍 안 맞음) --- .../Sources/MainTab/Trainee/TraineeMainTabFeature.swift | 1 + .../Sources/MainTab/Trainer/TrainerMainTabFeature.swift | 1 + 2 files changed, 2 insertions(+) diff --git a/TnT/Projects/Presentation/Sources/MainTab/Trainee/TraineeMainTabFeature.swift b/TnT/Projects/Presentation/Sources/MainTab/Trainee/TraineeMainTabFeature.swift index d1f0759f..1e0bcf25 100644 --- a/TnT/Projects/Presentation/Sources/MainTab/Trainee/TraineeMainTabFeature.swift +++ b/TnT/Projects/Presentation/Sources/MainTab/Trainee/TraineeMainTabFeature.swift @@ -72,6 +72,7 @@ public struct TraineeMainTabFeature { case let .view(view): switch view { case .selectTab(let tab): + guard state.tabInfo != tab else { return .none } switch tab { case .home: state = .home(.init()) diff --git a/TnT/Projects/Presentation/Sources/MainTab/Trainer/TrainerMainTabFeature.swift b/TnT/Projects/Presentation/Sources/MainTab/Trainer/TrainerMainTabFeature.swift index 3c3a24b0..f16ccf8f 100644 --- a/TnT/Projects/Presentation/Sources/MainTab/Trainer/TrainerMainTabFeature.swift +++ b/TnT/Projects/Presentation/Sources/MainTab/Trainer/TrainerMainTabFeature.swift @@ -85,6 +85,7 @@ public struct TrainerMainTabFeature { case let .view(view): switch view { case .selectTab(let tab): + guard state.tabInfo != tab else { return .none } switch tab { case .home: state = .home(.init()) From ab0299a2d56723c524965556f62054fc00ea92c6 Mon Sep 17 00:00:00 2001 From: Park Seo Yeon Date: Sat, 15 Feb 2025 15:50:03 +0900 Subject: [PATCH 02/32] =?UTF-8?q?[Refactor]=20=EB=B2=84=EC=A0=84=20?= =?UTF-8?q?=EC=A1=B0=EC=A0=95,=20=ED=9A=8C=EC=9B=90=20=EB=AA=A9=EB=A1=9D?= =?UTF-8?q?=20=EB=AA=A8=EB=8B=AC=20padding=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/AddPTSession/TrainerSelectSessionTraineeView.swift | 1 - .../ProjectDescriptionHelpers/Environment/Environment.swift | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/TnT/Projects/Presentation/Sources/AddPTSession/TrainerSelectSessionTraineeView.swift b/TnT/Projects/Presentation/Sources/AddPTSession/TrainerSelectSessionTraineeView.swift index 7a383ff8..98dee8ac 100644 --- a/TnT/Projects/Presentation/Sources/AddPTSession/TrainerSelectSessionTraineeView.swift +++ b/TnT/Projects/Presentation/Sources/AddPTSession/TrainerSelectSessionTraineeView.swift @@ -37,7 +37,6 @@ public struct TrainerSelectSessionTraineeView: View { public var body: some View { VStack(alignment: .leading, spacing: 0) { Header() - .padding(.top, 30) if contentHeight >= 708 { ScrollView { diff --git a/TnT/Tuist/ProjectDescriptionHelpers/Environment/Environment.swift b/TnT/Tuist/ProjectDescriptionHelpers/Environment/Environment.swift index 311f8965..b18f9903 100644 --- a/TnT/Tuist/ProjectDescriptionHelpers/Environment/Environment.swift +++ b/TnT/Tuist/ProjectDescriptionHelpers/Environment/Environment.swift @@ -11,5 +11,5 @@ public enum Environment { public static let appName: String = "TnTApp" public static let organizationName = "yapp25thTeamTnT" public static let destinations: Destinations = [.iPhone] - public static let deploymentTarget: DeploymentTargets = .iOS("17.5") + public static let deploymentTarget: DeploymentTargets = .iOS("17.0") } From 02ed9489774677c37b7cebfe0f4a0a13138a1b3e Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Sat, 15 Feb 2025 15:26:07 +0900 Subject: [PATCH 03/32] =?UTF-8?q?[Fix]=20Gesture=20Nav=20Back=20-=20DragDi?= =?UTF-8?q?smiss=20=EC=B6=A9=EB=8F=8C=20=EC=97=90=EB=9F=AC=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/DietRecordDetail/TraineeDietRecordDetailView.swift | 1 - TnT/Projects/Presentation/Sources/Utility/KeyboardDismiss.swift | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/TnT/Projects/Presentation/Sources/DietRecordDetail/TraineeDietRecordDetailView.swift b/TnT/Projects/Presentation/Sources/DietRecordDetail/TraineeDietRecordDetailView.swift index bd6c9b58..5d30b96c 100644 --- a/TnT/Projects/Presentation/Sources/DietRecordDetail/TraineeDietRecordDetailView.swift +++ b/TnT/Projects/Presentation/Sources/DietRecordDetail/TraineeDietRecordDetailView.swift @@ -51,7 +51,6 @@ public struct TraineeDietRecordDetailView: View { } } .navigationBarBackButtonHidden() - .keyboardDismissOnTap() .onAppear { send(.onAppear) } diff --git a/TnT/Projects/Presentation/Sources/Utility/KeyboardDismiss.swift b/TnT/Projects/Presentation/Sources/Utility/KeyboardDismiss.swift index ea070de9..2d0ab8be 100644 --- a/TnT/Projects/Presentation/Sources/Utility/KeyboardDismiss.swift +++ b/TnT/Projects/Presentation/Sources/Utility/KeyboardDismiss.swift @@ -39,7 +39,7 @@ struct KeyboardDismissModifier: ViewModifier { /// `View`에 `.keyboardDismissOnTap()`을 추가할 수 있도록 Extension extension View { - func keyboardDismissOnTap(dismissOnDrag: Bool = true) -> some View { + func keyboardDismissOnTap(dismissOnDrag: Bool = false) -> some View { self.modifier(KeyboardDismissModifier(dismissOnDrag: dismissOnDrag)) } } From d3d35fe0c2f456641b66a275f4433f47c4207b73 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Sat, 15 Feb 2025 16:00:45 +0900 Subject: [PATCH 04/32] =?UTF-8?q?[Feat]=20=EB=84=A4=ED=8A=B8=EC=9B=8C?= =?UTF-8?q?=ED=81=AC=20=EC=97=90=EB=9F=AC=20=ED=91=9C=EC=8B=9C=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Network/Foundation/NetworkError.swift | 13 +++++++++++++ .../Data/Sources/Network/NetworkService.swift | 2 ++ 2 files changed, 15 insertions(+) diff --git a/TnT/Projects/Data/Sources/Network/Foundation/NetworkError.swift b/TnT/Projects/Data/Sources/Network/Foundation/NetworkError.swift index b60fc17d..fcd9e811 100644 --- a/TnT/Projects/Data/Sources/Network/Foundation/NetworkError.swift +++ b/TnT/Projects/Data/Sources/Network/Foundation/NetworkError.swift @@ -17,6 +17,7 @@ enum NetworkError: Error { case unauthorized(message: String?) // 401 case forbidden(message: String?) // 403 case notFound(message: String?) // 404 + case conflict(message: String?) // 409 // MARK: - Server Errors (500 ~ 599) case serverError(statusCode: Int, message: String?) @@ -47,6 +48,8 @@ extension NetworkError: LocalizedError { return "[403] \(message ?? "권한이 없습니다.")" case .notFound(let message): return "[404] \(message ?? "요청한 리소스를 찾을 수 없습니다.")" + case .conflict(let message): + return "[409] \(message ?? "요청이 충돌되었습니다.")" case .timeout: return "요청 시간이 초과되었습니다." case .noInternet: @@ -57,4 +60,14 @@ extension NetworkError: LocalizedError { return "[\(statusCode ?? 0)] \(message ?? "알 수 없는 오류 발생")" } } + + /// UI 표시 여부 + var isUIToastError: Bool { + switch self { + case .notFound, .conflict: + return true + default: + return false + } + } } diff --git a/TnT/Projects/Data/Sources/Network/NetworkService.swift b/TnT/Projects/Data/Sources/Network/NetworkService.swift index 9b1e4d22..00cdf441 100644 --- a/TnT/Projects/Data/Sources/Network/NetworkService.swift +++ b/TnT/Projects/Data/Sources/Network/NetworkService.swift @@ -48,6 +48,8 @@ public final class NetworkService { switch error { case .unauthorized: NotificationCenter.default.postSessionExpired() + case .notFound(let message), .conflict(let message): + NotificationCenter.default.post(toast: .init(presentType: .text("⚠"), message: message ?? "")) default: NotificationCenter.default.post(toast: .init(presentType: .text("⚠"), message: "서버 요청에 실패했어요")) } From b90168e6395dfc8005ee21c03943cd61556f5854 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Sat, 15 Feb 2025 16:03:04 +0900 Subject: [PATCH 05/32] =?UTF-8?q?[Fix]=20AddDiet,=20AddPTSession=20?= =?UTF-8?q?=EB=9D=BC=EC=9A=B0=ED=8C=85=20=EC=B2=98=EB=A6=AC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/AddDietRecord/TraineeAddDietRecordFeature.swift | 4 +++- .../Sources/AddPTSession/TrainerAddPTSessionFeature.swift | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordFeature.swift b/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordFeature.swift index 6da5147f..3002f7f7 100644 --- a/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordFeature.swift +++ b/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordFeature.swift @@ -219,7 +219,9 @@ public struct TraineeAddDietRecordFeature { case .tapPopUpPrimaryButton(let popUp): guard popUp != nil else { return .none } - return setPopUpStatus(&state, status: nil) + return popUp == .dietAdded + ? .send(.setNavigating) + : setPopUpStatus(&state, status: nil) case let .setFocus(oldFocus, newFocus): guard oldFocus != newFocus else { return .none } diff --git a/TnT/Projects/Presentation/Sources/AddPTSession/TrainerAddPTSessionFeature.swift b/TnT/Projects/Presentation/Sources/AddPTSession/TrainerAddPTSessionFeature.swift index 9c25568f..01d86cc7 100644 --- a/TnT/Projects/Presentation/Sources/AddPTSession/TrainerAddPTSessionFeature.swift +++ b/TnT/Projects/Presentation/Sources/AddPTSession/TrainerAddPTSessionFeature.swift @@ -241,7 +241,9 @@ public struct TrainerAddPTSessionFeature { case .tapPopUpPrimaryButton(let popUp): guard popUp != nil else { return .none } - return setPopUpStatus(&state, status: nil) + return popUp == .sessionAdded + ? .send(.setNavigating) + : setPopUpStatus(&state, status: nil) case let .setFocus(oldFocus, newFocus): state.view_focusField = newFocus From 21f5ff7e697ba84d2df726cd61e5c8fdedb453ae Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Sat, 15 Feb 2025 16:04:59 +0900 Subject: [PATCH 06/32] =?UTF-8?q?[Fix]=20AddDiet=20=EB=A9=94=EB=AA=A8=20?= =?UTF-8?q?=EC=9C=A0=ED=9A=A8=EA=B2=80=EC=82=AC=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/AddDietRecord/TraineeAddDietRecordFeature.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordFeature.swift b/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordFeature.swift index 3002f7f7..0aba704f 100644 --- a/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordFeature.swift +++ b/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordFeature.swift @@ -276,7 +276,7 @@ private extension TraineeAddDietRecordFeature { guard state.dietDate != nil else { return .none } guard state.dietTime != nil else { return .none } guard state.dietType != nil else { return .none } - guard state.dietInfo != nil else { return .none } + guard !state.dietInfo.isEmpty else { return .none } state.view_isSubmitButtonEnabled = true return .none From 0fb5947498645c61cb622a8d7b6ce611e3543296 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Sat, 15 Feb 2025 16:31:45 +0900 Subject: [PATCH 07/32] =?UTF-8?q?[Fix]=20AddDiet=20=EC=9C=A0=ED=9A=A8?= =?UTF-8?q?=EC=84=B1=20=EA=B2=80=EC=82=AC=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95,=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A0=84=EB=8B=AC?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TraineeAddDietRecordFeature.swift | 17 ++++++++++++++--- .../TraineeAddDietRecordView.swift | 2 ++ .../TraineeMainFlowFeature.swift | 4 ++-- .../Home/Trainee/TraineeHomeFeature.swift | 4 ++-- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordFeature.swift b/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordFeature.swift index 0aba704f..ac23e5b3 100644 --- a/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordFeature.swift +++ b/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordFeature.swift @@ -21,6 +21,8 @@ public struct TraineeAddDietRecordFeature { @ObservableState public struct State: Equatable { // MARK: Data related state + /// 캘린더에서 선택된 날짜 + var calendarSelectedDate: Date /// 식단 날짜 var dietDate: Date? /// 식단 시간 @@ -51,6 +53,7 @@ public struct TraineeAddDietRecordFeature { var view_isPopUpPresented: Bool public init( + calendarSelectedDate: Date = .now, dietDate: Date? = nil, dietTime: Date? = nil, dietType: DietType? = nil, @@ -66,6 +69,7 @@ public struct TraineeAddDietRecordFeature { view_popUp: PopUp? = nil, view_isPopUpPresented: Bool = false ) { + self.calendarSelectedDate = calendarSelectedDate self.dietDate = dietDate self.dietTime = dietTime self.dietType = dietType @@ -122,6 +126,8 @@ public struct TraineeAddDietRecordFeature { case tapPopUpPrimaryButton(popUp: PopUp?) /// 포커스 상태 변경 case setFocus(FocusField?, FocusField?) + /// 화면 표시될 때 + case onAppear } @CasePathable @@ -227,6 +233,11 @@ public struct TraineeAddDietRecordFeature { guard oldFocus != newFocus else { return .none } state.view_focusField = newFocus return .none + + case .onAppear: + state.dietDate = state.calendarSelectedDate + state.view_dietDateStatus = .filled + return .none } case .api(let action): @@ -271,12 +282,12 @@ private extension TraineeAddDietRecordFeature { /// 모든 필드의 상태를 검증하여 "다음" 버튼 활성화 여부를 결정 func validateAllFields(_ state: inout State) -> Effect { - - guard state.dietImageData != nil else { return .none } guard state.dietDate != nil else { return .none } guard state.dietTime != nil else { return .none } + guard let date = combinedDietDateTime(date: state.dietDate, time: state.dietTime), + date <= .now else { return .none } guard state.dietType != nil else { return .none } - guard !state.dietInfo.isEmpty else { return .none } + guard !state.dietInfo.isEmpty && state.dietInfo.count <= 100 else { return .none } state.view_isSubmitButtonEnabled = true return .none diff --git a/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordView.swift b/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordView.swift index 34def510..ff514c88 100644 --- a/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordView.swift +++ b/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordView.swift @@ -71,6 +71,7 @@ public struct TraineeAddDietRecordView: View { .disabled(!store.view_isSubmitButtonEnabled) .debounce() .padding(.horizontal, 16) + .background(Color.common0) } .sheet(item: $store.view_bottomSheetItem) { item in switch item { @@ -109,6 +110,7 @@ public struct TraineeAddDietRecordView: View { send(.setFocus(oldValue, newValue)) } } + .onAppear { send(.onAppear) } } // MARK: - Sections diff --git a/TnT/Projects/Presentation/Sources/Coordinator/TraineeMainFlow/TraineeMainFlowFeature.swift b/TnT/Projects/Presentation/Sources/Coordinator/TraineeMainFlow/TraineeMainFlowFeature.swift index ed340219..b61f12f0 100644 --- a/TnT/Projects/Presentation/Sources/Coordinator/TraineeMainFlow/TraineeMainFlowFeature.swift +++ b/TnT/Projects/Presentation/Sources/Coordinator/TraineeMainFlow/TraineeMainFlowFeature.swift @@ -55,8 +55,8 @@ public struct TraineeMainFlowFeature { return .none case .addWorkoutRecordPage: return .none - case .addDietRecordPage: - state.path.append(.addDietRecordPage(.init())) + case .addDietRecordPage(let date): + state.path.append(.addDietRecordPage(.init(calendarSelectedDate: date))) return .none case .traineeInvitationCodeInput: state.path.append(.traineeInvitationCodeInput(.init(view_navigationType: .existingUser))) diff --git a/TnT/Projects/Presentation/Sources/Home/Trainee/TraineeHomeFeature.swift b/TnT/Projects/Presentation/Sources/Home/Trainee/TraineeHomeFeature.swift index 18ef3378..cc600258 100644 --- a/TnT/Projects/Presentation/Sources/Home/Trainee/TraineeHomeFeature.swift +++ b/TnT/Projects/Presentation/Sources/Home/Trainee/TraineeHomeFeature.swift @@ -181,7 +181,7 @@ public struct TraineeHomeFeature { case .tapAddDietRecordButton: state.view_isBottomSheetPresented = false - return .send(.setNavigating(.addDietRecordPage)) + return .send(.setNavigating(.addDietRecordPage(selectedDate: state.selectedDate))) case .tapPopUpNextButton: if state.isHideUntilSelected { @@ -303,7 +303,7 @@ extension TraineeHomeFeature { /// 운동 기록 추가 페이지 case addWorkoutRecordPage /// 식단 기록 추가 페이지 - case addDietRecordPage + case addDietRecordPage(selectedDate: Date) /// 초대코드 입력 페이지 case traineeInvitationCodeInput } From 569305b6f3137bf86eb807dc51b900b30c4fa4c5 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Sat, 15 Feb 2025 16:38:56 +0900 Subject: [PATCH 08/32] =?UTF-8?q?[Feat]=20AddPTSession=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=A0=84=EB=8B=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/AddPTSession/TrainerAddPTSessionFeature.swift | 6 ++++++ .../TrainerMainFlow/TrainerMainFlowFeature.swift | 4 ++-- .../Sources/Home/Trainer/TrainerHomeFeature.swift | 6 +++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/TnT/Projects/Presentation/Sources/AddPTSession/TrainerAddPTSessionFeature.swift b/TnT/Projects/Presentation/Sources/AddPTSession/TrainerAddPTSessionFeature.swift index 01d86cc7..d473413e 100644 --- a/TnT/Projects/Presentation/Sources/AddPTSession/TrainerAddPTSessionFeature.swift +++ b/TnT/Projects/Presentation/Sources/AddPTSession/TrainerAddPTSessionFeature.swift @@ -20,6 +20,8 @@ public struct TrainerAddPTSessionFeature { @ObservableState public struct State: Equatable { // MARK: Data related state + /// 캘린더에서 선택된 날짜 + var calendarSelectedDate: Date /// 트레이너 회원 목록 var traineeList: [TraineeListItemEntity] /// 선택된 회원 @@ -58,6 +60,7 @@ public struct TrainerAddPTSessionFeature { } public init( + calendarSelectedDate: Date = .now, traineeList: [TraineeListItemEntity] = [], trainee: TraineeListItemEntity? = nil, ptDate: Date? = nil, @@ -75,6 +78,7 @@ public struct TrainerAddPTSessionFeature { view_popUp: PopUp? = nil, view_isPopUpPresented: Bool = false ) { + self.calendarSelectedDate = calendarSelectedDate self.traineeList = traineeList self.trainee = trainee self.ptDate = ptDate @@ -250,6 +254,8 @@ public struct TrainerAddPTSessionFeature { return .none case .onAppear: + state.ptDate = state.calendarSelectedDate + state.view_ptDateStatus = .filled return .send(.api(.getTraineeList)) } diff --git a/TnT/Projects/Presentation/Sources/Coordinator/TrainerMainFlow/TrainerMainFlowFeature.swift b/TnT/Projects/Presentation/Sources/Coordinator/TrainerMainFlow/TrainerMainFlowFeature.swift index ca1c531f..191d0154 100644 --- a/TnT/Projects/Presentation/Sources/Coordinator/TrainerMainFlow/TrainerMainFlowFeature.swift +++ b/TnT/Projects/Presentation/Sources/Coordinator/TrainerMainFlow/TrainerMainFlowFeature.swift @@ -46,8 +46,8 @@ public struct TrainerMainFlowFeature { case .alarmPage: state.path.append(.alarmCheck(.init(userType: .trainer))) return .none - case .addPTSessionPage: - state.path.append(.addPTSession(.init())) + case .addPTSessionPage(let date): + state.path.append(.addPTSession(.init(calendarSelectedDate: date))) return .none case .checkTrainerInvitationCode: state.path.append(.checkInvitationCode(.init())) diff --git a/TnT/Projects/Presentation/Sources/Home/Trainer/TrainerHomeFeature.swift b/TnT/Projects/Presentation/Sources/Home/Trainer/TrainerHomeFeature.swift index 489dd757..0f78b47f 100644 --- a/TnT/Projects/Presentation/Sources/Home/Trainer/TrainerHomeFeature.swift +++ b/TnT/Projects/Presentation/Sources/Home/Trainer/TrainerHomeFeature.swift @@ -150,13 +150,13 @@ public struct TrainerHomeFeature { return .none case .tapAddSessionButton: - return .run { send in + return .run { [state] send in let result: GetActiveTraineesListResDTO = try await trainerRepoUseCase.getActiveTraineesList() if result.trainees.isEmpty { return await send(.view(.popUpOfCheckTrainee)) } else { - return await send(.setNavigating(.addPTSessionPage)) + return await send(.setNavigating(.addPTSessionPage(selectedDate: state.selectedDate))) } } @@ -284,7 +284,7 @@ extension TrainerHomeFeature { /// 알림 페이지 case alarmPage /// PT 일정 추가페이지 - case addPTSessionPage + case addPTSessionPage(selectedDate: Date) /// 초대 코드 발급페이지 case trainerMakeInvitationCodePage /// 초대 코드 확인 페잊 From 951af1dd813058f64b084ba52e36b80ab863b242 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Sat, 15 Feb 2025 17:46:58 +0900 Subject: [PATCH 09/32] =?UTF-8?q?[Fix]=20=EC=84=9C=EB=B2=84=20=EB=B0=98?= =?UTF-8?q?=EC=B6=9C=20UI=20=ED=91=9C=EC=8B=9C=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Implements/ResponseValidator.swift | 16 ++++++++++++++-- .../Domain/Sources/DTO/EmptyResponse.swift | 4 ++++ .../TrainerAddPTSessionFeature.swift | 18 +++++++----------- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/TnT/Projects/Data/Sources/Network/Interceptor/Implements/ResponseValidator.swift b/TnT/Projects/Data/Sources/Network/Interceptor/Implements/ResponseValidator.swift index 7737cda7..a3c61386 100644 --- a/TnT/Projects/Data/Sources/Network/Interceptor/Implements/ResponseValidator.swift +++ b/TnT/Projects/Data/Sources/Network/Interceptor/Implements/ResponseValidator.swift @@ -8,6 +8,8 @@ import Foundation +import Domain + struct ResponseValidatorInterceptor: Interceptor { let priority: InterceptorPriority = .normal @@ -17,11 +19,18 @@ struct ResponseValidatorInterceptor: Interceptor { } let statusCode: Int = httpResponse.statusCode - let responseBody: String = String(data: data, encoding: .utf8) ?? "No Response" - switch statusCode { case 200..<300: return + default: + try throwError(with: data, statusCode: statusCode) + } + } + + private func throwError(with data: Data, statusCode: Int) throws { + let responseBody: String = try JSONDecoder().decode(ErrorResponse.self, from: data).message + + switch statusCode { case 400: throw NetworkError.badRequest(message: responseBody) @@ -33,6 +42,9 @@ struct ResponseValidatorInterceptor: Interceptor { case 404: throw NetworkError.notFound(message: responseBody) + + case 409: + throw NetworkError.conflict(message: responseBody) case 405..<500: throw NetworkError.clientError(statusCode: statusCode, message: responseBody) diff --git a/TnT/Projects/Domain/Sources/DTO/EmptyResponse.swift b/TnT/Projects/Domain/Sources/DTO/EmptyResponse.swift index 5f2392a7..b3310b26 100644 --- a/TnT/Projects/Domain/Sources/DTO/EmptyResponse.swift +++ b/TnT/Projects/Domain/Sources/DTO/EmptyResponse.swift @@ -12,3 +12,7 @@ import Foundation public struct EmptyResponse: Decodable { public init() {} } + +public struct ErrorResponse: Decodable { + public let message: String +} diff --git a/TnT/Projects/Presentation/Sources/AddPTSession/TrainerAddPTSessionFeature.swift b/TnT/Projects/Presentation/Sources/AddPTSession/TrainerAddPTSessionFeature.swift index d473413e..833f13df 100644 --- a/TnT/Projects/Presentation/Sources/AddPTSession/TrainerAddPTSessionFeature.swift +++ b/TnT/Projects/Presentation/Sources/AddPTSession/TrainerAddPTSessionFeature.swift @@ -275,18 +275,14 @@ public struct TrainerAddPTSessionFeature { else { return .none } return .run { send in - do { - let _ = try await trainerRepoUseCase.postLesson( - reqDTO: .init( - start: startDate, - end: endDate, - traineeId: traineeId - ) + let _ = try await trainerRepoUseCase.postLesson( + reqDTO: .init( + start: startDate, + end: endDate, + traineeId: traineeId ) - await send(.setPopUp(.sessionAdded)) - } catch { - NotificationCenter.default.post(toast: .init(presentType: .text("⚠"), message: "이미 예약된 시간대입니다")) - } + ) + await send(.setPopUp(.sessionAdded)) } } From ef2934491f2fd66822373ae4d7bc6d82b9197809 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Mon, 17 Feb 2025 00:37:15 +0900 Subject: [PATCH 10/32] =?UTF-8?q?[Feat]=20UINavigationController=20?= =?UTF-8?q?=EC=A0=9C=EC=8A=A4=EC=B2=98=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GestureNavigation/GestureNavigation.swift | 21 -------- .../NavigationGesture/NavigationGesture.swift | 53 +++++++++++++++++++ 2 files changed, 53 insertions(+), 21 deletions(-) delete mode 100644 TnT/Projects/Presentation/Sources/GestureNavigation/GestureNavigation.swift create mode 100644 TnT/Projects/Presentation/Sources/Utility/NavigationGesture/NavigationGesture.swift diff --git a/TnT/Projects/Presentation/Sources/GestureNavigation/GestureNavigation.swift b/TnT/Projects/Presentation/Sources/GestureNavigation/GestureNavigation.swift deleted file mode 100644 index d5bb5729..00000000 --- a/TnT/Projects/Presentation/Sources/GestureNavigation/GestureNavigation.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// GestureNavigation.swift -// Presentation -// -// Created by 박서연 on 2/13/25. -// Copyright © 2025 yapp25thTeamTnT. All rights reserved. -// - -import Foundation -import UIKit - -extension UINavigationController: ObservableObject, UIGestureRecognizerDelegate { - open override func viewDidLoad() { - super.viewDidLoad() - interactivePopGestureRecognizer?.delegate = self - } - - public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { - return viewControllers.count > 2 - } -} diff --git a/TnT/Projects/Presentation/Sources/Utility/NavigationGesture/NavigationGesture.swift b/TnT/Projects/Presentation/Sources/Utility/NavigationGesture/NavigationGesture.swift new file mode 100644 index 00000000..7e01b372 --- /dev/null +++ b/TnT/Projects/Presentation/Sources/Utility/NavigationGesture/NavigationGesture.swift @@ -0,0 +1,53 @@ +// +// NavigationGesture.swift +// Presentation +// +// Created by 박서연 on 2/13/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import Foundation +import UIKit +import SwiftUI + +extension UINavigationController: ObservableObject, UIGestureRecognizerDelegate { + static var gestureEnabled: Bool = true + + open override func viewDidLoad() { + super.viewDidLoad() + interactivePopGestureRecognizer?.delegate = self + } + + public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + return viewControllers.count > 2 && UINavigationController.gestureEnabled + } +} + +extension UIView { + var parentViewController: UIViewController? { + sequence(first: self) { + $0.next + }.first { $0 is UIViewController } as? UIViewController + } +} + +struct PopGestureModifier: ViewModifier { + let disabled: Bool + + func body(content: Content) -> some View { + content + .onAppear { + UINavigationController.gestureEnabled = !disabled + } + .onDisappear { + UINavigationController.gestureEnabled = true + } + } +} + +extension View { + /// 뷰가 실제 화면에 보일 때만 pop 제스처가 비활성화되고, 화면에서 사라지면 자동으로 제스처가 다시 활성화됩니다. + func navigationPopGestureDisabled(_ disabled: Bool = true) -> some View { + self.modifier(PopGestureModifier(disabled: disabled)) + } +} From 3aa3c3048fc352b71fd8d9c2d09f914fd3debeb2 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Mon, 17 Feb 2025 01:19:29 +0900 Subject: [PATCH 11/32] =?UTF-8?q?[Feat]=20Nav=20Gesture=20=EB=B9=84?= =?UTF-8?q?=ED=99=9C=EC=84=B1=20=EC=83=81=ED=83=9C=EB=A5=BC=20Int=EB=A1=9C?= =?UTF-8?q?=20=EA=B4=80=EB=A6=AC=20->=20gestureDisableCount=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NavigationGesture/NavigationGesture.swift | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/TnT/Projects/Presentation/Sources/Utility/NavigationGesture/NavigationGesture.swift b/TnT/Projects/Presentation/Sources/Utility/NavigationGesture/NavigationGesture.swift index 7e01b372..d56faeaf 100644 --- a/TnT/Projects/Presentation/Sources/Utility/NavigationGesture/NavigationGesture.swift +++ b/TnT/Projects/Presentation/Sources/Utility/NavigationGesture/NavigationGesture.swift @@ -10,8 +10,9 @@ import Foundation import UIKit import SwiftUI -extension UINavigationController: ObservableObject, UIGestureRecognizerDelegate { - static var gestureEnabled: Bool = true +extension UINavigationController: @retroactive ObservableObject, @retroactive UIGestureRecognizerDelegate { + /// 현재 제스처 비활성화된 화면의 수를 추적하는 카운터 + static var gestureDisabledCount: Int = 0 open override func viewDidLoad() { super.viewDidLoad() @@ -19,15 +20,7 @@ extension UINavigationController: ObservableObject, UIGestureRecognizerDelegate } public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { - return viewControllers.count > 2 && UINavigationController.gestureEnabled - } -} - -extension UIView { - var parentViewController: UIViewController? { - sequence(first: self) { - $0.next - }.first { $0 is UIViewController } as? UIViewController + return UINavigationController.gestureDisabledCount == 0 } } @@ -37,10 +30,10 @@ struct PopGestureModifier: ViewModifier { func body(content: Content) -> some View { content .onAppear { - UINavigationController.gestureEnabled = !disabled + UINavigationController.gestureDisabledCount += 1 } .onDisappear { - UINavigationController.gestureEnabled = true + UINavigationController.gestureDisabledCount -= 1 } } } From 1cbaf989450f1088bf2754502a36a2a86d311c55 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Mon, 17 Feb 2025 01:32:50 +0900 Subject: [PATCH 12/32] =?UTF-8?q?[Feat]=20=EC=A0=9C=EC=8A=A4=EC=B2=98=20?= =?UTF-8?q?=EC=BB=A8=ED=8A=B8=EB=A1=A4=20=ED=95=84=EC=9A=94=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EC=A0=84=EC=B2=B4=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/MainTab/Trainee/TraineeMainTabView.swift | 1 + .../Sources/MainTab/Trainer/TrainerMainTabView.swift | 1 + .../Presentation/Sources/Onboarding/Common/LoginView.swift | 1 + .../TraineeConnectionCompleteView.swift | 1 + .../TraineeInvitationCodeInputView.swift | 1 + .../TraineeProfileCompletion/ProfileCompletionView.swift | 1 + .../Onboarding/Trainer/Login/Code/MakeInvitationCodeView.swift | 1 + .../Trainer/Login/Connection/ConnectionCompleteView.swift | 1 + .../Trainer/Login/Profile/TrainerSignUpCompleteView.swift | 1 + .../Sources/Utility/NavigationGesture/NavigationGesture.swift | 2 ++ 10 files changed, 11 insertions(+) diff --git a/TnT/Projects/Presentation/Sources/MainTab/Trainee/TraineeMainTabView.swift b/TnT/Projects/Presentation/Sources/MainTab/Trainee/TraineeMainTabView.swift index 50b033b5..d537f12b 100644 --- a/TnT/Projects/Presentation/Sources/MainTab/Trainee/TraineeMainTabView.swift +++ b/TnT/Projects/Presentation/Sources/MainTab/Trainee/TraineeMainTabView.swift @@ -34,6 +34,7 @@ public struct TraineeMainTabView: View { BottomTabBar() } + .navigationPopGestureDisabled() .ignoresSafeArea(.all, edges: .bottom) } diff --git a/TnT/Projects/Presentation/Sources/MainTab/Trainer/TrainerMainTabView.swift b/TnT/Projects/Presentation/Sources/MainTab/Trainer/TrainerMainTabView.swift index c1d39a43..f49382b9 100644 --- a/TnT/Projects/Presentation/Sources/MainTab/Trainer/TrainerMainTabView.swift +++ b/TnT/Projects/Presentation/Sources/MainTab/Trainer/TrainerMainTabView.swift @@ -42,6 +42,7 @@ public struct TrainerMainTabView: View { BottomTabBar() } + .navigationPopGestureDisabled() .ignoresSafeArea(.all, edges: .bottom) } diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Common/LoginView.swift b/TnT/Projects/Presentation/Sources/Onboarding/Common/LoginView.swift index 4de2321f..416f7e8b 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Common/LoginView.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Common/LoginView.swift @@ -36,6 +36,7 @@ public struct LoginView: View { } .padding(.horizontal, 28) .navigationBarBackButtonHidden() + .navigationPopGestureDisabled() .sheet(item: $store.scope(state: \.termFeature, action: \.subFeature.termAction)) { store in TermView(store: store) .padding(.top, 10) diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeConnectionComplete/TraineeConnectionCompleteView.swift b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeConnectionComplete/TraineeConnectionCompleteView.swift index 04dfab9d..95fb6c4a 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeConnectionComplete/TraineeConnectionCompleteView.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeConnectionComplete/TraineeConnectionCompleteView.swift @@ -51,6 +51,7 @@ public struct TraineeConnectionCompleteView: View { } } .navigationBarBackButtonHidden() + .navigationPopGestureDisabled() } // MARK: - Sections diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeInvitationCodeInput/TraineeInvitationCodeInputView.swift b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeInvitationCodeInput/TraineeInvitationCodeInputView.swift index e360df6f..e7d64e2b 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeInvitationCodeInput/TraineeInvitationCodeInputView.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeInvitationCodeInput/TraineeInvitationCodeInputView.swift @@ -38,6 +38,7 @@ public struct TraineeInvitationCodeInputView: View { Spacer() } .navigationBarBackButtonHidden() + .navigationPopGestureDisabled(store.view_navigationType == .newUser) .keyboardDismissOnTap() .bottomFixWith { TBottomButton( diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeProfileCompletion/ProfileCompletionView.swift b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeProfileCompletion/ProfileCompletionView.swift index 250129cf..59e6f4c1 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeProfileCompletion/ProfileCompletionView.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeProfileCompletion/ProfileCompletionView.swift @@ -38,6 +38,7 @@ public struct ProfileCompletionView: View { Spacer() } .navigationBarBackButtonHidden() + .navigationPopGestureDisabled() .keyboardDismissOnTap() .bottomFixWith { TBottomButton( 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 852374e6..3b4d5983 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() + .navigationPopGestureDisabled() .onAppear { send(.onAppear) } } 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 9dc00f9c..b08c3fde 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Trainer/Login/Connection/ConnectionCompleteView.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Trainer/Login/Connection/ConnectionCompleteView.swift @@ -55,6 +55,7 @@ public struct ConnectionCompleteView: View { } .onAppear { send(.onAppear) } .navigationBarBackButtonHidden() + .navigationPopGestureDisabled() } @ViewBuilder diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Trainer/Login/Profile/TrainerSignUpCompleteView.swift b/TnT/Projects/Presentation/Sources/Onboarding/Trainer/Login/Profile/TrainerSignUpCompleteView.swift index 3431d2ed..118472ee 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Trainer/Login/Profile/TrainerSignUpCompleteView.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Trainer/Login/Profile/TrainerSignUpCompleteView.swift @@ -32,6 +32,7 @@ public struct TrainerSignUpCompleteView: View { } } .navigationBarBackButtonHidden() + .navigationPopGestureDisabled() } @ViewBuilder diff --git a/TnT/Projects/Presentation/Sources/Utility/NavigationGesture/NavigationGesture.swift b/TnT/Projects/Presentation/Sources/Utility/NavigationGesture/NavigationGesture.swift index d56faeaf..3f2154e6 100644 --- a/TnT/Projects/Presentation/Sources/Utility/NavigationGesture/NavigationGesture.swift +++ b/TnT/Projects/Presentation/Sources/Utility/NavigationGesture/NavigationGesture.swift @@ -30,9 +30,11 @@ struct PopGestureModifier: ViewModifier { func body(content: Content) -> some View { content .onAppear { + guard disabled else { return } UINavigationController.gestureDisabledCount += 1 } .onDisappear { + guard disabled else { return } UINavigationController.gestureDisabledCount -= 1 } } From ffbd35638f6ad2bbeb91b9bd94534b1411b1c432 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Mon, 17 Feb 2025 02:23:21 +0900 Subject: [PATCH 13/32] =?UTF-8?q?[Feat]=20=ED=8A=B8=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EB=84=88/=ED=8A=B8=EB=A0=88=EC=9D=B4=EB=8B=88=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0=20=EC=97=AC=EB=B6=80=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/MyPage/Trainee/TraineeMyPageFeature.swift | 1 + .../Sources/MyPage/Trainer/TrainerMypageFeature.swift | 3 +++ .../TraineeConnectionCompleteFeature.swift | 8 ++++++++ .../TraineeConnectionCompleteView.swift | 1 + .../Login/Connection/ConnectionCompleteFeature.swift | 3 +++ 5 files changed, 16 insertions(+) diff --git a/TnT/Projects/Presentation/Sources/MyPage/Trainee/TraineeMyPageFeature.swift b/TnT/Projects/Presentation/Sources/MyPage/Trainee/TraineeMyPageFeature.swift index 18a46dab..60f79f8c 100644 --- a/TnT/Projects/Presentation/Sources/MyPage/Trainee/TraineeMyPageFeature.swift +++ b/TnT/Projects/Presentation/Sources/MyPage/Trainee/TraineeMyPageFeature.swift @@ -204,6 +204,7 @@ public struct TraineeMyPageFeature { case .logoutCompleted, .withdrawCompleted: state.$hidePopupUntil.withLock { $0 = nil } + state.$isConnected.withLock { $0 = false } return .concatenate( .send(.setPopUpStatus(nil)), .send(.setNavigating(.onboardingLogin)) diff --git a/TnT/Projects/Presentation/Sources/MyPage/Trainer/TrainerMypageFeature.swift b/TnT/Projects/Presentation/Sources/MyPage/Trainer/TrainerMypageFeature.swift index 8dad7367..261d513a 100644 --- a/TnT/Projects/Presentation/Sources/MyPage/Trainer/TrainerMypageFeature.swift +++ b/TnT/Projects/Presentation/Sources/MyPage/Trainer/TrainerMypageFeature.swift @@ -19,6 +19,8 @@ public struct TrainerMypageFeature { public struct State: Equatable { /// 3일 동안 보지 않기 시작 날짜 @Shared(.appStorage(AppStorage.hideHomePopupUntil)) var hidePopupUntil: Date? + /// 트레이니 연결 여부 + @Shared(.appStorage(AppStorage.isConnected)) var isConnected: Bool = false /// 사용자 이름 var userName: String /// 사용자 이미지 URL @@ -164,6 +166,7 @@ public struct TrainerMypageFeature { case .logoutCompleted, .withdrawCompleted: state.$hidePopupUntil.withLock { $0 = nil } + state.$isConnected.withLock { $0 = false } return .concatenate( .send(.setPopUpStatus(nil)), .send(.setNavigating(.onboardingLogin)) diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeConnectionComplete/TraineeConnectionCompleteFeature.swift b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeConnectionComplete/TraineeConnectionCompleteFeature.swift index 3c5870b7..fa39087e 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeConnectionComplete/TraineeConnectionCompleteFeature.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeConnectionComplete/TraineeConnectionCompleteFeature.swift @@ -18,6 +18,8 @@ public struct TraineeConnectionCompleteFeature { @ObservableState public struct State: Equatable { // MARK: Data related state + /// 연결 여부 + @Shared(.appStorage(AppStorage.isConnected)) var isConnected: Bool = true /// 현재 사용자 유저 타입 (트레이너/트레이니) var userType: UserType /// 트레이니 사용자 이름 @@ -78,6 +80,8 @@ public struct TraineeConnectionCompleteFeature { case binding(BindingAction) /// "다음으로" 버튼이 눌렸을 때 case tapNextButton + /// 화면이 표시되었을 때 + case onAppear } } @@ -95,6 +99,10 @@ public struct TraineeConnectionCompleteFeature { case .tapNextButton: return .send(.setNavigating) + + case .onAppear: + state.$isConnected.withLock { $0 = true } + return .none } case .setNavigating: diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeConnectionComplete/TraineeConnectionCompleteView.swift b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeConnectionComplete/TraineeConnectionCompleteView.swift index 95fb6c4a..6347f80a 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeConnectionComplete/TraineeConnectionCompleteView.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineeConnectionComplete/TraineeConnectionCompleteView.swift @@ -52,6 +52,7 @@ public struct TraineeConnectionCompleteView: View { } .navigationBarBackButtonHidden() .navigationPopGestureDisabled() + .onAppear { send(.onAppear) } } // MARK: - Sections 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 6eb45c0f..2316ecaf 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Trainer/Login/Connection/ConnectionCompleteFeature.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Trainer/Login/Connection/ConnectionCompleteFeature.swift @@ -15,6 +15,8 @@ import Domain public struct ConnectionCompleteFeature { @ObservableState public struct State: Equatable { + /// 연결 여부 + @Shared(.appStorage(AppStorage.isConnected)) var isConnected: Bool = true var traineeId: Int64? var trainerId: Int64? var connectionInfo: ConnectionInfoEntity? @@ -66,6 +68,7 @@ public struct ConnectionCompleteFeature { return .send(.setNavigating(profile)) case .onAppear: + state.$isConnected.withLock { $0 = true } return .send(.api(.getConnectedTraineeInfo)) } From 659f68b5d28995d84bffd5e21d6a82a605cc1788 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Mon, 17 Feb 2025 02:41:54 +0900 Subject: [PATCH 14/32] =?UTF-8?q?[Fix]=20=EC=8B=9D=EB=8B=A8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EC=9C=A0=ED=9A=A8=20=EA=B2=80=EC=82=AC=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TraineeAddDietRecordFeature.swift | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordFeature.swift b/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordFeature.swift index ac23e5b3..a3b4a178 100644 --- a/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordFeature.swift +++ b/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordFeature.swift @@ -282,12 +282,16 @@ private extension TraineeAddDietRecordFeature { /// 모든 필드의 상태를 검증하여 "다음" 버튼 활성화 여부를 결정 func validateAllFields(_ state: inout State) -> Effect { - guard state.dietDate != nil else { return .none } - guard state.dietTime != nil else { return .none } - guard let date = combinedDietDateTime(date: state.dietDate, time: state.dietTime), - date <= .now else { return .none } - guard state.dietType != nil else { return .none } - guard !state.dietInfo.isEmpty && state.dietInfo.count <= 100 else { return .none } + guard state.dietDate != nil, + state.dietTime != nil, + let date = combinedDietDateTime(date: state.dietDate, time: state.dietTime), + date <= .now, + state.dietType != nil, + !state.dietInfo.isEmpty && state.dietInfo.count <= 100 + else { + state.view_isSubmitButtonEnabled = false + return .none + } state.view_isSubmitButtonEnabled = true return .none From edcf1d4dcfc58f31c9674e0b49bab935f5ba83a0 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Mon, 17 Feb 2025 03:41:58 +0900 Subject: [PATCH 15/32] =?UTF-8?q?[Feat]=20=ED=99=88=20=EC=A7=84=EC=9E=85?= =?UTF-8?q?=EC=8B=9C=20=EC=84=B8=EC=85=98=20=EC=B2=B4=ED=81=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Home/Trainee/TraineeHomeFeature.swift | 23 ++++++++++++++----- .../Home/Trainer/TrainerHomeFeature.swift | 18 +++++++++------ 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/TnT/Projects/Presentation/Sources/Home/Trainee/TraineeHomeFeature.swift b/TnT/Projects/Presentation/Sources/Home/Trainee/TraineeHomeFeature.swift index cc600258..335243e4 100644 --- a/TnT/Projects/Presentation/Sources/Home/Trainee/TraineeHomeFeature.swift +++ b/TnT/Projects/Presentation/Sources/Home/Trainee/TraineeHomeFeature.swift @@ -76,8 +76,9 @@ public struct TraineeHomeFeature { } } + @Dependency(\.userUseRepoCase) private var userUseRepoCase: UserRepository @Dependency(\.traineeUseCase) private var traineeUseCase: TraineeUseCase - @Dependency(\.traineeRepoUseCase) private var traineeRepoUseCase + @Dependency(\.traineeRepoUseCase) private var traineeRepoUseCase: TraineeRepository public enum Action: Equatable, Sendable, ViewAction { /// 뷰에서 발생한 액션을 처리합니다. @@ -90,6 +91,8 @@ public struct TraineeHomeFeature { case setContent(session: WorkoutListItemEntity?, records: [RecordListItemEntity]) /// 팝업 표시 처리 case showPopUp + /// 화면이 표시될 때 - 세션 체크 이후 + case onAppearAfterSessionCheck(isConnected: Bool) /// 네비게이션 여부 설정 case setNavigating(RoutingScreen) @@ -206,11 +209,11 @@ public struct TraineeHomeFeature { return .send(.setNavigating(.traineeInvitationCodeInput)) case .onAppear: - return .concatenate( - .send(.showPopUp), - currentPageUpdated(state: &state), - .send(.api(.getActiveDateDetail(date: state.selectedDate))) - ) + return .run { send in + if let result = try? await userUseRepoCase.getSessionCheck() { + await send(.onAppearAfterSessionCheck(isConnected: result.isConnected)) + } + } } case .api(let action): @@ -257,6 +260,14 @@ public struct TraineeHomeFeature { state.view_isPopUpPresented = !hidePopUp return .none + case .onAppearAfterSessionCheck(let isConnected): + state.$isConnected.withLock { $0 = isConnected } + return .concatenate( + .send(.showPopUp), + currentPageUpdated(state: &state), + .send(.api(.getActiveDateDetail(date: state.selectedDate))) + ) + case .setNavigating: return .none } diff --git a/TnT/Projects/Presentation/Sources/Home/Trainer/TrainerHomeFeature.swift b/TnT/Projects/Presentation/Sources/Home/Trainer/TrainerHomeFeature.swift index 0f78b47f..73bcc1cf 100644 --- a/TnT/Projects/Presentation/Sources/Home/Trainer/TrainerHomeFeature.swift +++ b/TnT/Projects/Presentation/Sources/Home/Trainer/TrainerHomeFeature.swift @@ -74,6 +74,7 @@ public struct TrainerHomeFeature { } } + @Dependency(\.userUseRepoCase) private var userUseRepoCase: UserRepository @Dependency(\.traineeUseCase) private var traineeUseCase: TraineeUseCase @Dependency(\.trainerRepoUseCase) private var trainerRepoUseCase: TrainerRepository @@ -101,6 +102,8 @@ public struct TrainerHomeFeature { case tapPopUpConnectButton /// 화면이 표시될 때 case onAppear + /// 화면이 표시될 때 - 세션 체크 이후 + case onAppearAfterSessionCheck(isConnected: Bool) /// events 타입에 맞춰서 달력 스케줄 캐수 표시 데이터 계산 case fetchMonthlyLessons(year: Int, month: Int) /// 달력 스케줄 캐수 표시 데이터 업데이트 @@ -188,6 +191,14 @@ public struct TrainerHomeFeature { return .send(.setNavigating(.checkTrainerInvitationCode)) case .onAppear: + return .run { send in + if let result = try? await userUseRepoCase.getSessionCheck() { + await send(.view(.onAppearAfterSessionCheck(isConnected: result.isConnected))) + } + } + + case .onAppearAfterSessionCheck(let isConnected): + state.$isConnected.withLock { $0 = isConnected } let year: Int = Calendar.current.component(.year, from: state.selectedDate) let month: Int = Calendar.current.component(.month, from: state.selectedDate) @@ -196,13 +207,6 @@ public struct TrainerHomeFeature { state.view_isPopUpPresented = !hidePopUp state.popUpFlag = !hidePopUp -// if let hideUntil = state.hidePopupUntil, hideUntil > Date() { -// state.view_isPopUpPresented = false -// } else { -// state.popUpFlag = true -// state.view_isPopUpPresented = true -// } - return .concatenate( .send(.view(.fetchMonthlyLessons(year: month == 1 ? year-1 : year, month: month == 1 ? 12 : month-1))), .send(.view(.fetchMonthlyLessons(year: year, month: month))), From 2fca0ac468a077cbf277d693810cff4576ee85a7 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Mon, 17 Feb 2025 05:46:27 +0900 Subject: [PATCH 16/32] =?UTF-8?q?[Feat]=20PhotoPicker=20Wrapper=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 --- .../PhotoPicker/PhotoLibraryFeature.swift | 74 +++++++++++++++++++ .../Utility/PhotoPicker/PhotoPickerView.swift | 46 ++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 TnT/Projects/Presentation/Sources/Utility/PhotoPicker/PhotoLibraryFeature.swift create mode 100644 TnT/Projects/Presentation/Sources/Utility/PhotoPicker/PhotoPickerView.swift diff --git a/TnT/Projects/Presentation/Sources/Utility/PhotoPicker/PhotoLibraryFeature.swift b/TnT/Projects/Presentation/Sources/Utility/PhotoPicker/PhotoLibraryFeature.swift new file mode 100644 index 00000000..522ca230 --- /dev/null +++ b/TnT/Projects/Presentation/Sources/Utility/PhotoPicker/PhotoLibraryFeature.swift @@ -0,0 +1,74 @@ +// +// PhotoLibraryFeature.swift +// Presentation +// +// Created by 박민서 on 2/17/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import Photos +import UIKit +import ComposableArchitecture + +@Reducer +public struct PhotoLibraryFeature { + @ObservableState + public struct State: Equatable { + /// 접근 권한 허용 여부 + var isAuthorized: Bool = false + } + + public enum Action: Equatable, Sendable { + /// 권한 체크 + case checkPermission + /// 권한 요청 + case requestPermission + /// 권한 상태 반영 + case setAuthorizedStatus(Bool) + /// 권한 관련 팝업 표시 + case showPermissionPopup + } + + public init() {} + + public var body: some ReducerOf { + Reduce { state, action in + switch action { + + case .checkPermission: + let status = PHPhotoLibrary.authorizationStatus(for: .readWrite) + let authorized = (status == .authorized || status == .limited) + return .send(.setAuthorizedStatus(authorized)) + + case .requestPermission: + return .run { send in + let status = await requestPhotoAuthorization() + let authorized = (status == .authorized || status == .limited) + if authorized { + await send(.setAuthorizedStatus(authorized)) + } else { + await send(.showPermissionPopup) + } + } + + case let .setAuthorizedStatus(authorized): + state.isAuthorized = authorized + return .none + + case .showPermissionPopup: + return .none + } + } + } +} + +extension PhotoLibraryFeature { + /// 권한 요청 진행 + func requestPhotoAuthorization() async -> PHAuthorizationStatus { + return await withCheckedContinuation { continuation in + PHPhotoLibrary.requestAuthorization(for: .readWrite) { newStatus in + continuation.resume(returning: newStatus) + } + } + } +} diff --git a/TnT/Projects/Presentation/Sources/Utility/PhotoPicker/PhotoPickerView.swift b/TnT/Projects/Presentation/Sources/Utility/PhotoPicker/PhotoPickerView.swift new file mode 100644 index 00000000..d1d1e312 --- /dev/null +++ b/TnT/Projects/Presentation/Sources/Utility/PhotoPicker/PhotoPickerView.swift @@ -0,0 +1,46 @@ +// +// PhotoPickerView.swift +// Presentation +// +// Created by 박민서 on 2/17/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import SwiftUI +import PhotosUI +import ComposableArchitecture + +public struct PhotoPickerView: View { + + private let store: StoreOf + @Binding private var selectedItem: PhotosPickerItem? + private let content: () -> Content + + public init( + store: StoreOf, + selectedItem: Binding, + content: @escaping () -> Content + ) { + self.store = store + self._selectedItem = selectedItem + self.content = content + } + + public var body: some View { + ZStack { + PhotosPicker(selection: $selectedItem, matching: .images) { + content() + } + + if !store.isAuthorized { + Color.black.opacity(0.00001) + .onTapGesture { + store.send(.requestPermission) + } + } + } + .onAppear { + store.send(.checkPermission) + } + } +} From c0bd3f3082da1fd9f71dd043440e6c63fca8d3e9 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Mon, 17 Feb 2025 05:46:44 +0900 Subject: [PATCH 17/32] =?UTF-8?q?[Feat]=20=EC=82=AC=EC=A7=84=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20info.plist=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TnT/Tuist/Config/Info.plist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TnT/Tuist/Config/Info.plist b/TnT/Tuist/Config/Info.plist index b5d2bf8c..344d06c3 100644 --- a/TnT/Tuist/Config/Info.plist +++ b/TnT/Tuist/Config/Info.plist @@ -90,5 +90,7 @@ UIUserInterfaceStyle Light + NSPhotoLibraryUsageDescription + 앱이 사진 라이브러리에 접근하여 사진을 선택할 수 있도록 허용해주세요. From 85de89daddd0780c19c27d1d6bae4db9d1512399 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Mon, 17 Feb 2025 06:15:20 +0900 Subject: [PATCH 18/32] =?UTF-8?q?[Feat]=20TraineeAddDietRecord=20PhotoPick?= =?UTF-8?q?erView=20=EB=A1=9C=EC=A7=81=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TraineeAddDietRecordFeature.swift | 56 +++++++++++++++---- .../TraineeAddDietRecordView.swift | 13 ++--- 2 files changed, 50 insertions(+), 19 deletions(-) diff --git a/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordFeature.swift b/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordFeature.swift index a3b4a178..772d253c 100644 --- a/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordFeature.swift +++ b/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordFeature.swift @@ -52,6 +52,9 @@ public struct TraineeAddDietRecordFeature { /// 팝업 표시 여부 var view_isPopUpPresented: Bool + // MARK: SubFeature state + var photoLibraryState = PhotoLibraryFeature.State() + public init( calendarSelectedDate: Date = .now, dietDate: Date? = nil, @@ -95,6 +98,8 @@ public struct TraineeAddDietRecordFeature { case view(View) /// api 콜 액션을 처리합니다 case api(APIAction) + /// 하위 피처 액션을 처리합니다 + case subFeature(SubFeatureAction) /// 선택된 이미지 데이터 저장 case imagePicked(Data?) /// 팝업 상태 설정 @@ -135,13 +140,23 @@ public struct TraineeAddDietRecordFeature { /// 식단 등록 API case registerDietRecord } + + @CasePathable + public enum SubFeatureAction: Sendable { + case photoLibrary(PhotoLibraryFeature.Action) + } } public init() {} public var body: some ReducerOf { - BindingReducer(action: \.view) + Scope(state: \.photoLibraryState, action: \.subFeature.photoLibrary) { + PhotoLibraryFeature() + } + + BindingReducer(action: \.view) + Reduce { state, action in switch action { case .view(let action): @@ -218,6 +233,10 @@ public struct TraineeAddDietRecordFeature { case .tapPopUpSecondaryButton(let popUp): guard popUp != nil else { return .none } + guard popUp != .photoAuthorization else { + return setPopUpStatus(&state, status: nil) + } + return .concatenate( setPopUpStatus(&state, status: nil), .run{ _ in await self.dismiss() } @@ -225,6 +244,13 @@ public struct TraineeAddDietRecordFeature { case .tapPopUpPrimaryButton(let popUp): guard popUp != nil else { return .none } + + if popUp == .photoAuthorization, + let url = URL(string: UIApplication.openSettingsURLString), + UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url) + } + return popUp == .dietAdded ? .send(.setNavigating) : setPopUpStatus(&state, status: nil) @@ -258,6 +284,15 @@ public struct TraineeAddDietRecordFeature { } } + case .subFeature(let internalAction): + switch internalAction { + case .photoLibrary(.showPermissionPopup): + return setPopUpStatus(&state, status: .photoAuthorization) + + default: + return .none + } + case .imagePicked(let imgData): state.dietImageData = imgData return self.validateAllFields(&state) @@ -358,6 +393,8 @@ public extension TraineeAddDietRecordFeature { case dietAdded /// 식단 기록을 종료할까요? case cancelDietAdd + /// 사진 접근 권한이 필요해요 + case photoAuthorization var title: String { switch self { @@ -365,6 +402,8 @@ public extension TraineeAddDietRecordFeature { return "식단을 기록했어요!" case .cancelDietAdd: return "식단 기록을 종료할까요?" + case .photoAuthorization: + return "식단 사진 설정을 위해\n사진 접근 권한이 필요해요" } } @@ -374,12 +413,14 @@ public extension TraineeAddDietRecordFeature { return "내일도 기록해 주실 거죠?" case .cancelDietAdd: return "기록이 저장되지 않아요!" + case .photoAuthorization: + return "사진 추가는 식단 기록 말고도\n프로필과 운동 기록에도 사용돼요" } } var showAlertIcon: Bool { switch self { - case .dietAdded: + case .dietAdded, .photoAuthorization: return false case .cancelDietAdd: return true @@ -390,20 +431,11 @@ public extension TraineeAddDietRecordFeature { switch self { case .dietAdded: return nil - case .cancelDietAdd: + case .cancelDietAdd, .photoAuthorization: return .tapPopUpSecondaryButton(popUp: self) } } - var primaryTitle: String { - switch self { - case .dietAdded: - return "확인" - case .cancelDietAdd: - return "계속 수정" - } - } - var primaryAction: Action.View { return .tapPopUpPrimaryButton(popUp: self) } diff --git a/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordView.swift b/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordView.swift index ff514c88..ede52ce8 100644 --- a/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordView.swift +++ b/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordView.swift @@ -116,11 +116,10 @@ public struct TraineeAddDietRecordView: View { // MARK: - Sections @ViewBuilder private func DietPhotoSection() -> some View { - PhotosPicker( - selection: $store.view_photoPickerItem, - matching: .images, - photoLibrary: .shared() - ) { + PhotoPickerView(store: store.scope( + state: \.photoLibraryState, + action: \.subFeature.photoLibrary + ), selectedItem: $store.view_photoPickerItem) { GeometryReader { geometry in if let imageData = store.dietImageData, let uiImage = UIImage(data: imageData) { @@ -269,9 +268,9 @@ public struct TraineeAddDietRecordView: View { if let popUp = store.view_popUp { let buttons: [TPopupAlertState.ButtonState] = [ popUp.secondaryAction.map { action in - .init(title: "종료", style: .secondary, action: .init(action: { send(action) })) + .init(title: "취소", style: .secondary, action: .init(action: { send(action) })) }, - .init(title: popUp.primaryTitle, style: .primary, action: .init(action: { send(popUp.primaryAction) })) + .init(title: "확인", style: .primary, action: .init(action: { send(popUp.primaryAction) })) ].compactMap { $0 } TPopUpAlertView( From 47fef38d42c27d009234f4af048e2a00dba128c5 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Mon, 17 Feb 2025 06:15:37 +0900 Subject: [PATCH 19/32] =?UTF-8?q?[Feat]=20CreateProfile=20=20PhotoPickerVi?= =?UTF-8?q?ew=20=EB=A1=9C=EC=A7=81=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CreateProfile/CreateProfileFeature.swift | 138 +++++++++++++++--- .../CreateProfile/CreateProfileView.swift | 35 ++++- 2 files changed, 148 insertions(+), 25 deletions(-) diff --git a/TnT/Projects/Presentation/Sources/Onboarding/CreateProfile/CreateProfileFeature.swift b/TnT/Projects/Presentation/Sources/Onboarding/CreateProfile/CreateProfileFeature.swift index 0a754b70..07cc94d6 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/CreateProfile/CreateProfileFeature.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/CreateProfile/CreateProfileFeature.swift @@ -48,6 +48,13 @@ public struct CreateProfileFeature { } /// 유저의 최대 이름 길이 var view_nameMaxLength: Int? + /// 표시되는 팝업 + var view_popUp: PopUp? + /// 팝업 표시 여부 + var view_isPopUpPresented: Bool + + // MARK: SubFeature state + var photoLibraryState = PhotoLibraryFeature.State() /// `CreateProfileFeature.State`의 생성자 /// - Parameters: @@ -74,7 +81,9 @@ public struct CreateProfileFeature { view_isNextButtonEnabled: Bool = false, view_isNavigating: Bool = false, view_photoPickerItem: PhotosPickerItem? = nil, - view_nameMaxLength: Int? = nil + view_nameMaxLength: Int? = nil, + view_popUp: PopUp? = nil, + view_isPopUpPresented: Bool = false ) { self._signUpEntity = signUpEntity self.userType = userType @@ -87,6 +96,8 @@ public struct CreateProfileFeature { self.view_isNavigating = view_isNavigating self.view_photoPickerItem = view_photoPickerItem self.view_nameMaxLength = view_nameMaxLength + self.view_popUp = view_popUp + self.view_isPopUpPresented = view_isPopUpPresented } } @@ -95,14 +106,16 @@ public struct CreateProfileFeature { @Dependency(\.keyChainManager) var keyChainManager public enum Action: Sendable, ViewAction { - /// 네비게이션 여부 설정 - case setNavigating(RoutingScreen) - /// 선택된 이미지 데이터 저장 - case imagePicked(Data?) /// 뷰에서 발생한 액션을 처리합니다. case view(View) - /// 회원가입 POST - case postSignUp + /// api 콜 액션을 처리합니다 + case api(APIAction) + /// 하위 피처 액션을 처리합니다 + case subFeature(SubFeatureAction) + /// 선택된 이미지 데이터 저장 + case imagePicked(Data?) + /// 네비게이션 여부 설정 + case setNavigating(RoutingScreen) @CasePathable public enum View: Sendable, BindableAction { @@ -112,12 +125,32 @@ public struct CreateProfileFeature { case tapWriteButton /// "다음으로" 버튼이 눌렸을 때 case tapNextButton + /// 팝업 좌측 secondary 버튼 탭 + case tapPopUpSecondaryButton(popUp: PopUp?) + /// 팝업 우측 primary 버튼 탭 + case tapPopUpPrimaryButton(popUp: PopUp?) + } + + @CasePathable + public enum APIAction: Sendable { + /// 회원가입 POST + case postSignUp + } + + @CasePathable + public enum SubFeatureAction: Sendable { + case photoLibrary(PhotoLibraryFeature.Action) } } public init() {} public var body: some ReducerOf { + + Scope(state: \.photoLibraryState, action: \.subFeature.photoLibrary) { + PhotoLibraryFeature() + } + BindingReducer(action: \.view) Reduce { state, action in @@ -149,28 +182,56 @@ public struct CreateProfileFeature { case .trainee: return .send(.setNavigating(.traineeBasicInfoInput)) case .trainer: - return .send(.postSignUp) + return .send(.api(.postSignUp)) } - } - - case .postSignUp: - guard let reqDTO = state.signUpEntity.toDTO() else { + + case .tapPopUpSecondaryButton(let popUp): + guard popUp != nil else { return .none } + return setPopUpStatus(&state, status: nil) + + + case .tapPopUpPrimaryButton(let popUp): + guard popUp != nil else { return .none } + + if popUp == .photoAuthorization, + let url = URL(string: UIApplication.openSettingsURLString), + UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url) + } + return .none } - let imgData = state.signUpEntity.imageData - return .run { send in - let result = try await userUseRepoCase.postSignUp(reqDTO, profileImage: imgData).toEntity() - saveSessionId(result.sessionId) - await send(.setNavigating(.trainerSignUpComplete(result))) + case .api(let action): + switch action { + 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() + saveSessionId(result.sessionId) + await send(.setNavigating(.trainerSignUpComplete(result))) + } } - case .setNavigating: - return .none + case .subFeature(let internalAction): + switch internalAction { + case .photoLibrary(.showPermissionPopup): + return setPopUpStatus(&state, status: .photoAuthorization) + + default: + return .none + } case .imagePicked(let imgData): state.userImageData = imgData return .none + + case .setNavigating: + return .none } } } @@ -200,6 +261,14 @@ private extension CreateProfileFeature { print("로그인 정보 저장 싪패") } } + + /// 팝업 상태, 표시 상태를 업데이트 + /// status nil 입력인 경우 팝업 표시 해제 + func setPopUpStatus(_ state: inout State, status: PopUp?) -> Effect { + state.view_popUp = status + state.view_isPopUpPresented = status != nil + return .none + } } extension CreateProfileFeature { @@ -211,3 +280,34 @@ extension CreateProfileFeature { case trainerSignUpComplete(PostSignUpResEntity) } } + +// MARK: PopUp +public extension CreateProfileFeature { + /// 본 화면에 팝업으로 표시되는 목록 + enum PopUp: Equatable, Sendable { + /// 사진 접근 권한이 필요해요 + case photoAuthorization + + var title: String { + switch self { + case .photoAuthorization: + return "프로필 사진 설정을 위해\n사진 접근 권한이 필요해요" + } + } + + var message: String { + switch self { + case .photoAuthorization: + return "사진 추가는 프로필 말고도\n운동과 식단 기록에도 사용돼요" + } + } + + var secondaryAction: Action.View { + return .tapPopUpSecondaryButton(popUp: self) + } + + var primaryAction: Action.View { + return .tapPopUpPrimaryButton(popUp: self) + } + } +} diff --git a/TnT/Projects/Presentation/Sources/Onboarding/CreateProfile/CreateProfileView.swift b/TnT/Projects/Presentation/Sources/Onboarding/CreateProfile/CreateProfileView.swift index 37ba77be..91bf5e9e 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/CreateProfile/CreateProfileView.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/CreateProfile/CreateProfileView.swift @@ -56,6 +56,9 @@ public struct CreateProfileView: View { .disabled(!store.view_isNextButtonEnabled) .debounce() } + .tPopUp(isPresented: $store.view_isPopUpPresented) { + PopUpView() + } } @ViewBuilder @@ -98,20 +101,20 @@ public struct CreateProfileView: View { } .frame(width: 132, height: 132) .overlay(alignment: .bottomTrailing) { - PhotosPicker( - selection: $store.view_photoPickerItem, - matching: .images, - photoLibrary: .shared() - ) { + PhotoPickerView(store: store.scope( + state: \.photoLibraryState, + action: \.subFeature.photoLibrary + ), selectedItem: $store.view_photoPickerItem) { ZStack { Circle() .fill(Color.neutral900) - .frame(width: 28, height: 28) + Image(.icnWriteWhite) .resizable() .frame(width: 16, height: 16) } } + .frame(width: 28, height: 28) } } @@ -138,4 +141,24 @@ public struct CreateProfileView: View { ) .padding(.horizontal, 20) } + + @ViewBuilder + private func PopUpView() -> some View { + if let popUp = store.view_popUp { + let buttons: [TPopupAlertState.ButtonState] = [ + .init(title: "취소", style: .secondary, action: .init(action: { send(popUp.secondaryAction) })), + .init(title: "확인", style: .primary, action: .init(action: { send(popUp.primaryAction) })) + ] + + TPopUpAlertView( + alertState: .init( + title: popUp.title, + message: popUp.message, + buttons: buttons + ) + ) + } else { + EmptyView() + } + } } From 61d0f18e3ab5302ecbaf5aaee2620a01acf53a09 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Mon, 17 Feb 2025 06:18:28 +0900 Subject: [PATCH 20/32] =?UTF-8?q?[Fix]=20=EC=8B=9D=EB=8B=A8=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=20=EC=9E=91=EC=84=B1=20=ED=9B=84=20nav=20back=20?= =?UTF-8?q?=EC=8B=9C=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AddDietRecord/TraineeAddDietRecordFeature.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordFeature.swift b/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordFeature.swift index 772d253c..e436c522 100644 --- a/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordFeature.swift +++ b/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordFeature.swift @@ -184,12 +184,12 @@ public struct TraineeAddDietRecordFeature { return .none case .tapNavBackButton: - if state.view_isSubmitButtonEnabled { + if state.dietTime != nil, + state.dietType != nil, + state.dietInfo.isEmpty == false { return self.setPopUpStatus(&state, status: .cancelDietAdd) } else { - return .run { send in - await self.dismiss() - } + return .run { _ in await self.dismiss() } } case .tapPhotoPickerDeleteButton: From 5b7df5c26e77d3a8e85ed41623498a87ce2f59f9 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Mon, 17 Feb 2025 06:23:10 +0900 Subject: [PATCH 21/32] =?UTF-8?q?[Feat]=20=EC=9C=A0=EC=A0=80=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EA=B4=80=EB=A0=A8=20=EC=A0=95=EC=B1=85=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TnT/Projects/Domain/Sources/Policy/UserPolicy.swift | 2 +- .../Onboarding/CreateProfile/CreateProfileFeature.swift | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/TnT/Projects/Domain/Sources/Policy/UserPolicy.swift b/TnT/Projects/Domain/Sources/Policy/UserPolicy.swift index fdff9457..47d52de5 100644 --- a/TnT/Projects/Domain/Sources/Policy/UserPolicy.swift +++ b/TnT/Projects/Domain/Sources/Policy/UserPolicy.swift @@ -12,7 +12,7 @@ struct UserPolicy { /// 사용자 이름 검증 - 한글/영어/공백만 허용 (특수문자 불가) static let userNameInput: PolicyInputInfo = .init( - textValidation: { TextValidator.isValidInput($0, maxLength: maxNameLength, regexPattern: "^[ㄱ-ㅎㅏ-ㅣ가-힣a-zA-Z ]*$") }, + textValidation: { TextValidator.isValidInput($0, maxLength: maxNameLength, regexPattern: "^(?!\\s*$)[ㄱ-ㅎㅏ-ㅣ가-힣a-zA-Z ]*$") }, isRequired: true ) diff --git a/TnT/Projects/Presentation/Sources/Onboarding/CreateProfile/CreateProfileFeature.swift b/TnT/Projects/Presentation/Sources/Onboarding/CreateProfile/CreateProfileFeature.swift index 07cc94d6..c757ea9a 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/CreateProfile/CreateProfileFeature.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/CreateProfile/CreateProfileFeature.swift @@ -158,6 +158,10 @@ public struct CreateProfileFeature { case .view(let action): switch action { case .binding(\.userName): + let maxLength = userUseCase.getMaxNameLength() + if state.userName.count > maxLength { + state.userName = String(state.userName.prefix(maxLength)) + } return self.validate(&state) case .binding(\.view_photoPickerItem): From c6ca366ae7ddeca111b91cc86fd3e86e3679e87c Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Mon, 17 Feb 2025 06:53:30 +0900 Subject: [PATCH 22/32] =?UTF-8?q?[Feat]=20=ED=83=AD=EB=B0=94=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20UI=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/MainTab/Trainee/TraineeMainTabView.swift | 4 +--- .../Sources/MainTab/Trainer/TrainerMainTabView.swift | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/TnT/Projects/Presentation/Sources/MainTab/Trainee/TraineeMainTabView.swift b/TnT/Projects/Presentation/Sources/MainTab/Trainee/TraineeMainTabView.swift index d537f12b..bb7ae7b4 100644 --- a/TnT/Projects/Presentation/Sources/MainTab/Trainee/TraineeMainTabView.swift +++ b/TnT/Projects/Presentation/Sources/MainTab/Trainee/TraineeMainTabView.swift @@ -43,7 +43,6 @@ public struct TraineeMainTabView: View { private func BottomTabBar() -> some View { HStack(alignment: .top) { ForEach(TraineeTabInfo.allCases, id: \.hashValue) { tab in - Spacer() TMainTabButton( unselectedIcon: tab.emptyIcn, selectedIcon: tab.filledIcn, @@ -51,8 +50,7 @@ public struct TraineeMainTabView: View { isSelected: store.state.tabInfo == tab, action: { send(.selectTab(tab)) } ) - .frame(maxHeight: .infinity, alignment: .top) - Spacer() + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) } } .frame(height: 54 + .safeAreaBottom) diff --git a/TnT/Projects/Presentation/Sources/MainTab/Trainer/TrainerMainTabView.swift b/TnT/Projects/Presentation/Sources/MainTab/Trainer/TrainerMainTabView.swift index f49382b9..1c124513 100644 --- a/TnT/Projects/Presentation/Sources/MainTab/Trainer/TrainerMainTabView.swift +++ b/TnT/Projects/Presentation/Sources/MainTab/Trainer/TrainerMainTabView.swift @@ -51,7 +51,6 @@ public struct TrainerMainTabView: View { private func BottomTabBar() -> some View { HStack(alignment: .top) { ForEach(TrainerTabInfo.allCases, id: \.hashValue) { tab in - Spacer() TMainTabButton( unselectedIcon: tab.emptyIcn, selectedIcon: tab.filledIcn, @@ -59,8 +58,7 @@ public struct TrainerMainTabView: View { isSelected: store.state.tabInfo == tab, action: { send(.selectTab(tab)) } ) - .frame(maxHeight: .infinity, alignment: .top) - Spacer() + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) } } .frame(height: 54 + .safeAreaBottom) From 061015b61fb64d5033cb1a35a62bc4a5a51f5a99 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Mon, 17 Feb 2025 07:35:31 +0900 Subject: [PATCH 23/32] =?UTF-8?q?[Feat]=20=EB=B0=94=ED=85=80=20=EC=8B=9C?= =?UTF-8?q?=ED=8A=B8=20=EC=83=81=EB=8B=A8=20=ED=8C=A8=EB=94=A9=20=EC=A1=B0?= =?UTF-8?q?=EC=A0=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BottomSheet/AutoSizingBottomSheetModifier.swift | 5 +++-- .../AddPTSession/TrainerSelectSessionTraineeView.swift | 1 + .../Presentation/Sources/Home/Trainee/TraineeHomeView.swift | 1 - .../Presentation/Sources/Onboarding/Common/LoginView.swift | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/TnT/Projects/DesignSystem/Sources/Components/BottomSheet/AutoSizingBottomSheetModifier.swift b/TnT/Projects/DesignSystem/Sources/Components/BottomSheet/AutoSizingBottomSheetModifier.swift index ef0a0aa4..867f3ca9 100644 --- a/TnT/Projects/DesignSystem/Sources/Components/BottomSheet/AutoSizingBottomSheetModifier.swift +++ b/TnT/Projects/DesignSystem/Sources/Components/BottomSheet/AutoSizingBottomSheetModifier.swift @@ -22,14 +22,15 @@ struct AutoSizingBottomSheetModifier: ViewModifier { func body(content: Content) -> some View { content + .padding(.top, 24) .background( GeometryReader { proxy in Color.clear .onAppear { - contentHeight = proxy.size.height + 10 + contentHeight = proxy.size.height } .onChange(of: proxy.size.height) { _, newHeight in - contentHeight = newHeight + 10 + contentHeight = newHeight } } ) diff --git a/TnT/Projects/Presentation/Sources/AddPTSession/TrainerSelectSessionTraineeView.swift b/TnT/Projects/Presentation/Sources/AddPTSession/TrainerSelectSessionTraineeView.swift index 98dee8ac..3d07cb15 100644 --- a/TnT/Projects/Presentation/Sources/AddPTSession/TrainerSelectSessionTraineeView.swift +++ b/TnT/Projects/Presentation/Sources/AddPTSession/TrainerSelectSessionTraineeView.swift @@ -48,6 +48,7 @@ public struct TrainerSelectSessionTraineeView: View { Spacer(minLength: 0) } + .padding(.top, 24) .presentationDetents([.height(contentHeight)]) .presentationDragIndicator(contentHeight == maxHeight ? .visible : .hidden) } diff --git a/TnT/Projects/Presentation/Sources/Home/Trainee/TraineeHomeView.swift b/TnT/Projects/Presentation/Sources/Home/Trainee/TraineeHomeView.swift index 47366475..04df3bae 100644 --- a/TnT/Projects/Presentation/Sources/Home/Trainee/TraineeHomeView.swift +++ b/TnT/Projects/Presentation/Sources/Home/Trainee/TraineeHomeView.swift @@ -63,7 +63,6 @@ public struct TraineeHomeView: View { // ("🏋🏻‍♀️", "개인 운동", { send(.tapAddWorkoutRecordButton) }), ("🥗", "식단", { send(.tapAddDietRecordButton) }) ]) - .padding(.top, 10) .padding(.bottom, 20) .autoSizingBottomSheet() } diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Common/LoginView.swift b/TnT/Projects/Presentation/Sources/Onboarding/Common/LoginView.swift index 416f7e8b..b463f077 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Common/LoginView.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Common/LoginView.swift @@ -39,7 +39,7 @@ public struct LoginView: View { .navigationPopGestureDisabled() .sheet(item: $store.scope(state: \.termFeature, action: \.subFeature.termAction)) { store in TermView(store: store) - .padding(.top, 10) + .padding(.top, 24) .presentationDetents([.height(512)]) .presentationDragIndicator(.visible) } From 7be6d97525ceb652fd7d7f1ad1c4dec34f4c094e Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Mon, 17 Feb 2025 08:49:16 +0900 Subject: [PATCH 24/32] =?UTF-8?q?[Fix]=20=ED=8F=B0=ED=8A=B8,=20TButton=20i?= =?UTF-8?q?ntrinsic=20=EB=86=92=EC=9D=B4=20=EA=B4=80=EB=A0=A8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DesignSystem/Sources/Button/TButton.swift | 17 +++--- .../DesignSystem/Font+DesignSystem.swift | 52 +++++++++++++------ 2 files changed, 43 insertions(+), 26 deletions(-) diff --git a/TnT/Projects/DesignSystem/Sources/Button/TButton.swift b/TnT/Projects/DesignSystem/Sources/Button/TButton.swift index 3cc47f6e..d5231c8b 100644 --- a/TnT/Projects/DesignSystem/Sources/Button/TButton.swift +++ b/TnT/Projects/DesignSystem/Sources/Button/TButton.swift @@ -68,8 +68,9 @@ public struct TButton: View { // 제목 추가 Text(title) - .typographyStyle(config.font, with: textColor) - .frame(maxWidth: .infinity, alignment: .center) + .typographyStyle(config.font, with: textColor) + .frame(maxWidth: .infinity, alignment: .center) + .frame(minHeight: config.font.lineHeight) // 오른쪽 이미지 추가 if let rightImage = image, rightImage.type == .right || rightImage.type == .both { @@ -79,15 +80,13 @@ public struct TButton: View { .frame(width: rightImage.size, height: rightImage.size) } } - .padding(.vertical, config.verticalSize) - .padding(.horizontal, config.horizontalSize) - .background(backgroundColor) - .clipShape(RoundedRectangle(cornerRadius: config.radius)) - .overlay { + .padding(.vertical, config.verticalSize + 0.5) + .padding(.horizontal, config.horizontalSize + 0.5) + .background( RoundedRectangle(cornerRadius: config.radius) + .fill(backgroundColor) .stroke(borderColor, lineWidth: 1.5) - } - .contentShape(Rectangle()) + ) } } diff --git a/TnT/Projects/DesignSystem/Sources/DesignSystem/Font+DesignSystem.swift b/TnT/Projects/DesignSystem/Sources/DesignSystem/Font+DesignSystem.swift index 059299f8..79f19557 100644 --- a/TnT/Projects/DesignSystem/Sources/DesignSystem/Font+DesignSystem.swift +++ b/TnT/Projects/DesignSystem/Sources/DesignSystem/Font+DesignSystem.swift @@ -49,13 +49,13 @@ public struct Typography { /// - size: 폰트 크기 /// - lineHeightMultiplier: 줄 높이 배율 (CGFloat) /// - letterSpacing: 자간 (CGFloat) - init(_ weight: Pretendard.Weight, size: CGFloat, lineHeightMultiplier: CGFloat, letterSpacing: CGFloat) { + init(_ weight: Pretendard.Weight, size: CGFloat, lineHeightMultiplier: CGFloat, letterSpacingRate: CGFloat) { self.font = weight.fontConvertible.swiftUIFont(size: size) self.uiFont = weight.fontConvertible.font(size: size) self.size = size self.lineHeight = size * lineHeightMultiplier self.lineSpacing = (size * lineHeightMultiplier) - size - self.letterSpacing = letterSpacing + self.letterSpacing = size * letterSpacingRate } } } @@ -63,26 +63,26 @@ public struct Typography { /// 앱에서 사용할 기본적인 폰트 스타일을 정의합니다. public extension Typography.FontStyle { // Heading Styles - static let heading1: Typography.FontStyle = Typography.FontStyle(.bold, size: 28, lineHeightMultiplier: 1.4, letterSpacing: -0.02) - static let heading2: Typography.FontStyle = Typography.FontStyle(.bold, size: 24, lineHeightMultiplier: 1.5, letterSpacing: -0.02) - static let heading3: Typography.FontStyle = Typography.FontStyle(.bold, size: 20, lineHeightMultiplier: 1.5, letterSpacing: -0.02) - static let heading4: Typography.FontStyle = Typography.FontStyle(.bold, size: 18, lineHeightMultiplier: 1.5, letterSpacing: -0.02) + static let heading1: Typography.FontStyle = Typography.FontStyle(.bold, size: 28, lineHeightMultiplier: 1.4, letterSpacingRate: -0.02) + static let heading2: Typography.FontStyle = Typography.FontStyle(.bold, size: 24, lineHeightMultiplier: 1.5, letterSpacingRate: -0.02) + static let heading3: Typography.FontStyle = Typography.FontStyle(.bold, size: 20, lineHeightMultiplier: 1.5, letterSpacingRate: -0.02) + static let heading4: Typography.FontStyle = Typography.FontStyle(.bold, size: 18, lineHeightMultiplier: 1.5, letterSpacingRate: -0.02) // Body Styles - static let body1Bold: Typography.FontStyle = Typography.FontStyle(.bold, size: 16, lineHeightMultiplier: 1.5, letterSpacing: -0.02) - static let body1Semibold: Typography.FontStyle = Typography.FontStyle(.semibold, size: 16, lineHeightMultiplier: 1.5, letterSpacing: -0.02) - static let body1Medium: Typography.FontStyle = Typography.FontStyle(.medium, size: 16, lineHeightMultiplier: 1.6, letterSpacing: -0.02) - static let body2Bold: Typography.FontStyle = Typography.FontStyle(.bold, size: 15, lineHeightMultiplier: 1.5, letterSpacing: -0.02) - static let body2Medium: Typography.FontStyle = Typography.FontStyle(.medium, size: 15, lineHeightMultiplier: 1.5, letterSpacing: -0.02) + static let body1Bold: Typography.FontStyle = Typography.FontStyle(.bold, size: 16, lineHeightMultiplier: 1.5, letterSpacingRate: -0.02) + static let body1Semibold: Typography.FontStyle = Typography.FontStyle(.semibold, size: 16, lineHeightMultiplier: 1.5, letterSpacingRate: -0.02) + static let body1Medium: Typography.FontStyle = Typography.FontStyle(.medium, size: 16, lineHeightMultiplier: 1.6, letterSpacingRate: -0.02) + static let body2Bold: Typography.FontStyle = Typography.FontStyle(.bold, size: 15, lineHeightMultiplier: 1.5, letterSpacingRate: -0.02) + static let body2Medium: Typography.FontStyle = Typography.FontStyle(.medium, size: 15, lineHeightMultiplier: 1.5, letterSpacingRate: -0.02) // Label Styles - static let label1Bold: Typography.FontStyle = Typography.FontStyle(.bold, size: 13, lineHeightMultiplier: 1.3, letterSpacing: -0.02) - static let label1Medium: Typography.FontStyle = Typography.FontStyle(.medium, size: 13, lineHeightMultiplier: 1.5, letterSpacing: -0.02) - static let label2Bold: Typography.FontStyle = Typography.FontStyle(.bold, size: 12, lineHeightMultiplier: 1.5, letterSpacing: -0.02) - static let label2Medium: Typography.FontStyle = Typography.FontStyle(.medium, size: 12, lineHeightMultiplier: 1.5, letterSpacing: -0.02) + static let label1Bold: Typography.FontStyle = Typography.FontStyle(.bold, size: 13, lineHeightMultiplier: 1.3, letterSpacingRate: -0.02) + static let label1Medium: Typography.FontStyle = Typography.FontStyle(.medium, size: 13, lineHeightMultiplier: 1.5, letterSpacingRate: -0.02) + static let label2Bold: Typography.FontStyle = Typography.FontStyle(.bold, size: 12, lineHeightMultiplier: 1.5, letterSpacingRate: -0.02) + static let label2Medium: Typography.FontStyle = Typography.FontStyle(.medium, size: 12, lineHeightMultiplier: 1.5, letterSpacingRate: -0.02) // Caption Styles - static let caption1: Typography.FontStyle = Typography.FontStyle(.medium, size: 11, lineHeightMultiplier: 1.3, letterSpacing: -0.02) + static let caption1: Typography.FontStyle = Typography.FontStyle(.medium, size: 11, lineHeightMultiplier: 1.3, letterSpacingRate: -0.02) } /// 텍스트에 Typography 스타일과 색상을 적용하는 ViewModifier입니다. @@ -91,13 +91,31 @@ public extension Typography.FontStyle { struct TypographyModifier: ViewModifier { let style: Typography.FontStyle let color: Color - + func body(content: Content) -> some View { content .font(style.font) .lineSpacing(style.lineSpacing) .kerning(style.letterSpacing) .foregroundStyle(color) + .background( + GeometryReader { proxy in + Color.clear + .onAppear { + if proxy.size.height < style.lineHeight { + applySingleLineFix(content: content) + } + } + } + ) + } + + /// 한 줄짜리 텍스트에 대한 lineHeight 적용 + @ViewBuilder + private func applySingleLineFix(content: Content) -> some View { + content + .frame(height: style.lineHeight) + .baselineOffset((style.lineHeight - style.size) / 2) } } From 49b7183325dfb22ad02439b756410dad1bcccc7d Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Mon, 17 Feb 2025 08:55:13 +0900 Subject: [PATCH 25/32] =?UTF-8?q?[Fix]=20TrainerHome=20=EC=88=98=EC=97=85?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20UI=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Presentation/Sources/Home/Trainer/TrainerHomeView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/TnT/Projects/Presentation/Sources/Home/Trainer/TrainerHomeView.swift b/TnT/Projects/Presentation/Sources/Home/Trainer/TrainerHomeView.swift index 331effd5..3ad75163 100644 --- a/TnT/Projects/Presentation/Sources/Home/Trainer/TrainerHomeView.swift +++ b/TnT/Projects/Presentation/Sources/Home/Trainer/TrainerHomeView.swift @@ -247,7 +247,6 @@ extension TrainerHomeView { Image(.icnClock) Text("\(session.startTime) ~ \(session.endTime)") .typographyStyle(.label2Medium, with: .neutral500) - .frame(maxWidth: .infinity) } HStack(spacing: 6) { From 37b90ffe230c9159105a11818ccaf566204eb759 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Mon, 17 Feb 2025 09:01:01 +0900 Subject: [PATCH 26/32] =?UTF-8?q?[Fix]=20=ED=94=BC=EB=93=9C=EB=B0=B1,?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=EB=AA=A9=EB=A1=9D=20UI=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Presentation/Sources/Feedback/TrainerFeedbackView.swift | 1 + .../Sources/TrainerManagement/TrainerManagementView.swift | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/TnT/Projects/Presentation/Sources/Feedback/TrainerFeedbackView.swift b/TnT/Projects/Presentation/Sources/Feedback/TrainerFeedbackView.swift index feb60b43..287f5941 100644 --- a/TnT/Projects/Presentation/Sources/Feedback/TrainerFeedbackView.swift +++ b/TnT/Projects/Presentation/Sources/Feedback/TrainerFeedbackView.swift @@ -27,6 +27,7 @@ struct TrainerFeedbackView: View { .frame(minHeight: UIScreen.main.bounds.height - 204) } } + .background(Color.neutral100) .navigationBarBackButtonHidden() } diff --git a/TnT/Projects/Presentation/Sources/TrainerManagement/TrainerManagementView.swift b/TnT/Projects/Presentation/Sources/TrainerManagement/TrainerManagementView.swift index cee90fab..ac07d349 100644 --- a/TnT/Projects/Presentation/Sources/TrainerManagement/TrainerManagementView.swift +++ b/TnT/Projects/Presentation/Sources/TrainerManagement/TrainerManagementView.swift @@ -22,12 +22,13 @@ struct TrainerManagementView: View { } var body: some View { - VStack(spacing: 12) { + VStack(spacing: 0) { Header() ScrollView(showsIndicators: false) { if let trainees = store.traineeList, !trainees.isEmpty { TraineeListView(trainees: trainees) + .padding(.vertical, 12) } else { EmptyListView() .frame(minHeight: UIScreen.main.bounds.height - 204) From 45672b7a2b7d50b44bf65d384be8be8ade9a371b Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Tue, 18 Feb 2025 00:17:21 +0900 Subject: [PATCH 27/32] =?UTF-8?q?[Feat]=20=EA=B0=80=EC=9E=85=EC=8B=9C=20FC?= =?UTF-8?q?M=20=ED=86=A0=ED=81=B0=20=EB=93=B1=EB=A1=9D=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Service/SocialLogin/SNSLoginManager.swift | 18 +++++ .../SocialLogInRepositoryImpl.swift | 5 ++ .../Sources/Entity/SocailLoginEntity.swift | 12 ++-- .../Repository/SocialLoginRepository.swift | 2 + .../Sources/UseCase/SocialLoginUseCase.swift | 4 ++ .../Onboarding/Common/LoginFeature.swift | 67 +++++++++++-------- .../CreateProfile/CreateProfileFeature.swift | 17 ++++- .../TraineePrecautionInputFeature.swift | 48 +++++++++---- 8 files changed, 121 insertions(+), 52 deletions(-) diff --git a/TnT/Projects/Data/Sources/Network/Service/SocialLogin/SNSLoginManager.swift b/TnT/Projects/Data/Sources/Network/Service/SocialLogin/SNSLoginManager.swift index afb4eeda..63d69f1e 100644 --- a/TnT/Projects/Data/Sources/Network/Service/SocialLogin/SNSLoginManager.swift +++ b/TnT/Projects/Data/Sources/Network/Service/SocialLogin/SNSLoginManager.swift @@ -9,6 +9,7 @@ import AuthenticationServices import KakaoSDKCommon import KakaoSDKAuth import KakaoSDKUser +import FirebaseMessaging import Domain @@ -172,3 +173,20 @@ extension SNSLoginManager { ) } } + +// MARK: FCM 토큰 +extension SNSLoginManager { + public func getFCMToken() async throws -> String { + return try await withCheckedThrowingContinuation { continuation in + Messaging.messaging().token { token, error in + if let error = error { + continuation.resume(throwing: error) + } else if let token = token { + continuation.resume(returning: token) + } else { + continuation.resume(throwing: LoginError.fcmError) + } + } + } + } +} diff --git a/TnT/Projects/Data/Sources/Network/Service/SocialLogin/SocialLogInRepositoryImpl.swift b/TnT/Projects/Data/Sources/Network/Service/SocialLogin/SocialLogInRepositoryImpl.swift index ffdd4e36..a51454bb 100644 --- a/TnT/Projects/Data/Sources/Network/Service/SocialLogin/SocialLogInRepositoryImpl.swift +++ b/TnT/Projects/Data/Sources/Network/Service/SocialLogin/SocialLogInRepositoryImpl.swift @@ -16,6 +16,7 @@ public enum LoginError: Error { case networkFailure case kakaoError case appleError + case fcmError case unknown(message: String) } @@ -34,4 +35,8 @@ public struct SocialLogInRepositoryImpl: SocialLoginRepository { public func kakaoLogin() async -> KakaoLoginInfo? { return await loginManager.kakaoLogin() } + + public func getFCMToken() async throws -> String { + return try await loginManager.getFCMToken() + } } diff --git a/TnT/Projects/Domain/Sources/Entity/SocailLoginEntity.swift b/TnT/Projects/Domain/Sources/Entity/SocailLoginEntity.swift index 85649c86..593f3809 100644 --- a/TnT/Projects/Domain/Sources/Entity/SocailLoginEntity.swift +++ b/TnT/Projects/Domain/Sources/Entity/SocailLoginEntity.swift @@ -8,21 +8,21 @@ import Foundation -public enum SocialType: String { +public enum SocialType: String, Sendable { case kakao = "KAKAO" case apple = "APPLE" } /// 소셜 로그인 요청 DTO -public struct PostSocialEntity: Equatable { +public struct PostSocialEntity: Equatable, Sendable { /// 소셜 로그인 타입 (KAKAO, APPLE) - let socialType: SocialType + public let socialType: SocialType /// FCM 토큰 - let fcmToken: String + public var fcmToken: String /// 소셜 액세스 토큰 - let socialAccessToken: String? + public let socialAccessToken: String? /// 애플 ID 토큰 (Apple z로그인 시 필요) - let idToken: String? + public let idToken: String? public init( socialType: SocialType, diff --git a/TnT/Projects/Domain/Sources/Repository/SocialLoginRepository.swift b/TnT/Projects/Domain/Sources/Repository/SocialLoginRepository.swift index 38abc6d3..60451f8e 100644 --- a/TnT/Projects/Domain/Sources/Repository/SocialLoginRepository.swift +++ b/TnT/Projects/Domain/Sources/Repository/SocialLoginRepository.swift @@ -14,4 +14,6 @@ public protocol SocialLoginRepository { func appleLogin() async -> AppleLoginInfo? /// 카카오 로그인을 수행합니다 func kakaoLogin() async -> KakaoLoginInfo? + /// FCM 토큰을 가져옵니다 + func getFCMToken() async throws -> String } diff --git a/TnT/Projects/Domain/Sources/UseCase/SocialLoginUseCase.swift b/TnT/Projects/Domain/Sources/UseCase/SocialLoginUseCase.swift index 6eea308b..e3c220c4 100644 --- a/TnT/Projects/Domain/Sources/UseCase/SocialLoginUseCase.swift +++ b/TnT/Projects/Domain/Sources/UseCase/SocialLoginUseCase.swift @@ -23,4 +23,8 @@ public struct SocialLoginUseCase { public func kakaoLogin() async -> KakaoLoginInfo? { return await socialLoginRepository.kakaoLogin() } + + public func getFCMToken() async throws -> String { + return try await socialLoginRepository.getFCMToken() + } } diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Common/LoginFeature.swift b/TnT/Projects/Presentation/Sources/Onboarding/Common/LoginFeature.swift index 6ac03a6a..595b5af0 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Common/LoginFeature.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Common/LoginFeature.swift @@ -44,14 +44,12 @@ public struct LoginFeature { public enum Action: ViewAction { /// 뷰에서 일어나는 액션을 처리합니다.(카카오,애플로그인 실행) case view(View) + /// api 액션처리 + case api(APIAction) /// 하위 화면에서 일어나는 액션을 처리합니다 case subFeature(SubFeatureAction) /// 네비게이션 여부 설정 case setNavigating(RoutingScreen) - /// 소셜 로그인 post 요청 - case postSocialLogin(entity: PostSocialEntity) - /// 소셜 로그인 실패 - case socialLoginFail /// signUpEntity를 소셜로그인 정보로 업데이트 case updateSignUpEntityWithSocialInfo(res: PostSocialLoginResDTO) /// 약관 동의 화면 표시 @@ -63,6 +61,14 @@ public struct LoginFeature { case tappedKakaoLogin } + @CasePathable + public enum APIAction: Sendable { + /// FCM 토큰 받아 주입 + case insertFCMToken(entity: PostSocialEntity) + /// 소셜 로그인 post 요청 + case postSocialLogin(entity: PostSocialEntity) + } + @CasePathable public enum SubFeatureAction: Equatable { /// 역관 동의 화면에서 발생하는 액션 처리 @@ -90,7 +96,7 @@ public struct LoginFeature { idToken: result.identityToken ) - await send(.postSocialLogin(entity: entity)) + await send(.api(.insertFCMToken(entity: entity))) } case .tappedKakaoLogin: @@ -106,29 +112,24 @@ public struct LoginFeature { idToken: "" ) - await send(.postSocialLogin(entity: entity)) + await send(.api(.insertFCMToken(entity: entity))) } } - 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)): - state.termFeature = nil - return .none - - case .subFeature: - return .none - - case .postSocialLogin(let entity): - let post: PostSocialLoginReqDTO = entity.toDTO() - - return .run { send in - do { + case .api(let action): + switch action { + case .insertFCMToken(let entity): + return .run { send in + var mutatedEntity = entity + let fcmToken = try await socialLoginUseCase.getFCMToken() + mutatedEntity.fcmToken = fcmToken + await send(.api(.postSocialLogin(entity: mutatedEntity))) + } + + case .postSocialLogin(let entity): + let post: PostSocialLoginReqDTO = entity.toDTO() + + return .run { send in let result: PostSocialLoginResDTO = try await userUseCaseRepo.postSocialLogin(post) saveSessionId(result.sessionId) @@ -142,13 +143,21 @@ public struct LoginFeature { case .unknown: print("unknown 타입이에요 토스트해줏요") } - } catch { - await send(.socialLoginFail) } } - case .socialLoginFail: - print("네트워크 에러 발생") + 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)): + state.termFeature = nil + return .none + + case .subFeature: return .none case .updateSignUpEntityWithSocialInfo(let res): diff --git a/TnT/Projects/Presentation/Sources/Onboarding/CreateProfile/CreateProfileFeature.swift b/TnT/Projects/Presentation/Sources/Onboarding/CreateProfile/CreateProfileFeature.swift index c757ea9a..7feea109 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/CreateProfile/CreateProfileFeature.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/CreateProfile/CreateProfileFeature.swift @@ -103,6 +103,7 @@ public struct CreateProfileFeature { @Dependency(\.userUseCase) private var userUseCase: UserUseCase @Dependency(\.userUseRepoCase) private var userUseRepoCase: UserRepository + @Dependency(\.socialLogInUseCase) private var socialLoginUseCase: SocialLoginUseCase @Dependency(\.keyChainManager) var keyChainManager public enum Action: Sendable, ViewAction { @@ -133,8 +134,10 @@ public struct CreateProfileFeature { @CasePathable public enum APIAction: Sendable { + /// FCM 토큰 get + case getFCMToken /// 회원가입 POST - case postSignUp + case postSignUp(fcmToken: String) } @CasePathable @@ -186,7 +189,7 @@ public struct CreateProfileFeature { case .trainee: return .send(.setNavigating(.traineeBasicInfoInput)) case .trainer: - return .send(.api(.postSignUp)) + return .send(.api(.getFCMToken)) } case .tapPopUpSecondaryButton(let popUp): @@ -208,7 +211,15 @@ public struct CreateProfileFeature { case .api(let action): switch action { - case .postSignUp: + case .getFCMToken: + return .run { send in + let fcmToken = try await socialLoginUseCase.getFCMToken() + await send(.api(.postSignUp(fcmToken: fcmToken))) + } + + case .postSignUp(let fcmToken): + state.$signUpEntity.withLock { $0.fcmToken = fcmToken } + guard let reqDTO = state.signUpEntity.toDTO() else { return .none } diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineePrecautionInput/TraineePrecautionInputFeature.swift b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineePrecautionInput/TraineePrecautionInputFeature.swift index 06aec261..ed5259ef 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineePrecautionInput/TraineePrecautionInputFeature.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineePrecautionInput/TraineePrecautionInputFeature.swift @@ -62,15 +62,16 @@ public struct TraineePrecautionInputFeature { @Dependency(\.userUseCase) private var userUseCase: UserUseCase @Dependency(\.userUseRepoCase) private var userUseRepoCase: UserRepository + @Dependency(\.socialLogInUseCase) private var socialLoginUseCase: SocialLoginUseCase @Dependency(\.keyChainManager) var keyChainManager public enum Action: Sendable, ViewAction { /// 뷰에서 발생한 액션을 처리합니다. case view(View) + /// api 액션처리 + case api(APIAction) /// 네비게이션 여부 설정 case setNavigating(PostSignUpResEntity) - /// 회원가입 POST - case postSignUp @CasePathable public enum View: Sendable, BindableAction { @@ -81,6 +82,14 @@ public struct TraineePrecautionInputFeature { /// 포커스 상태 변경 case setFocus(Bool) } + + @CasePathable + public enum APIAction: Sendable { + /// FCM 토큰 get + case getFCMToken + /// 회원가입 POST + case postSignUp(fcmToken: String) + } } public init() {} @@ -104,21 +113,32 @@ public struct TraineePrecautionInputFeature { case .tapNextButton: state.$signUpEntity.withLock { $0.cautionNote = state.precaution } - return .send(.postSignUp) - } - - case .postSignUp: - guard let reqDTO = state.signUpEntity.toDTO() else { - return .none + return .send(.api(.getFCMToken)) } - let imgData = state.signUpEntity.imageData - return .run { send in - let result = try await userUseRepoCase.postSignUp(reqDTO, profileImage: imgData).toEntity() - saveSessionId(result.sessionId) - await send(.setNavigating(result)) + case .api(let action): + switch action { + case .getFCMToken: + return .run { send in + let fcmToken = try await socialLoginUseCase.getFCMToken() + await send(.api(.postSignUp(fcmToken: fcmToken))) + } + + case .postSignUp(let fcmToken): + state.$signUpEntity.withLock { $0.fcmToken = fcmToken } + + 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() + saveSessionId(result.sessionId) + await send(.setNavigating(result)) + } } - + case .setNavigating: return .none } From 42dfbbe0a6e56a2ec7e76d323f25c49a223865e9 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Tue, 18 Feb 2025 17:14:45 +0900 Subject: [PATCH 28/32] =?UTF-8?q?[Feat]=20=EA=B6=8C=ED=95=9C=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EB=AC=B8=EA=B5=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/AddDietRecord/TraineeAddDietRecordFeature.swift | 2 +- .../Onboarding/CreateProfile/CreateProfileFeature.swift | 2 +- .../Onboarding/CreateProfile/CreateProfileView.swift | 6 +++--- TnT/Tuist/Config/Info.plist | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordFeature.swift b/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordFeature.swift index e436c522..d6602975 100644 --- a/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordFeature.swift +++ b/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordFeature.swift @@ -414,7 +414,7 @@ public extension TraineeAddDietRecordFeature { case .cancelDietAdd: return "기록이 저장되지 않아요!" case .photoAuthorization: - return "사진 추가는 식단 기록 말고도\n프로필과 운동 기록에도 사용돼요" + return "‘TnT'는 프로필 사진 설정, 운동 기록 및 식단 기록 저장 등 주요 기능 제공을 위해 사진 접근 권한이 필요합니다.\n설정에서 권한을 활성화해주세요." } } diff --git a/TnT/Projects/Presentation/Sources/Onboarding/CreateProfile/CreateProfileFeature.swift b/TnT/Projects/Presentation/Sources/Onboarding/CreateProfile/CreateProfileFeature.swift index 7feea109..6ef55046 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/CreateProfile/CreateProfileFeature.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/CreateProfile/CreateProfileFeature.swift @@ -313,7 +313,7 @@ public extension CreateProfileFeature { var message: String { switch self { case .photoAuthorization: - return "사진 추가는 프로필 말고도\n운동과 식단 기록에도 사용돼요" + return "‘TnT'는 프로필 사진 설정, 운동 기록 및 식단 기록 저장 등 주요 기능 제공을 위해 사진 접근 권한이 필요합니다.\n설정에서 권한을 활성화해주세요." } } diff --git a/TnT/Projects/Presentation/Sources/Onboarding/CreateProfile/CreateProfileView.swift b/TnT/Projects/Presentation/Sources/Onboarding/CreateProfile/CreateProfileView.swift index 91bf5e9e..9a8baeab 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/CreateProfile/CreateProfileView.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/CreateProfile/CreateProfileView.swift @@ -76,7 +76,7 @@ public struct CreateProfileView: View { .padding(.horizontal, 20) } - TInfoTitleHeader(title: "이름이 어떻게 되세요?") + TInfoTitleHeader(title: "닉네임이 어떻게 되세요?") } .padding(.vertical, 12) } @@ -121,14 +121,14 @@ public struct CreateProfileView: View { @ViewBuilder private func TextFieldSection() -> some View { TTextField( - placeholder: "이름을 입력해주세요", + placeholder: "닉네임을 입력해주세요", text: $store.userName, textFieldStatus: $store.view_textFieldStatus ) .withSectionLayout( header: .init( isRequired: true, - title: "이름", + title: "닉네임", limitCount: store.view_nameMaxLength ?? 15, textCount: store.userName.count ), diff --git a/TnT/Tuist/Config/Info.plist b/TnT/Tuist/Config/Info.plist index 344d06c3..9df036df 100644 --- a/TnT/Tuist/Config/Info.plist +++ b/TnT/Tuist/Config/Info.plist @@ -91,6 +91,6 @@ UIUserInterfaceStyle Light NSPhotoLibraryUsageDescription - 앱이 사진 라이브러리에 접근하여 사진을 선택할 수 있도록 허용해주세요. + 'TnT'가 사진 추가, 운동 및 식단 기록, 프로필 사진 업로드를 위한 기능 제공을 위해 사진 라이브러리에 접근하도록 허용합니다. From 2681716901e152692bfbb30cd6d8abaeb5506727 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Tue, 18 Feb 2025 17:15:01 +0900 Subject: [PATCH 29/32] =?UTF-8?q?[Fix]=20FCM=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Components/PopUp/TPopUpAlertView.swift | 1 + .../Sources/Onboarding/Common/LoginFeature.swift | 11 ++++++++--- .../CreateProfile/CreateProfileFeature.swift | 8 ++++++-- .../TraineePrecautionInputFeature.swift | 8 ++++++-- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/TnT/Projects/DesignSystem/Sources/Components/PopUp/TPopUpAlertView.swift b/TnT/Projects/DesignSystem/Sources/Components/PopUp/TPopUpAlertView.swift index 5a83f15f..948c150d 100644 --- a/TnT/Projects/DesignSystem/Sources/Components/PopUp/TPopUpAlertView.swift +++ b/TnT/Projects/DesignSystem/Sources/Components/PopUp/TPopUpAlertView.swift @@ -35,6 +35,7 @@ public struct TPopUpAlertView: View { Text(alertState.title) .typographyStyle(.heading3, with: .neutral900) .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) } if let message = alertState.message { diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Common/LoginFeature.swift b/TnT/Projects/Presentation/Sources/Onboarding/Common/LoginFeature.swift index 595b5af0..fff59f3e 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Common/LoginFeature.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Common/LoginFeature.swift @@ -121,9 +121,14 @@ public struct LoginFeature { case .insertFCMToken(let entity): return .run { send in var mutatedEntity = entity - let fcmToken = try await socialLoginUseCase.getFCMToken() - mutatedEntity.fcmToken = fcmToken - await send(.api(.postSocialLogin(entity: mutatedEntity))) + if let fcmToken = try? await socialLoginUseCase.getFCMToken() { + mutatedEntity.fcmToken = fcmToken + await send(.api(.postSocialLogin(entity: mutatedEntity))) + } else { + let fcmToken: String? = try? keyChainManager.read(for: .apns) + mutatedEntity.fcmToken = fcmToken ?? "" + await send(.api(.postSocialLogin(entity: mutatedEntity))) + } } case .postSocialLogin(let entity): diff --git a/TnT/Projects/Presentation/Sources/Onboarding/CreateProfile/CreateProfileFeature.swift b/TnT/Projects/Presentation/Sources/Onboarding/CreateProfile/CreateProfileFeature.swift index 6ef55046..7f575a56 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/CreateProfile/CreateProfileFeature.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/CreateProfile/CreateProfileFeature.swift @@ -213,8 +213,12 @@ public struct CreateProfileFeature { switch action { case .getFCMToken: return .run { send in - let fcmToken = try await socialLoginUseCase.getFCMToken() - await send(.api(.postSignUp(fcmToken: fcmToken))) + if let fcmToken = try? await socialLoginUseCase.getFCMToken() { + await send(.api(.postSignUp(fcmToken: fcmToken))) + } else { + let fcmToken: String? = try? keyChainManager.read(for: .apns) + await send(.api(.postSignUp(fcmToken: fcmToken ?? ""))) + } } case .postSignUp(let fcmToken): diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineePrecautionInput/TraineePrecautionInputFeature.swift b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineePrecautionInput/TraineePrecautionInputFeature.swift index ed5259ef..00540560 100644 --- a/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineePrecautionInput/TraineePrecautionInputFeature.swift +++ b/TnT/Projects/Presentation/Sources/Onboarding/Trainee/TraineePrecautionInput/TraineePrecautionInputFeature.swift @@ -120,8 +120,12 @@ public struct TraineePrecautionInputFeature { switch action { case .getFCMToken: return .run { send in - let fcmToken = try await socialLoginUseCase.getFCMToken() - await send(.api(.postSignUp(fcmToken: fcmToken))) + if let fcmToken = try? await socialLoginUseCase.getFCMToken() { + await send(.api(.postSignUp(fcmToken: fcmToken))) + } else { + let fcmToken: String? = try? keyChainManager.read(for: .apns) + await send(.api(.postSignUp(fcmToken: fcmToken ?? ""))) + } } case .postSignUp(let fcmToken): From 57af9bbd503949e54b53da473c442587af3fa48c Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Sat, 22 Feb 2025 12:58:47 +0900 Subject: [PATCH 30/32] =?UTF-8?q?[Fix]=20AddDietRecord=20=ED=8C=9D?= =?UTF-8?q?=EC=97=85=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TraineeAddDietRecordFeature.swift | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordFeature.swift b/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordFeature.swift index d6602975..1e6564ac 100644 --- a/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordFeature.swift +++ b/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordFeature.swift @@ -233,28 +233,28 @@ public struct TraineeAddDietRecordFeature { case .tapPopUpSecondaryButton(let popUp): guard popUp != nil else { return .none } - guard popUp != .photoAuthorization else { - return setPopUpStatus(&state, status: nil) - } - - return .concatenate( - setPopUpStatus(&state, status: nil), - .run{ _ in await self.dismiss() } - ) + return setPopUpStatus(&state, status: nil) case .tapPopUpPrimaryButton(let popUp): - guard popUp != nil else { return .none } + guard let popUp else { return .none } - if popUp == .photoAuthorization, - let url = URL(string: UIApplication.openSettingsURLString), - UIApplication.shared.canOpenURL(url) { - UIApplication.shared.open(url) + switch popUp { + case .dietAdded: + return .send(.setNavigating) + + case .cancelDietAdd: + return .concatenate( + setPopUpStatus(&state, status: nil), + .run { _ in await self.dismiss() } + ) + + case .photoAuthorization: + if let url = URL(string: UIApplication.openSettingsURLString), UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url) + } + return setPopUpStatus(&state, status: nil) } - return popUp == .dietAdded - ? .send(.setNavigating) - : setPopUpStatus(&state, status: nil) - case let .setFocus(oldFocus, newFocus): guard oldFocus != newFocus else { return .none } state.view_focusField = newFocus From 403148f992cf3f23979bb29045a0769b4b398d30 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Sat, 22 Feb 2025 13:04:03 +0900 Subject: [PATCH 31/32] =?UTF-8?q?[Fix]=20AddPTSession=20=EB=B2=84=ED=8A=BC?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TrainerAddPTSessionView.swift | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/TnT/Projects/Presentation/Sources/AddPTSession/TrainerAddPTSessionView.swift b/TnT/Projects/Presentation/Sources/AddPTSession/TrainerAddPTSessionView.swift index af82e370..1868234d 100644 --- a/TnT/Projects/Presentation/Sources/AddPTSession/TrainerAddPTSessionView.swift +++ b/TnT/Projects/Presentation/Sources/AddPTSession/TrainerAddPTSessionView.swift @@ -266,19 +266,14 @@ public struct TrainerAddPTSessionView: View { LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())], spacing: 12) { ForEach(TrainerAddPTSessionFeature.SessionTime.allCases, id: \.rawValue) { interval in - Button(action: { send(.tapSessionIntervalButton(interval)) }) { - Text("+\(interval.rawValue)분") - .typographyStyle(.body1Medium, with: store.view_sessionTime == interval.rawValue ? Color.red600 : Color.neutral500) - .padding(.vertical, 13) - .padding(.horizontal, 33) - .frame(maxWidth: .infinity) - .frame(height: 50) - .background( - RoundedRectangle(cornerRadius: 12) - .stroke(store.view_sessionTime == interval.rawValue ? Color.red400 : Color.neutral300, lineWidth: 1.5) - .backgroundStyle(store.view_sessionTime == interval.rawValue ? Color.red50 : Color.common0) - ) - } + TButton( + title: "+\(interval.rawValue)분", + config: .medium, + state: store.view_sessionTime == interval.rawValue + ? .default(.red(isEnabled: true)) + : .default(.outline(isEnabled: true)), + action: { send(.tapSessionIntervalButton(interval)) } + ) } } } From fbe3af4e00f9713a485fa8d13c88160fc7dddac6 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Sat, 22 Feb 2025 13:13:03 +0900 Subject: [PATCH 32/32] =?UTF-8?q?[chore]=20TraineeHome=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EC=BA=90=EC=8B=B1=20=EB=B9=84=ED=99=9C=EC=84=B1?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Home/Trainee/TraineeHomeFeature.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TnT/Projects/Presentation/Sources/Home/Trainee/TraineeHomeFeature.swift b/TnT/Projects/Presentation/Sources/Home/Trainee/TraineeHomeFeature.swift index 335243e4..80e9ee5b 100644 --- a/TnT/Projects/Presentation/Sources/Home/Trainee/TraineeHomeFeature.swift +++ b/TnT/Projects/Presentation/Sources/Home/Trainee/TraineeHomeFeature.swift @@ -283,8 +283,8 @@ extension TraineeHomeFeature { let newMonth = newPage.toString(format: .yyyyMM) // 이전에 불러온 년/달과 같은 경우 API 호출 생략 - guard !state.loadedMonths.contains(newMonth) else { return .none } - state.loadedMonths.insert(newMonth) +// guard !state.loadedMonths.contains(newMonth) else { return .none } +// state.loadedMonths.insert(newMonth) // API 호출할 범위 설정