diff --git a/TnT/Projects/Data/Sources/Network/Service/Trainer/TrainerRepositoryImpl.swift b/TnT/Projects/Data/Sources/Network/Service/Trainer/TrainerRepositoryImpl.swift
index 493f409a..e6729693 100644
--- a/TnT/Projects/Data/Sources/Network/Service/Trainer/TrainerRepositoryImpl.swift
+++ b/TnT/Projects/Data/Sources/Network/Service/Trainer/TrainerRepositoryImpl.swift
@@ -50,13 +50,6 @@ public struct TrainerRepositoryImpl: TrainerRepository {
return try await networkService.request(TrainerTargetType.getMonthlyLessonList(year: year, month: month), decodingType: GetMonthlyLessonListResDTO.self)
}
- public func getMembersList() async throws -> GetActiveTraineesListResDTO {
- return try await networkService.request(
- TrainerTargetType.getMemebersList,
- decodingType: GetActiveTraineesListResDTO.self
- )
- }
-
public func getConnectedTraineeInfo(trainerId: Int, traineeId: Int) async throws -> GetConnectedTraineeInfoResponseDTO {
return try await networkService.request(TrainerTargetType.getConnectedTraineeInfo(trainerId: trainerId, traineeId: traineeId), decodingType: GetConnectedTraineeInfoResponseDTO.self)
}
diff --git a/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_check_mark_green.imageset/Contents.json b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_check_mark_green.imageset/Contents.json
new file mode 100644
index 00000000..6c472576
--- /dev/null
+++ b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_check_mark_green.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "icn_check_mark_green.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_check_mark_green.imageset/icn_check_mark_green.svg b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_check_mark_green.imageset/icn_check_mark_green.svg
new file mode 100644
index 00000000..69a51a56
--- /dev/null
+++ b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_check_mark_green.imageset/icn_check_mark_green.svg
@@ -0,0 +1,9 @@
+
diff --git a/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_plus_gray.imageset/Contents.json b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_plus_gray.imageset/Contents.json
new file mode 100644
index 00000000..853622df
--- /dev/null
+++ b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_plus_gray.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "icn_plus_gray.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "icn_plus_gray@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "icn_plus_gray@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_plus_gray.imageset/icn_plus_gray.png b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_plus_gray.imageset/icn_plus_gray.png
new file mode 100644
index 00000000..8f07edcf
Binary files /dev/null and b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_plus_gray.imageset/icn_plus_gray.png differ
diff --git a/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_plus_gray.imageset/icn_plus_gray@2x.png b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_plus_gray.imageset/icn_plus_gray@2x.png
new file mode 100644
index 00000000..9b1633f5
Binary files /dev/null and b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_plus_gray.imageset/icn_plus_gray@2x.png differ
diff --git a/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_plus_gray.imageset/icn_plus_gray@3x.png b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_plus_gray.imageset/icn_plus_gray@3x.png
new file mode 100644
index 00000000..59e4e830
Binary files /dev/null and b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_plus_gray.imageset/icn_plus_gray@3x.png differ
diff --git a/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_plus_white.imageset/Contents.json b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_plus_white.imageset/Contents.json
new file mode 100644
index 00000000..b65b482b
--- /dev/null
+++ b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_plus_white.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "icn_plus_white.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_plus_white.imageset/icn_plus_white.svg b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_plus_white.imageset/icn_plus_white.svg
new file mode 100644
index 00000000..5019f950
--- /dev/null
+++ b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_plus_white.imageset/icn_plus_white.svg
@@ -0,0 +1,4 @@
+
diff --git a/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_write_gray.imageset/Contents.json b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_write_gray.imageset/Contents.json
new file mode 100644
index 00000000..e5c5eaee
--- /dev/null
+++ b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_write_gray.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "icn_write_gray.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_write_gray.imageset/icn_write_gray.svg b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_write_gray.imageset/icn_write_gray.svg
new file mode 100644
index 00000000..cb2f7be1
--- /dev/null
+++ b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_write_gray.imageset/icn_write_gray.svg
@@ -0,0 +1,5 @@
+
diff --git a/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarRepresentable.swift b/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarRepresentable.swift
index f67d5cb3..e8872dda 100644
--- a/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarRepresentable.swift
+++ b/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarRepresentable.swift
@@ -64,7 +64,6 @@ public struct TCalendarRepresentable: UIViewRepresentable {
calendar.appearance.titleDefaultColor = .clear
calendar.calendarWeekdayView.weekdayLabels[0].textColor = UIColor(.red500)
-
return calendar
}
diff --git a/TnT/Projects/DesignSystem/Sources/DesignSystem/Image+DesignSystem.swift b/TnT/Projects/DesignSystem/Sources/DesignSystem/Image+DesignSystem.swift
index 239b95b1..6e7f95d6 100644
--- a/TnT/Projects/DesignSystem/Sources/DesignSystem/Image+DesignSystem.swift
+++ b/TnT/Projects/DesignSystem/Sources/DesignSystem/Image+DesignSystem.swift
@@ -29,9 +29,11 @@ public extension ImageResource {
static let icnMypageFilled: ImageResource = DesignSystemAsset.icnMypageFilled.imageResource
static let icnWriteWhite: ImageResource = DesignSystemAsset.icnWriteWhite.imageResource
static let icnWriteBlack: ImageResource = DesignSystemAsset.icnWriteBlack.imageResource
+ static let icnWriteGray: ImageResource = DesignSystemAsset.icnWriteGray.imageResource
static let icnRadioButtonUnselected: ImageResource = DesignSystemAsset.icnRadioButtonUnselected.imageResource
static let icnRadioButtonSelected: ImageResource = DesignSystemAsset.icnRadioButtonSelected.imageResource
static let icnCheckMarkEmpty: ImageResource = DesignSystemAsset.icnCheckMarkEmpty.imageResource
+ static let icnCheckMarkGreen: ImageResource = DesignSystemAsset.icnCheckMarkGreen.imageResource
static let icnCheckMarkFilled: ImageResource = DesignSystemAsset.icnCheckMarkFilled.imageResource
static let icnCheckButtonUnselected: ImageResource = DesignSystemAsset.icnCheckButtonUnselected.imageResource
static let icnCheckButtonSelected: ImageResource = DesignSystemAsset.icnCheckButtonSelected.imageResource
@@ -50,6 +52,7 @@ public extension ImageResource {
static let icnWriteBlackFilled: ImageResource = DesignSystemAsset.icnWriteBlackFilled.imageResource
static let icnPlus: ImageResource = DesignSystemAsset.icnPlus.imageResource
static let icnPlusEmpty: ImageResource = DesignSystemAsset.icnPlusEmpty.imageResource
+ static let icnPlusGray: ImageResource = DesignSystemAsset.icnPlusGray.imageResource
static let icnAlarm: ImageResource = DesignSystemAsset.icnAlarm.imageResource
static let icnCalendar: ImageResource = DesignSystemAsset.icnCalendar.imageResource
static let icnDelete24px: ImageResource = DesignSystemAsset.icnDelete24.imageResource
diff --git a/TnT/Projects/Domain/Sources/DTO/Trainer/TrainerHomeResponseDTO.swift b/TnT/Projects/Domain/Sources/DTO/Trainer/TrainerHomeResponseDTO.swift
index dcf33eb1..62962f2c 100644
--- a/TnT/Projects/Domain/Sources/DTO/Trainer/TrainerHomeResponseDTO.swift
+++ b/TnT/Projects/Domain/Sources/DTO/Trainer/TrainerHomeResponseDTO.swift
@@ -29,6 +29,7 @@ public struct SessonDTO: Decodable {
public let ptLessonId: String
public let traineeId: String
public let traineeName: String
+ public let traineeProfileImageUrl: String
public let session: Int
public let startTime: String
public let endTime: String
@@ -38,6 +39,7 @@ public struct SessonDTO: Decodable {
ptLessonId: String,
traineeId: String,
traineeName: String,
+ traineeProfileImageUrl: String,
session: Int,
startTime: String,
endTime: String,
@@ -46,6 +48,7 @@ public struct SessonDTO: Decodable {
self.ptLessonId = ptLessonId
self.traineeId = traineeId
self.traineeName = traineeName
+ self.traineeProfileImageUrl = traineeProfileImageUrl
self.session = session
self.startTime = startTime
self.endTime = endTime
@@ -75,6 +78,7 @@ public struct SessonEntity: Equatable, Encodable {
public let ptLessonId: String
public let traineeId: String
public let traineeName: String
+ public let traineeProfileImageUrl: String
public let session: Int
public let startTime: String
public let endTime: String
@@ -84,6 +88,7 @@ public struct SessonEntity: Equatable, Encodable {
ptLessonId: String,
traineeId: String,
traineeName: String,
+ traineeProfileImageUrl: String,
session: Int,
startTime: String,
endTime: String,
@@ -92,6 +97,7 @@ public struct SessonEntity: Equatable, Encodable {
self.ptLessonId = ptLessonId
self.traineeId = traineeId
self.traineeName = traineeName
+ self.traineeProfileImageUrl = traineeProfileImageUrl
self.session = session
self.startTime = startTime
self.endTime = endTime
@@ -122,14 +128,15 @@ public extension GetDateSessionListDTO {
// MARK: - SessonDTO -> SessonEntity
public extension SessonDTO {
- public func toEntity() -> SessonEntity {
+ func toEntity() -> SessonEntity {
return SessonEntity(
ptLessonId: self.ptLessonId,
traineeId: self.traineeId,
traineeName: self.traineeName,
+ traineeProfileImageUrl: self.traineeProfileImageUrl,
session: self.session,
- startTime: self.startTime,
- endTime: self.endTime,
+ startTime: self.startTime.toDate(format: .ISO8601)?.toString(format: .a_HHmm) ?? "",
+ endTime: self.endTime.toDate(format: .ISO8601)?.toString(format: .a_HHmm) ?? "",
isCompleted: self.isCompleted
)
}
@@ -140,6 +147,7 @@ public extension SessonDTO {
ptLessonId: self.ptLessonId,
traineeId: self.traineeId,
traineeName: self.traineeName,
+ traineeProfileImageUrl: self.traineeProfileImageUrl,
session: self.session,
startTime: self.startTime,
endTime: self.endTime,
@@ -166,6 +174,7 @@ public extension SessonEntity {
ptLessonId: self.ptLessonId,
traineeId: self.traineeId,
traineeName: self.traineeName,
+ traineeProfileImageUrl: self.traineeProfileImageUrl,
session: self.session,
startTime: self.startTime,
endTime: self.endTime,
diff --git a/TnT/Projects/Domain/Sources/Repository/TrainerRepository.swift b/TnT/Projects/Domain/Sources/Repository/TrainerRepository.swift
index 796c98e0..7e86967d 100644
--- a/TnT/Projects/Domain/Sources/Repository/TrainerRepository.swift
+++ b/TnT/Projects/Domain/Sources/Repository/TrainerRepository.swift
@@ -29,9 +29,6 @@ public protocol TrainerRepository {
/// 달력 스케줄 카운트 표시에 필요한 PT 리스트 불러오기
func getMonthlyLessonList(year: Int, month: Int) async throws -> GetMonthlyLessonListResDTO
- /// 회원 조희
- 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 997999ec..7401b58e 100644
--- a/TnT/Projects/Domain/Sources/UseCase/TrainerUseCase.swift
+++ b/TnT/Projects/Domain/Sources/UseCase/TrainerUseCase.swift
@@ -33,10 +33,6 @@ public struct DefaultTrainerUseCase: TrainerRepository {
return try await trainerRepository.getDateSessionList(date: date)
}
- public func getMembersList() async throws -> GetActiveTraineesListResDTO {
- return try await trainerRepository.getMembersList()
- }
-
public func getConnectedTraineeInfo(trainerId: Int, traineeId: Int) async throws -> GetConnectedTraineeInfoResponseDTO {
return try await trainerRepository.getConnectedTraineeInfo(trainerId: trainerId, traineeId: traineeId)
}
diff --git a/TnT/Projects/Presentation/Sources/Coordinator/TrainerMainFlow/TrainerMainFlowFeature.swift b/TnT/Projects/Presentation/Sources/Coordinator/TrainerMainFlow/TrainerMainFlowFeature.swift
index 6f49f08c..12493df9 100644
--- a/TnT/Projects/Presentation/Sources/Coordinator/TrainerMainFlow/TrainerMainFlowFeature.swift
+++ b/TnT/Projects/Presentation/Sources/Coordinator/TrainerMainFlow/TrainerMainFlowFeature.swift
@@ -55,9 +55,12 @@ public struct TrainerMainFlowFeature {
}
/// 트레이너 회원목록
- case .trainerTraineeList:
- state.path.append(.trainerManagment(.init()))
- return .none
+ case .trainerTraineeList(let screen):
+ switch screen {
+ case .addTrainee:
+ state.path.append(.addTrainee(.init()))
+ return .none
+ }
/// 트레이너 마이페이지
case .trainerMyPage(let screen):
@@ -119,8 +122,10 @@ extension TrainerMainFlowFeature {
case connectionComplete(ConnectionCompleteFeature)
/// 연결된 트레이니 프로필
case connectedTraineeProfile(ConnectedTraineeProfileFeature)
- /// 트레이너 회원 관리 페이지
- case trainerManagment(TrainerManagementFeature)
+
+ // MARK: - 회원 목록
+ /// 회원 추가
+ case addTrainee(AddTraineeFeature)
// MARK: MyPage
/// 초대코드 발급
diff --git a/TnT/Projects/Presentation/Sources/Coordinator/TrainerMainFlow/TrainerMainFlowView.swift b/TnT/Projects/Presentation/Sources/Coordinator/TrainerMainFlow/TrainerMainFlowView.swift
index 0abe1a27..c8887b40 100644
--- a/TnT/Projects/Presentation/Sources/Coordinator/TrainerMainFlow/TrainerMainFlowView.swift
+++ b/TnT/Projects/Presentation/Sources/Coordinator/TrainerMainFlow/TrainerMainFlowView.swift
@@ -36,12 +36,14 @@ public struct TrainerMainFlowView: View {
ConnectionCompleteView(store: store)
case .connectedTraineeProfile(let store):
ConnectedTraineeProfileView(store: store)
+
+ // MARK: - TraineeList
+ case .addTrainee(let store):
+ AddTraineeView(store: store)
// MARK: MyPage
case .trainerMakeInvitationCodePage(let store):
MakeInvitationCodeView(store: store)
- case .trainerManagment(let store):
- TrainerManagementView(store: store)
}
}
}
diff --git a/TnT/Projects/Presentation/Sources/Home/Trainer/TrainerHomeFeature.swift b/TnT/Projects/Presentation/Sources/Home/Trainer/TrainerHomeFeature.swift
index 43a7fd70..2055d32d 100644
--- a/TnT/Projects/Presentation/Sources/Home/Trainer/TrainerHomeFeature.swift
+++ b/TnT/Projects/Presentation/Sources/Home/Trainer/TrainerHomeFeature.swift
@@ -112,6 +112,8 @@ public struct TrainerHomeFeature {
case calendarDateTap
/// 탭한 일자 api 형태에 맞춰 변환하기(yyyy-mm-dd)
case settingSessionList(sessions: GetDateSessionListEntity)
+ /// 수업 완료 후 토스트 메시지
+ case completeToastMessage
}
}
@@ -126,7 +128,7 @@ public struct TrainerHomeFeature {
case .view(let action):
switch action {
case .binding(\.selectedDate):
- print("state.events[state.selectedDate] \(state.events[state.selectedDate])")
+ // print("state.events[state.selectedDate] \(state.events[state.selectedDate])")
return .none
case .binding:
@@ -136,8 +138,15 @@ public struct TrainerHomeFeature {
return .send(.setNavigating(.alarmPage))
case .tapSessionCompleted(let id):
- // TODO: 네비게이션 연결 시 추가
- print("tapSessionCompleted otLessionID \(id)")
+ guard let id = Int(id) else { return .none }
+ return .run { send in
+ let result: PutCompleteLessonResDTO = try await trainerRepoUseCase.putCompleteLesson(lessonId: id)
+ await send(.view(.completeToastMessage))
+ await send(.view(.calendarDateTap))
+ }
+
+ case .completeToastMessage:
+ NotificationCenter.default.post(toast: .init(presentType: .image(.icnCheckMarkGreen), message: "PT 수업을 완료했어요"))
return .none
case .tapAddSessionButton:
@@ -175,7 +184,12 @@ public struct TrainerHomeFeature {
state.view_isPopUpPresented = true
}
- return .send(.view(.fetchMonthlyLessons(year: year, month: month)))
+ return .concatenate(
+ .send(.view(.fetchMonthlyLessons(year: month == 1 ? year-1 : year, month: month == 1 ? 12 : month-1))),
+ .send(.view(.fetchMonthlyLessons(year: year, month: month))),
+ .send(.view(.fetchMonthlyLessons(year: year, month: month+1))),
+ .send(.view(.calendarDateTap))
+ )
case .fetchMonthlyLessons(year: let year, month: let month):
return .run { send in
@@ -206,7 +220,7 @@ public struct TrainerHomeFeature {
return .none
case .calendarDateTap:
- let formattedDate = TDateFormatUtility.formatter(for: .yyyyMMdd).string(from: state.selectedDate)
+ let formattedDate: String = TDateFormatUtility.formatter(for: .yyyyMMdd).string(from: state.selectedDate)
return .run { send in
do {
diff --git a/TnT/Projects/Presentation/Sources/Home/Trainer/TrainerHomeView.swift b/TnT/Projects/Presentation/Sources/Home/Trainer/TrainerHomeView.swift
index 70232296..6ff563f1 100644
--- a/TnT/Projects/Presentation/Sources/Home/Trainer/TrainerHomeView.swift
+++ b/TnT/Projects/Presentation/Sources/Home/Trainer/TrainerHomeView.swift
@@ -76,9 +76,9 @@ public struct TrainerHomeView: View {
events: store.events
)
.onChange(of: store.state.selectedDate, { oldValue, newValue in
- let startOfDay = Calendar.current.startOfDay(for: newValue)
+ let startOfDay: Date = Calendar.current.startOfDay(for: newValue)
store.selectedDate = startOfDay
- store.send(.view(.calendarDateTap))
+ send(.calendarDateTap)
})
.padding(.horizontal, 20)
}
@@ -99,7 +99,7 @@ public struct TrainerHomeView: View {
HStack(spacing: 0) {
Text("🧨")
.typographyStyle(.label1Medium)
- Text("\(store.sessionCount)")
+ Text("\(store.tappedsessionInfo?.lessons.count ?? 0)")
.typographyStyle(.label2Bold, with: Color.red500)
Text("개의 수업이 있어요")
.typographyStyle(.label2Medium, with: Color.neutral800)
@@ -137,10 +137,9 @@ public struct TrainerHomeView: View {
.frame(width: 126, height: 58)
.overlay {
HStack(spacing: 4) {
- Image(.icnPlus)
+ Image(.icnPlusGray)
.resizable()
.frame(width: 24, height: 24)
- .tint(Color.common0)
Text("수업추가")
.typographyStyle(.body1Medium, with: .neutral50)
}
@@ -240,19 +239,29 @@ extension TrainerHomeView {
Spacer()
Image(.icnClock)
Text("\(session.startTime) ~ \(session.endTime)")
+ .typographyStyle(.label2Medium, with: .neutral500)
+ .frame(maxWidth: .infinity)
+ }
+
+ HStack(spacing: 6) {
+ ProfileImageView(imageURL: session.traineeProfileImageUrl)
+ Text(session.traineeName)
+ .typographyStyle(.body1Bold, with: .neutral800)
+ .frame(maxWidth: .infinity, alignment: .leading)
}
- Text(session.traineeName)
if session.isCompleted {
Button {
onTap?()
} label: {
HStack(spacing: 4) {
- Image(.icnWriteWhite)
+ Image(.icnWriteGray)
Text("PT 수업 기록 남기기")
.typographyStyle(.label2Medium, with: .neutral400)
}
.frame(maxWidth: .infinity)
+ .padding(.vertical, 12)
+ .background(Color.neutral100)
.clipShape(RoundedRectangle(cornerRadius: 8))
}
}
@@ -262,8 +271,47 @@ extension TrainerHomeView {
.clipShape(RoundedRectangle(cornerRadius: 12))
.frame(maxWidth: .infinity)
}
- .padding(.horizontal, 20)
.padding(.bottom, 12)
}
}
+
+ struct ProfileImageView: View {
+ let imageURL: String?
+
+ var body: some View {
+ if let urlString = imageURL, let url = URL(string: urlString) {
+ AsyncImage(url: url) { phase in
+ switch phase {
+ case .empty:
+ ProgressView()
+ .tint(.red500)
+ .frame(width: 24, height: 24)
+
+ case .success(let image):
+ image
+ .resizable()
+ .aspectRatio(contentMode: .fill)
+ .frame(width: 24, height: 24)
+ .clipShape(Circle())
+
+ case .failure:
+ Image(.imgDefaultTrainerImage)
+ .resizable()
+ .aspectRatio(contentMode: .fill)
+ .frame(width: 24, height: 24)
+ .clipShape(Circle())
+
+ @unknown default:
+ EmptyView()
+ }
+ }
+ } else {
+ Image(.imgDefaultTrainerImage)
+ .resizable()
+ .scaledToFill()
+ .frame(width: 132, height: 132)
+ .clipShape(Circle())
+ }
+ }
+ }
}
diff --git a/TnT/Projects/Presentation/Sources/MainTab/Trainer/TrainerMainTabFeature.swift b/TnT/Projects/Presentation/Sources/MainTab/Trainer/TrainerMainTabFeature.swift
index b7a8cdb8..1b8d82c5 100644
--- a/TnT/Projects/Presentation/Sources/MainTab/Trainer/TrainerMainTabFeature.swift
+++ b/TnT/Projects/Presentation/Sources/MainTab/Trainer/TrainerMainTabFeature.swift
@@ -105,6 +105,8 @@ public struct TrainerMainTabFeature {
switch internalAction {
case .homeAction(.setNavigating(let screen)):
return .send(.setNavigating(.trainerHome(screen)))
+ case .traineeListAction(.setNavigating(let screen)):
+ return .send(.setNavigating(.trainerTraineeList(screen)))
case .myPageAction(.setNavigating(let screen)):
return .send(.setNavigating(.trainerMyPage(screen)))
default:
@@ -118,6 +120,9 @@ public struct TrainerMainTabFeature {
.ifCaseLet(\.home, action: \.subFeature.homeAction) {
TrainerHomeFeature()
}
+ .ifCaseLet(\.traineeList, action: \.subFeature.traineeListAction, then: {
+ TrainerManagementFeature()
+ })
.ifCaseLet(\.myPage, action: \.subFeature.myPageAction) {
TrainerMypageFeature()
}
diff --git a/TnT/Projects/Presentation/Sources/Onboarding/Common/LoginFeature.swift b/TnT/Projects/Presentation/Sources/Onboarding/Common/LoginFeature.swift
index 685b6b7a..6ac03a6a 100644
--- a/TnT/Projects/Presentation/Sources/Onboarding/Common/LoginFeature.swift
+++ b/TnT/Projects/Presentation/Sources/Onboarding/Common/LoginFeature.swift
@@ -129,7 +129,7 @@ public struct LoginFeature {
return .run { send in
do {
- let result = try await userUseCaseRepo.postSocialLogin(post)
+ let result: PostSocialLoginResDTO = try await userUseCaseRepo.postSocialLogin(post)
saveSessionId(result.sessionId)
switch result.memberType {
diff --git a/TnT/Projects/Presentation/Sources/TrainerManagement/AddTraineeFeature.swift b/TnT/Projects/Presentation/Sources/TrainerManagement/AddTraineeFeature.swift
new file mode 100644
index 00000000..32353eba
--- /dev/null
+++ b/TnT/Projects/Presentation/Sources/TrainerManagement/AddTraineeFeature.swift
@@ -0,0 +1,106 @@
+//
+// AddTraineeFeature.swift
+// Presentation
+//
+// Created by 박서연 on 2/14/25.
+// Copyright © 2025 yapp25thTeamTnT. All rights reserved.
+//
+
+import SwiftUI
+import ComposableArchitecture
+
+import DesignSystem
+import DIContainer
+
+@Reducer
+public struct AddTraineeFeature {
+
+ @ObservableState
+ public struct State: Equatable {
+ var invitationCode: String = ""
+
+ public init() { }
+ }
+
+ @Dependency(\.trainerRepoUseCase) var trainerRepoUseCase
+
+ public enum Action: ViewAction {
+ /// 화면 내 발생 액션 처리
+ case view(View)
+ /// api 콜 액션 처리
+ case api(APIAction)
+ /// 초대 코드 설정
+ case setInvitationCode(String)
+ /// 네비게이션 처리
+ case setNavigation
+
+ @CasePathable
+ public enum View: Sendable, BindableAction {
+ /// 바인딩 액션 처리
+ case binding(BindingAction)
+ /// 코드 재발급 버튼 탭
+ case tappedReissuanceButton
+ /// 코드 카피 영역 탭
+ case tapCodeToCopy
+ /// 화면 표시될 때
+ case onAppear
+ }
+
+ @CasePathable
+ public enum APIAction: Sendable {
+ /// 초대 코드 불러오기 API
+ case getInvitationCode
+ /// 초대 코드 재발급하기 API
+ case reissueInvitationCode
+ }
+ }
+
+ public init() { }
+
+ public var body: some ReducerOf {
+ BindingReducer(action: \.view)
+
+ Reduce { state, action in
+ switch action {
+ case .view(let view):
+ switch view {
+ case .binding:
+ return .none
+
+ case .tapCodeToCopy:
+ UIPasteboard.general.string = state.invitationCode
+ NotificationCenter.default.post(toast: .init(presentType: .text("⚠"), message: "코드가 복사되었어요!"))
+ return .none
+
+ case .tappedReissuanceButton:
+ return .send(.api(.reissueInvitationCode))
+
+ case .onAppear:
+ return .send(.api(.getInvitationCode))
+ }
+
+ case .api(let action):
+ switch action {
+ case .getInvitationCode:
+ return .run { send in
+ let result = try await trainerRepoUseCase.getTheFirstInvitationCode()
+ await send(.setInvitationCode(result.invitationCode))
+ }
+
+ case .reissueInvitationCode:
+ return .run { send in
+ let result = try await trainerRepoUseCase.getReissuanceInvitationCode()
+ await send(.setInvitationCode(result.invitationCode))
+ }
+ }
+
+ case .setInvitationCode(let code):
+ state.invitationCode = code
+ return .none
+
+ case .setNavigation:
+ return .none
+ }
+ }
+ }
+}
diff --git a/TnT/Projects/Presentation/Sources/TrainerManagement/AddTraineeView.swift b/TnT/Projects/Presentation/Sources/TrainerManagement/AddTraineeView.swift
new file mode 100644
index 00000000..6316baf8
--- /dev/null
+++ b/TnT/Projects/Presentation/Sources/TrainerManagement/AddTraineeView.swift
@@ -0,0 +1,90 @@
+//
+// AddTraineeView.swift
+// Presentation
+//
+// Created by 박서연 on 2/14/25.
+// Copyright © 2025 yapp25thTeamTnT. All rights reserved.
+//
+
+import SwiftUI
+import ComposableArchitecture
+
+import DesignSystem
+
+@ViewAction(for: AddTraineeFeature.self)
+public struct AddTraineeView: View {
+
+ @Environment(\.dismiss) var dismiss
+ public let store: StoreOf
+
+ public init(store: StoreOf) {
+ self.store = store
+ }
+
+ public var body: some View {
+ VStack(spacing: 0) {
+ Header()
+ InvitationCode()
+ }
+ .navigationBarBackButtonHidden()
+ .onAppear { send(.onAppear) }
+ }
+
+ @ViewBuilder
+ private func Header() -> some View {
+ TNavigation(
+ type: .LButtonWithTitle(
+ leftImage: .icnArrowLeft,
+ centerTitle: "회원추가")
+ )
+ .leftTap {
+ dismiss()
+ }
+ }
+
+ @ViewBuilder
+ private func InvitationCode() -> some View {
+ VStack(alignment: .leading, spacing: 0) {
+ Text("생성된 초대코드로\n트레이니가 로그인할 수 있어요")
+ .typographyStyle(.heading2, with: .neutral950)
+
+ Spacer().frame(height: 48)
+
+ VStack(spacing: 15) {
+ HStack(spacing: 0) {
+ Text("내 초대 코드")
+ .typographyStyle(.body1Bold, with: .neutral900)
+ Text("*")
+ .typographyStyle(.body1Bold, with: .red500)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+
+ HStack(spacing: 0) {
+ ZStack(alignment: .bottom) {
+ Text("\(store.invitationCode)")
+ .typographyStyle(.body1Medium, with: .neutral600)
+ .padding(8)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .onTapGesture {
+ send(.tapCodeToCopy)
+ }
+
+ TDivider(height: 1, color: .neutral300)
+ }
+
+ TButton(
+ title: "코드 재발급",
+ config: .small,
+ state: .default(.gray(isEnabled: true))
+ ) {
+ send(.tappedReissuanceButton)
+ }
+ .frame(width: 82)
+ }
+ }
+
+ Spacer()
+ }
+ .padding(.horizontal, 24)
+ }
+}
diff --git a/TnT/Projects/Presentation/Sources/TrainerManagement/TrainerManagementView.swift b/TnT/Projects/Presentation/Sources/TrainerManagement/TrainerManagementView.swift
index 6c5eb9b2..dd224e25 100644
--- a/TnT/Projects/Presentation/Sources/TrainerManagement/TrainerManagementView.swift
+++ b/TnT/Projects/Presentation/Sources/TrainerManagement/TrainerManagementView.swift
@@ -12,6 +12,7 @@ import ComposableArchitecture
import DesignSystem
import Domain
+@ViewAction(for: TrainerManagementFeature.self)
struct TrainerManagementView: View {
public var store: StoreOf
@@ -21,11 +22,11 @@ struct TrainerManagementView: View {
}
var body: some View {
- ScrollView {
+ ScrollView(showsIndicators: false) {
VStack(spacing: 12) {
-
Header()
- if let trainees = store.traineeList {
+
+ if let trainees = store.traineeList, !trainees.isEmpty {
TraineeListView(trainees: trainees)
} else {
EmptyListView()
@@ -33,7 +34,7 @@ struct TrainerManagementView: View {
}
}
.onAppear {
- store.send(.view(.onappear))
+ send(.onappear)
}
.navigationBarBackButtonHidden()
}
@@ -42,14 +43,25 @@ struct TrainerManagementView: View {
@ViewBuilder
func Header() -> some View {
- TNavigation(type: .LTextRButtonTitle(
- leftTitle: "내 회원",
- pointText: "\(store.traineeList?.count ?? 0)",
- rightButton: "회원 초대하기")
- )
- .rightTap {
- store.send(.view(.goTraineeInvitation))
+ HStack(spacing: 6) {
+ Text("내 회원")
+ .typographyStyle(.heading2, with: .neutral900)
+ Text("\(store.traineeList?.count ?? 0)")
+ .typographyStyle(.heading2, with: .red500)
+
+ Spacer()
+
+ Button {
+ send(.tapTraineeInvitation)
+ } label: {
+ Text("회원 초대하기")
+ .typographyStyle(.label2Medium, with: Color.neutral600)
+ .padding(.init(top: 7, leading: 12, bottom: 7, trailing: 12))
+ .background(Color.neutral200)
+ .clipShape(RoundedRectangle(cornerRadius: 8))
+ }
}
+ .padding(20)
}
/// 연결된 회원이 있는 경우
@@ -61,6 +73,7 @@ struct TrainerManagementView: View {
.padding(.bottom, 16)
}
}
+ .padding(.horizontal, 16)
}
/// 연결된 회원이 없는 경우
@@ -79,27 +92,36 @@ struct TrainerManagementView: View {
@ViewBuilder
func ListCellView(trainee: ActiveTraineeInfoResEntity) -> some View {
VStack(spacing: 12) {
- HStack {
- HStack {
+ HStack(spacing: 0) {
+ HStack(spacing: 12) {
ProfileImageView(imageURL: trainee.profileImageUrl)
- VStack(spacing: 12) {
+ VStack(spacing: 0) {
Text(trainee.name)
.typographyStyle(.body1Bold, with: Color.neutral900)
+ .frame(maxWidth: .infinity, alignment: .leading)
Text(trainee.ptGoals.joined(separator: ", "))
.typographyStyle(.label2Medium, with: Color.neutral500)
+ .frame(maxWidth: .infinity, alignment: .leading)
}
+ .padding(.vertical, 9)
}
Spacer()
- TChip(leadingEmoji: "💪", title: "\(trainee.finishedPtCount)", style: .blue)
+ VStack {
+ TChip(leadingEmoji: "💪", title: "\(trainee.finishedPtCount)/\(trainee.totalPtCount)회", style: .blue)
+ }
}
- VStack(spacing: 5) {
- Text("메모")
- .typographyStyle(.label2Bold, with: Color.neutral600)
- Text(trainee.memo)
- .typographyStyle(.label2Medium, with: Color.neutral500)
+ if !trainee.memo.isEmpty {
+ VStack(spacing: 5) {
+ Text("메모")
+ .typographyStyle(.label2Bold, with: Color.neutral600)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ Text(trainee.memo)
+ .typographyStyle(.label2Medium, with: Color.neutral500)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
}
}
.padding(12)
diff --git a/TnT/Projects/Presentation/Sources/TrainerManagement/TrainerManagmentFeature.swift b/TnT/Projects/Presentation/Sources/TrainerManagement/TrainerManagmentFeature.swift
index 4c4c3b4b..68961787 100644
--- a/TnT/Projects/Presentation/Sources/TrainerManagement/TrainerManagmentFeature.swift
+++ b/TnT/Projects/Presentation/Sources/TrainerManagement/TrainerManagmentFeature.swift
@@ -23,7 +23,7 @@ public struct TrainerManagementFeature {
/// 뷰에서 발생한 에러를 처리합니다.
case view(View)
/// 네비게이션 여부 설정
- case setNavigating
+ case setNavigating(RoutingScreen)
@CasePathable
public enum View: Sendable {
@@ -34,7 +34,7 @@ public struct TrainerManagementFeature {
/// 화면 진입시
case onappear
/// 회원 초대하기로 이동
- case goTraineeInvitation
+ case tapTraineeInvitation
}
}
@@ -48,21 +48,21 @@ public struct TrainerManagementFeature {
await send(.view(.getTraineeList))
}
- case .setNavigating:
- return .none
case .view(.getTraineeList):
return .run { send in
- let result: GetActiveTraineesListResDTO = try await trainerRepoUseCase.getMembersList()
+ let result: GetActiveTraineesListResDTO = try await trainerRepoUseCase.getActiveTraineesList()
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("트레이너 내 회원 > 회원 초대하기로 이동")
+ case .view(.tapTraineeInvitation):
+ return .send(.setNavigating(.addTrainee))
+
+ case .setNavigating:
return .none
}
}