From 0fcbe735e7dd868f88432a32ab14b0889b5ee23a Mon Sep 17 00:00:00 2001 From: Park Seo Yeon Date: Thu, 13 Feb 2025 23:49:34 +0900 Subject: [PATCH 1/4] =?UTF-8?q?[Feat]=20=ED=8A=B8=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EB=84=88=20=ED=9A=8C=EC=9B=90=EA=B4=80=EB=A6=AC=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 회원 초대하기 버튼은 아직 --- .../Trainer/TrainerRepositoryImpl.swift | 4 +- .../Sources/Navigation/TNavigation.swift | 7 +- .../DTO/Trainer/TrainerResponseDTO.swift | 14 +++ .../Entity/TraineeListItemEntity.swift | 16 ++++ .../Repository/TrainerRepository.swift | 2 +- .../Sources/UseCase/TrainerUseCase.swift | 2 +- .../TrainerMainFlowFeature.swift | 3 + .../TrainerMainFlow/TrainerMainFlowView.swift | 4 +- .../Trainer/TrainerMainTabFeature.swift | 9 +- .../MainTab/Trainer/TrainerMainTabView.swift | 3 +- .../Trainer/TrainerManagementView.swift | 85 +++++++++++++------ .../Trainer/TrainerManagmentFeature.swift | 40 +++++++-- 12 files changed, 141 insertions(+), 48 deletions(-) diff --git a/TnT/Projects/Data/Sources/Network/Service/Trainer/TrainerRepositoryImpl.swift b/TnT/Projects/Data/Sources/Network/Service/Trainer/TrainerRepositoryImpl.swift index ce757704..493f409a 100644 --- a/TnT/Projects/Data/Sources/Network/Service/Trainer/TrainerRepositoryImpl.swift +++ b/TnT/Projects/Data/Sources/Network/Service/Trainer/TrainerRepositoryImpl.swift @@ -50,10 +50,10 @@ public struct TrainerRepositoryImpl: TrainerRepository { return try await networkService.request(TrainerTargetType.getMonthlyLessonList(year: year, month: month), decodingType: GetMonthlyLessonListResDTO.self) } - public func getMembersList() async throws -> GetMembersListDTO { + public func getMembersList() async throws -> GetActiveTraineesListResDTO { return try await networkService.request( TrainerTargetType.getMemebersList, - decodingType: GetMembersListDTO.self + decodingType: GetActiveTraineesListResDTO.self ) } diff --git a/TnT/Projects/DesignSystem/Sources/Navigation/TNavigation.swift b/TnT/Projects/DesignSystem/Sources/Navigation/TNavigation.swift index 3ffbbbfd..74757064 100644 --- a/TnT/Projects/DesignSystem/Sources/Navigation/TNavigation.swift +++ b/TnT/Projects/DesignSystem/Sources/Navigation/TNavigation.swift @@ -66,7 +66,7 @@ public struct TNavigation: View { .frame(width: 32, height: 32) case .LTextRButtonTitle(let leftTitle, let pointText, _): - HStack(spacing: 6){ + HStack(spacing: 6) { Text(leftTitle) .typographyStyle(.heading2, with: .neutral900) if let pointText = pointText { @@ -88,7 +88,7 @@ public struct TNavigation: View { Text(centerTitle) .typographyStyle(.heading4, with: .neutral900) .frame(maxWidth: .infinity, alignment: .center) - case .LButton, .LTextRButtonTitle(_, _, _): + case .LButton, .LTextRButtonTitle: EmptyView() } } @@ -120,9 +120,10 @@ public struct TNavigation: View { TButton( title: rightButton, config: .small, - state: .disable(.gray(isEnabled: true))) { + state: .disable(.primary(isEnabled: true))) { rightAction?() } + .frame(width: 90) } } } diff --git a/TnT/Projects/Domain/Sources/DTO/Trainer/TrainerResponseDTO.swift b/TnT/Projects/Domain/Sources/DTO/Trainer/TrainerResponseDTO.swift index c5045111..454e02f8 100644 --- a/TnT/Projects/Domain/Sources/DTO/Trainer/TrainerResponseDTO.swift +++ b/TnT/Projects/Domain/Sources/DTO/Trainer/TrainerResponseDTO.swift @@ -48,3 +48,17 @@ public typealias PostLessonResDTO = EmptyResponse /// PT 수업 완료 처리 응답 DTO public typealias PutCompleteLessonResDTO = EmptyResponse + +public extension ActiveTraineeInfoResDTO { + func dtoToEntity() -> ActiveTraineeInfoResEntity { + return .init( + id: self.id, + name: self.name, + profileImageUrl: self.profileImageUrl, + finishedPtCount: self.finishedPtCount, + totalPtCount: self.totalPtCount, + memo: self.memo, + ptGoals: self.ptGoals + ) + } +} diff --git a/TnT/Projects/Domain/Sources/Entity/TraineeListItemEntity.swift b/TnT/Projects/Domain/Sources/Entity/TraineeListItemEntity.swift index 069c3180..0fba68c9 100644 --- a/TnT/Projects/Domain/Sources/Entity/TraineeListItemEntity.swift +++ b/TnT/Projects/Domain/Sources/Entity/TraineeListItemEntity.swift @@ -23,3 +23,19 @@ public struct TraineeListItemEntity: Equatable, Sendable { self.name = name } } + +/// 관리 중인 회원 목록 응답 DTO +public struct GetActiveTraineesListResEntity: Equatable { + public let trainees: [ActiveTraineeInfoResEntity] +} + +/// 관리 중인 회원 정보 DTO +public struct ActiveTraineeInfoResEntity: Sendable, Equatable { + public let id: Int + public let name: String + public let profileImageUrl: String + public let finishedPtCount: Int + public let totalPtCount: Int + public let memo: String + public let ptGoals: [String] +} diff --git a/TnT/Projects/Domain/Sources/Repository/TrainerRepository.swift b/TnT/Projects/Domain/Sources/Repository/TrainerRepository.swift index 059328dc..796c98e0 100644 --- a/TnT/Projects/Domain/Sources/Repository/TrainerRepository.swift +++ b/TnT/Projects/Domain/Sources/Repository/TrainerRepository.swift @@ -30,7 +30,7 @@ public protocol TrainerRepository { func getMonthlyLessonList(year: Int, month: Int) async throws -> GetMonthlyLessonListResDTO /// 회원 조희 - func getMembersList() async throws -> GetMembersListDTO + func getMembersList() async throws -> GetActiveTraineesListResDTO /// 연결 완료된 트레이니 정보 불러오기 func getConnectedTraineeInfo(trainerId: Int, traineeId: Int) async throws -> GetConnectedTraineeInfoResponseDTO diff --git a/TnT/Projects/Domain/Sources/UseCase/TrainerUseCase.swift b/TnT/Projects/Domain/Sources/UseCase/TrainerUseCase.swift index 297e3aa5..997999ec 100644 --- a/TnT/Projects/Domain/Sources/UseCase/TrainerUseCase.swift +++ b/TnT/Projects/Domain/Sources/UseCase/TrainerUseCase.swift @@ -33,7 +33,7 @@ public struct DefaultTrainerUseCase: TrainerRepository { return try await trainerRepository.getDateSessionList(date: date) } - public func getMembersList() async throws -> GetMembersListDTO { + public func getMembersList() async throws -> GetActiveTraineesListResDTO { return try await trainerRepository.getMembersList() } diff --git a/TnT/Projects/Presentation/Sources/Coordinator/TrainerMainFlow/TrainerMainFlowFeature.swift b/TnT/Projects/Presentation/Sources/Coordinator/TrainerMainFlow/TrainerMainFlowFeature.swift index 069a0d1c..6f49f08c 100644 --- a/TnT/Projects/Presentation/Sources/Coordinator/TrainerMainFlow/TrainerMainFlowFeature.swift +++ b/TnT/Projects/Presentation/Sources/Coordinator/TrainerMainFlow/TrainerMainFlowFeature.swift @@ -56,6 +56,7 @@ public struct TrainerMainFlowFeature { /// 트레이너 회원목록 case .trainerTraineeList: + state.path.append(.trainerManagment(.init())) return .none /// 트레이너 마이페이지 @@ -118,6 +119,8 @@ extension TrainerMainFlowFeature { case connectionComplete(ConnectionCompleteFeature) /// 연결된 트레이니 프로필 case connectedTraineeProfile(ConnectedTraineeProfileFeature) + /// 트레이너 회원 관리 페이지 + case trainerManagment(TrainerManagementFeature) // MARK: MyPage /// 초대코드 발급 diff --git a/TnT/Projects/Presentation/Sources/Coordinator/TrainerMainFlow/TrainerMainFlowView.swift b/TnT/Projects/Presentation/Sources/Coordinator/TrainerMainFlow/TrainerMainFlowView.swift index aea9ec95..0abe1a27 100644 --- a/TnT/Projects/Presentation/Sources/Coordinator/TrainerMainFlow/TrainerMainFlowView.swift +++ b/TnT/Projects/Presentation/Sources/Coordinator/TrainerMainFlow/TrainerMainFlowView.swift @@ -28,7 +28,7 @@ public struct TrainerMainFlowView: View { TrainerMainTabView(store: store) case .addPTSession(let store): TrainerAddPTSessionView(store: store) - + // MARK: Home case .alarmCheck(let store): AlarmCheckView(store: store) @@ -40,6 +40,8 @@ public struct TrainerMainFlowView: View { // MARK: MyPage case .trainerMakeInvitationCodePage(let store): MakeInvitationCodeView(store: store) + case .trainerManagment(let store): + TrainerManagementView(store: store) } } } diff --git a/TnT/Projects/Presentation/Sources/MainTab/Trainer/TrainerMainTabFeature.swift b/TnT/Projects/Presentation/Sources/MainTab/Trainer/TrainerMainTabFeature.swift index 36cc7e8c..b7a8cdb8 100644 --- a/TnT/Projects/Presentation/Sources/MainTab/Trainer/TrainerMainTabFeature.swift +++ b/TnT/Projects/Presentation/Sources/MainTab/Trainer/TrainerMainTabFeature.swift @@ -15,7 +15,7 @@ public struct TrainerMainTabFeature { public enum State: Equatable { case home(TrainerHomeFeature.State) // case feedback - case traineeList + case traineeList(TrainerManagementFeature.State) case myPage(TrainerMypageFeature.State) /// state case와 tabinfo 연결 @@ -69,7 +69,7 @@ public struct TrainerMainTabFeature { // /// 피드백 화면에서 발생하는 액션 처리 // case feedbackAction /// 회원 목록 화면에서 발생하는 액션 처리 - case traineeListAction + case traineeListAction(TrainerManagementFeature.Action) /// 마이페이지 화면에서 발생하는 액션 처리 case myPageAction(TrainerMypageFeature.Action) } @@ -92,8 +92,7 @@ public struct TrainerMainTabFeature { // state = .feedback // return .none case .traineeList: - // TODO: traineeList Feature 작성 후 추가해주세요 - state = .traineeList + state = .traineeList(.init()) return .none case .mypage: state = .myPage(.init()) @@ -134,7 +133,7 @@ extension TrainerMainTabFeature { // /// 트레이너 피드백 // case trainerFeedback /// 트레이너 회원 목록 - case trainerTraineeList + case trainerTraineeList(TrainerManagementFeature.RoutingScreen) /// 트레이너 마이페이지 case trainerMyPage(TrainerMypageFeature.RoutingScreen) } diff --git a/TnT/Projects/Presentation/Sources/MainTab/Trainer/TrainerMainTabView.swift b/TnT/Projects/Presentation/Sources/MainTab/Trainer/TrainerMainTabView.swift index 7e0fd984..79f165fd 100644 --- a/TnT/Projects/Presentation/Sources/MainTab/Trainer/TrainerMainTabView.swift +++ b/TnT/Projects/Presentation/Sources/MainTab/Trainer/TrainerMainTabView.swift @@ -33,8 +33,7 @@ public struct TrainerMainTabView: View { // } case .traineeList: if let store = store.scope(state: \.traineeList, action: \.subFeature.traineeListAction) { - Color.clear - .frame(maxWidth: .infinity, maxHeight: .infinity) + TrainerManagementView(store: store) } case .myPage: if let store = store.scope(state: \.myPage, action: \.subFeature.myPageAction) { diff --git a/TnT/Projects/Presentation/Sources/MyPage/Trainer/TrainerManagementView.swift b/TnT/Projects/Presentation/Sources/MyPage/Trainer/TrainerManagementView.swift index 3ab2aeb2..6c5eb9b2 100644 --- a/TnT/Projects/Presentation/Sources/MyPage/Trainer/TrainerManagementView.swift +++ b/TnT/Projects/Presentation/Sources/MyPage/Trainer/TrainerManagementView.swift @@ -10,62 +10,95 @@ import SwiftUI import ComposableArchitecture import DesignSystem +import Domain struct TrainerManagementView: View { + + public var store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + var body: some View { + ScrollView { + VStack(spacing: 12) { + + Header() + if let trainees = store.traineeList { + TraineeListView(trainees: trainees) + } else { + EmptyListView() + .frame(minHeight: UIScreen.main.bounds.height - 204) + } + } + .onAppear { + store.send(.view(.onappear)) + } + .navigationBarBackButtonHidden() + } + .background(Color.neutral100) + } + + @ViewBuilder + func Header() -> some View { + TNavigation(type: .LTextRButtonTitle( + leftTitle: "내 회원", + pointText: "\(store.traineeList?.count ?? 0)", + rightButton: "회원 초대하기") + ) + .rightTap { + store.send(.view(.goTraineeInvitation)) + } + } + + /// 연결된 회원이 있는 경우 + @ViewBuilder + func TraineeListView(trainees: [ActiveTraineeInfoResEntity]) -> some View { VStack(spacing: 0) { - TNavigation(type: .LTextRButtonTitle( - leftTitle: "내 회원", - pointText: "0", - rightButton: "회원 초대하기") - ) + ForEach(trainees, id: \.id) { trainee in + ListCellView(trainee: trainee) + .padding(.bottom, 16) + } } } /// 연결된 회원이 없는 경우 @ViewBuilder func EmptyListView() -> some View { - VStack(spacing: 0) { - TNavigation(type: .LTextRButtonTitle( - leftTitle: "내 회원", - pointText: "0", - rightButton: "회원 초대하기") - ) - + VStack(spacing: 4) { Spacer() - VStack(spacing: 4) { - Text("아직 연결된 회원이 없어요") - .typographyStyle(.body2Bold, with: Color.neutral600) - Text("추가 버튼을 눌러 회원을 추가해 보세요") - .typographyStyle(.label1Medium, with: Color.neutral400) - } + Text("아직 연결된 회원이 없어요") + .typographyStyle(.body2Bold, with: Color.neutral600) + Text("추가 버튼을 눌러 회원을 추가해 보세요") + .typographyStyle(.label1Medium, with: Color.neutral400) Spacer() } } @ViewBuilder - func ListCellView() -> some View { + func ListCellView(trainee: ActiveTraineeInfoResEntity) -> some View { VStack(spacing: 12) { HStack { HStack { - ProfileImageView(imageURL: "") + ProfileImageView(imageURL: trainee.profileImageUrl) VStack(spacing: 12) { - Text("") + Text(trainee.name) .typographyStyle(.body1Bold, with: Color.neutral900) - Text("") + Text(trainee.ptGoals.joined(separator: ", ")) .typographyStyle(.label2Medium, with: Color.neutral500) } } Spacer() - TChip(leadingEmoji: "💪", title: "", style: .blue) + TChip(leadingEmoji: "💪", title: "\(trainee.finishedPtCount)", style: .blue) } VStack(spacing: 5) { Text("메모") .typographyStyle(.label2Bold, with: Color.neutral600) - Text("") + Text(trainee.memo) .typographyStyle(.label2Medium, with: Color.neutral500) } } @@ -116,7 +149,3 @@ extension TrainerManagementView { } } } - -#Preview { - TrainerManagementView() -} diff --git a/TnT/Projects/Presentation/Sources/MyPage/Trainer/TrainerManagmentFeature.swift b/TnT/Projects/Presentation/Sources/MyPage/Trainer/TrainerManagmentFeature.swift index 4778c084..4c4c3b4b 100644 --- a/TnT/Projects/Presentation/Sources/MyPage/Trainer/TrainerManagmentFeature.swift +++ b/TnT/Projects/Presentation/Sources/MyPage/Trainer/TrainerManagmentFeature.swift @@ -11,10 +11,10 @@ import ComposableArchitecture import Domain @Reducer -struct TrainerManagementFeature { +public struct TrainerManagementFeature { @ObservableState - struct State { - var memberList: GetMembersListEntity + public struct State: Equatable { + var traineeList: [ActiveTraineeInfoResEntity]? } @Dependency(\.trainerRepoUseCase) private var trainerRepoUseCase: TrainerRepository @@ -27,7 +27,14 @@ struct TrainerManagementFeature { @CasePathable public enum View: Sendable { - case getMembersList + /// 관리중인 회원목록 가져오기 + case getTraineeList + /// 회원목록 적용 + case setTraineeList([ActiveTraineeInfoResEntity]) + /// 화면 진입시 + case onappear + /// 회원 초대하기로 이동 + case goTraineeInvitation } } @@ -36,12 +43,35 @@ struct TrainerManagementFeature { public var body: some ReducerOf { Reduce { state, action in switch action { + case .view(.onappear): + return .run { send in + await send(.view(.getTraineeList)) + } + case .setNavigating: return .none - case .view(.getMembersList): + case .view(.getTraineeList): + return .run { send in + let result: GetActiveTraineesListResDTO = try await trainerRepoUseCase.getMembersList() + let trainee: [ActiveTraineeInfoResEntity] = result.trainees.map { $0.dtoToEntity() } + await send(.view(.setTraineeList(trainee))) + + } + case .view(.setTraineeList(let trainees)): + state.traineeList = trainees + return .none + + case .view(.goTraineeInvitation): + print("트레이너 내 회원 > 회원 초대하기로 이동") return .none } } } } +extension TrainerManagementFeature { + public enum RoutingScreen: Sendable { + /// 회원추가 + case addTrainee + } +} From e37e5ba79fd65e90a8eec20ff5881f227ef8d2cc Mon Sep 17 00:00:00 2001 From: Park Seo Yeon Date: Thu, 13 Feb 2025 23:49:56 +0900 Subject: [PATCH 2/4] =?UTF-8?q?[Fix]=20=EB=92=A4=EB=A1=9C=EA=B0=80?= =?UTF-8?q?=EA=B8=B0=20=EC=A0=9C=EC=8A=A4=EC=B2=98=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GestureNavigation/GestureNavigation.swift | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 TnT/Projects/Presentation/Sources/GestureNavigation/GestureNavigation.swift diff --git a/TnT/Projects/Presentation/Sources/GestureNavigation/GestureNavigation.swift b/TnT/Projects/Presentation/Sources/GestureNavigation/GestureNavigation.swift new file mode 100644 index 00000000..d5bb5729 --- /dev/null +++ b/TnT/Projects/Presentation/Sources/GestureNavigation/GestureNavigation.swift @@ -0,0 +1,21 @@ +// +// 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 + } +} From e10061354a140c4b74bdfddbb36934f3fd039281 Mon Sep 17 00:00:00 2001 From: Park Seo Yeon Date: Thu, 13 Feb 2025 23:58:14 +0900 Subject: [PATCH 3/4] =?UTF-8?q?[Feat]=20=EC=95=8C=EB=9E=8C=20=EC=97=86?= =?UTF-8?q?=EC=9D=84=EB=95=8C=20=ED=99=94=EB=A9=B4=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Alarm/AlarmCheckView.swift | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/TnT/Projects/Presentation/Sources/Alarm/AlarmCheckView.swift b/TnT/Projects/Presentation/Sources/Alarm/AlarmCheckView.swift index 4eda7cba..3172af47 100644 --- a/TnT/Projects/Presentation/Sources/Alarm/AlarmCheckView.swift +++ b/TnT/Projects/Presentation/Sources/Alarm/AlarmCheckView.swift @@ -31,28 +31,45 @@ public struct AlarmCheckView: View { leftAction: { send(.tapNavBackButton) } ) - ScrollView { + ScrollView(showsIndicators: false) { AlarmList() Spacer() } } .navigationBarBackButtonHidden(true) + } // MARK: - Sections @ViewBuilder private func AlarmList() -> some View { VStack(spacing: 0) { - ForEach(store.alarmList, id: \.alarmId) { item in - AlarmListItem( - alarmTypeText: item.alarmTypeText, - alarmMainText: item.alarmMainText, - alarmTimeText: item.alarmDate.timeAgoDisplay(), - alarmSeenBefore: item.alarmSeenBefore - ) + if store.alarmList.isEmpty { + EmptyView() + } else { + ForEach(store.alarmList, id: \.alarmId) { item in + AlarmListItem( + alarmTypeText: item.alarmTypeText, + alarmMainText: item.alarmMainText, + alarmTimeText: item.alarmDate.timeAgoDisplay(), + alarmSeenBefore: item.alarmSeenBefore + ) + } } } } + + @ViewBuilder + private func EmptyView() -> some View { + VStack { + Spacer() + Text("최근 받은 알림이 없어요") + .typographyStyle(.label1Medium, with: Color.neutral400) + .frame(maxWidth: .infinity, alignment: .center) + Spacer() + } + .frame(minHeight: UIScreen.main.bounds.height - 104) + } } private extension AlarmCheckView { From 7874ba75d34b26c0fb5038d3841a7f42d15d7b01 Mon Sep 17 00:00:00 2001 From: Park Seo Yeon Date: Fri, 14 Feb 2025 01:28:39 +0900 Subject: [PATCH 4/4] =?UTF-8?q?[Refactor]=20=ED=8C=8C=EC=9D=BC=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Trainer => TrainerManagement}/TrainerManagementView.swift | 0 .../Trainer => TrainerManagement}/TrainerManagmentFeature.swift | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename TnT/Projects/Presentation/Sources/{MyPage/Trainer => TrainerManagement}/TrainerManagementView.swift (100%) rename TnT/Projects/Presentation/Sources/{MyPage/Trainer => TrainerManagement}/TrainerManagmentFeature.swift (100%) diff --git a/TnT/Projects/Presentation/Sources/MyPage/Trainer/TrainerManagementView.swift b/TnT/Projects/Presentation/Sources/TrainerManagement/TrainerManagementView.swift similarity index 100% rename from TnT/Projects/Presentation/Sources/MyPage/Trainer/TrainerManagementView.swift rename to TnT/Projects/Presentation/Sources/TrainerManagement/TrainerManagementView.swift diff --git a/TnT/Projects/Presentation/Sources/MyPage/Trainer/TrainerManagmentFeature.swift b/TnT/Projects/Presentation/Sources/TrainerManagement/TrainerManagmentFeature.swift similarity index 100% rename from TnT/Projects/Presentation/Sources/MyPage/Trainer/TrainerManagmentFeature.swift rename to TnT/Projects/Presentation/Sources/TrainerManagement/TrainerManagmentFeature.swift