Skip to content

Commit

Permalink
Merge pull request #75 from YAPP-Github/TNT-227-traineeDietPageAPI
Browse files Browse the repository at this point in the history
[TNT-227] νŠΈλ ˆμ΄λ‹ˆ 식단 등둝 ν™”λ©΄ API μ—°κ²°
  • Loading branch information
FpRaArNkK authored Feb 12, 2025
2 parents bfb1af4 + a3c67a6 commit 97ce6bf
Show file tree
Hide file tree
Showing 12 changed files with 146 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,8 @@ public struct TraineeRepositoryImpl: TraineeRepository {
decodingType: PostConnectTrainerResDTO.self
)
}

public func postTraineeDietRecord(_ reqDTO: PostTraineeDietRecordReqDTO, imgData: Data?) async throws -> PostTraineeDietRecordResDTO {
return try await networkService.request(TraineeTargetType.postTraineeDietRecord(reqDto: reqDTO, imgData: imgData), decodingType: PostTraineeDietRecordResDTO.self)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import Domain
public enum TraineeTargetType {
/// νŠΈλ ˆμ΄λ„ˆ μ—°κ²° μš”μ²­
case postConnectTrainer(reqDto: PostConnectTrainerReqDTO)
/// νŠΈλ ˆμ΄λ‹ˆ 식단 기둝 μž‘μ„±
case postTraineeDietRecord(reqDto: PostTraineeDietRecordReqDTO, imgData: Data?)
}

extension TraineeTargetType: TargetType {
Expand All @@ -26,12 +28,14 @@ extension TraineeTargetType: TargetType {
switch self {
case .postConnectTrainer:
return "/connect-trainer"
case .postTraineeDietRecord:
return "/diets"
}
}

var method: HTTPMethod {
switch self {
case .postConnectTrainer:
case .postConnectTrainer, .postTraineeDietRecord:
return .post
}
}
Expand All @@ -40,13 +44,25 @@ extension TraineeTargetType: TargetType {
switch self {
case .postConnectTrainer(let reqDto):
return .requestJSONEncodable(encodable: reqDto)
case let .postTraineeDietRecord(reqDto, imgData):
let files: [MultipartFile] = imgData.map {
[.init(fieldName: "dietImage", fileName: "dietImage.png", mimeType: "image/png", data: $0)]
} ?? []

return .uploadMultipart(
jsons: [.init(jsonName: "request", json: reqDto)],
files: files,
additionalFields: [:]
)
}
}

var headers: [String: String]? {
switch self {
case .postConnectTrainer:
return ["Content-Type": "application/json"]
case .postTraineeDietRecord:
return ["Content-Type": "multipart/form-data"]
}
}

Expand All @@ -56,7 +72,7 @@ extension TraineeTargetType: TargetType {
AuthTokenInterceptor(),
ProgressIndicatorInterceptor(),
ResponseValidatorInterceptor(),
RetryInterceptor(maxRetryCount: 0)
RetryInterceptor(maxRetryCount: 2)
]
}
}
20 changes: 20 additions & 0 deletions TnT/Projects/Domain/Sources/DTO/Trainee/TraineeRequestDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,23 @@ public struct PostConnectTrainerReqDTO: Encodable {
self.finishedPtCount = finishedPtCount
}
}

/// νŠΈλ ˆμ΄λ‹ˆ 식단 기둝 μš”μ²­ DTO
public struct PostTraineeDietRecordReqDTO: Encodable {
/// 식단 dateTime
public let date: String
/// 식단 νƒ€μž…
public let dietType: String
/// 식단 λ©”λͺ¨
public let memo: String

public init(
date: String,
dietType: String,
memo: String
) {
self.date = date
self.dietType = dietType
self.memo = memo
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@ public struct PostConnectTrainerResDTO: Decodable {
/// νŠΈλ ˆμ΄λ‹ˆ ν”„λ‘œν•„ 이미지 URL
public let traineeProfileImageUrl: String
}

/// νŠΈλ ˆμ΄λ‹ˆ 식단 기둝 응닡 DTO
public typealias PostTraineeDietRecordResDTO = EmptyResponse
6 changes: 3 additions & 3 deletions TnT/Projects/Domain/Sources/Entity/DietType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@
import Foundation

/// μ•±μ—μ„œ μ‘΄μž¬ν•˜λŠ” 식단 μœ ν˜•μ„ μ •μ˜ν•œ μ—΄κ±°ν˜•
public enum DietType: Sendable, CaseIterable {
case morning
public enum DietType: String, Sendable, CaseIterable {
case breakfast
case lunch
case dinner
case snack

/// 식사 μœ ν˜•μ„ ν•œκΈ€λ‘œ λ³€ν™˜ν•˜μ—¬ λ°˜ν™˜
public var koreanName: String {
switch self {
case .morning: return "μ•„μΉ¨"
case .breakfast: return "μ•„μΉ¨"
case .lunch: return "점심"
case .dinner: return "저녁"
case .snack: return "간식"
Expand Down
2 changes: 2 additions & 0 deletions TnT/Projects/Domain/Sources/Policy/TDateFormat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,6 @@ public enum TDateFormat: String {
case a_HHmm = "a HH:mm"
/// "17:00"
case HHmm = "HH:mm"
/// "2024-02-12T15:30:00Z"
case ISO8601 = "yyyy-MM-dd'T'HH:mm:ss'Z'"
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,12 @@ public protocol TraineeRepository {
/// - Returns: μ—°κ²° 성곡 μ‹œ, μ—°κ²°λœ νŠΈλ ˆμ΄λ„ˆ 정보가 ν¬ν•¨λœ 응닡 DTO (`PostConnectTrainerResDTO`)
/// - Throws: λ„€νŠΈμ›Œν¬ 였λ₯˜ λ˜λŠ” 잘λͺ»λœ μš”μ²­ λ°μ΄ν„°λ‘œ μΈν•œ μ„œλ²„ 였λ₯˜ λ°œμƒ κ°€λŠ₯
func postConnectTrainer(_ reqDTO: PostConnectTrainerReqDTO) async throws -> PostConnectTrainerResDTO

/// νšŒμ›κ°€μž… μš”μ²­
/// - Parameters:
/// - reqDTO: 식단 등둝 μš”μ²­μ— ν•„μš”ν•œ 데이터
/// - imgData: μ‚¬μš©μžκ°€ μ—…λ‘œλ“œν•œ 식단 이미지 (μ˜΅μ…˜)
/// - Returns: 등둝 성곡 μ‹œ, 응닡 DTO (empty) (`PostTraineeDietRecordResDTO`)
/// - Throws: λ„€νŠΈμ›Œν¬ 였λ₯˜ λ˜λŠ” μ„œλ²„μ—μ„œ λ°˜ν™˜ν•œ 였λ₯˜λ₯Ό λ°œμƒμ‹œν‚¬ 수 있음
func postTraineeDietRecord(_ reqDTO: PostTraineeDietRecordReqDTO, imgData: Data?) async throws -> PostTraineeDietRecordResDTO
}
6 changes: 6 additions & 0 deletions TnT/Projects/Domain/Sources/UseCase/TraineeUseCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
// Copyright Β© 2025 yapp25thTeamTnT. All rights reserved.
//

import Foundation

// MARK: - TraineeUseCase ν”„λ‘œν† μ½œ
public protocol TraineeUseCase {
/// μž…λ ₯ μ΄ˆλŒ€ μ½”λ“œ 검증
Expand Down Expand Up @@ -66,4 +68,8 @@ extension DefaultTraineeUseCase: TraineeRepository {
public func postConnectTrainer(_ reqDTO: PostConnectTrainerReqDTO) async throws -> PostConnectTrainerResDTO {
return try await traineeRepository.postConnectTrainer(reqDTO)
}

public func postTraineeDietRecord(_ reqDTO: PostTraineeDietRecordReqDTO, imgData: Data?) async throws -> PostTraineeDietRecordResDTO {
return try await traineeRepository.postTraineeDietRecord(reqDTO, imgData: imgData)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,14 @@ public struct TraineeAddDietRecordFeature {
}
}

@Dependency(\.traineeRepoUseCase) private var traineeRepoUseCase
@Dependency(\.dismiss) private var dismiss

public enum Action: Sendable, ViewAction {
/// λ·°μ—μ„œ λ°œμƒν•œ μ•‘μ…˜μ„ μ²˜λ¦¬ν•©λ‹ˆλ‹€.
case view(View)
/// api 콜 μ•‘μ…˜μ„ μ²˜λ¦¬ν•©λ‹ˆλ‹€
case api(APIAction)
/// μ„ νƒλœ 이미지 데이터 μ €μž₯
case imagePicked(Data?)
/// λ„€λΉ„κ²Œμ΄μ…˜ μ—¬λΆ€ μ„€μ •
Expand Down Expand Up @@ -118,6 +121,12 @@ public struct TraineeAddDietRecordFeature {
/// 포컀슀 μƒνƒœ λ³€κ²½
case setFocus(FocusField?, FocusField?)
}

@CasePathable
public enum APIAction: Sendable {
/// 식단 등둝 API
case registerDietRecord
}
}

public init() {}
Expand All @@ -129,7 +138,9 @@ public struct TraineeAddDietRecordFeature {
switch action {
case .view(let action):
switch action {
case .binding(\.dietDate), .binding(\.dietTime), .binding(\.dietType):
case .binding(\.dietDate),
.binding(\.dietTime),
.binding(\.dietType):
return self.validateAllFields(&state)

case .binding(\.dietInfo):
Expand All @@ -139,14 +150,15 @@ public struct TraineeAddDietRecordFeature {
case .binding(\.view_photoPickerItem):
let item: PhotosPickerItem? = state.view_photoPickerItem
return .run { [item] send in
if let item, let data = try? await item.loadTransferable(type: Data.self) {
if let item,
let data = try? await item.loadTransferable(type: Data.self) {
await send(.imagePicked(data))
}
}

case .binding:
return .none

case .tapNavBackButton:
if state.view_isSubmitButtonEnabled {
return self.setPopUpStatus(&state, status: .cancelDietAdd)
Expand All @@ -164,11 +176,11 @@ public struct TraineeAddDietRecordFeature {
case .tapDietDateDropDown:
state.view_bottomSheetItem = .datePicker(.dietDate)
return .send(.view(.setFocus(state.view_focusField, .dietDate)))

case .tapDietTimeDropDown:
state.view_bottomSheetItem = .timePicker(.dietTime)
return .send(.view(.setFocus(state.view_focusField, .dietTime)))

case let .tapBottomSheetSubmitButton(field, date):
state.view_bottomSheetItem = nil

Expand All @@ -193,7 +205,7 @@ public struct TraineeAddDietRecordFeature {
return self.validateAllFields(&state)

case .tapSubmitButton:
return .send(.setNavigating)
return .send(.api(.registerDietRecord))

case .tapPopUpSecondaryButton(let popUp):
guard popUp != nil else { return .none }
Expand All @@ -202,13 +214,31 @@ public struct TraineeAddDietRecordFeature {
case .tapPopUpPrimaryButton(let popUp):
guard popUp != nil else { return .none }
return setPopUpStatus(&state, status: nil)

case let .setFocus(oldFocus, newFocus):
guard oldFocus != newFocus else { return .none }
state.view_focusField = newFocus
return .none
}

case .api(let action):
switch action {
case .registerDietRecord:
guard let date = combinedDietDateTime(date: state.dietDate, time: state.dietTime)?.toString(format: .ISO8601),
let dietType = state.dietType?.rawValue else { return .none }
return .run { [state] send in
let result = try await traineeRepoUseCase.postTraineeDietRecord(
.init(
date: date,
dietType: dietType,
memo: state.dietInfo
),
imgData: state.dietImageData
)
await send(.setNavigating)
}
}

case .imagePicked(let imgData):
state.dietImageData = imgData
return self.validateAllFields(&state)
Expand All @@ -235,7 +265,8 @@ 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 }

state.view_isSubmitButtonEnabled = true
return .none
}
Expand All @@ -247,6 +278,24 @@ private extension TraineeAddDietRecordFeature {
state.view_isPopUpPresented = status != nil
return .none
}

/// dietDate와 dietTime을 κ²°ν•©ν•˜μ—¬ μ΅œμ’… `Date`λ₯Ό 생성
func combinedDietDateTime(date: Date?, time: Date?) -> Date? {
guard let date = date, let time = time else { return nil }

let calendar = Calendar.current
let dateComponents = calendar.dateComponents([.year, .month, .day], from: date)
let timeComponents = calendar.dateComponents([.hour, .minute, .second], from: time)

return calendar.date(from: DateComponents(
year: dateComponents.year,
month: dateComponents.month,
day: dateComponents.day,
hour: timeComponents.hour,
minute: timeComponents.minute,
second: timeComponents.second
))
}
}

// MARK: BottomSheet
Expand Down Expand Up @@ -317,7 +366,6 @@ public extension TraineeAddDietRecordFeature {
return nil
case .cancelDietAdd:
return .tapPopUpSecondaryButton(popUp: self)
return nil
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,20 +254,24 @@ public struct TraineeAddDietRecordView: View {

@ViewBuilder
private func DietInfoSection() -> some View {
TTextEditor(
placeholder: "식단에 λŒ€ν•œ 정보λ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”!",
text: $store.dietInfo,
textEditorStatus: $store.view_dietInfoStatus,
footer: {
.init(
textLimit: 100,
status: $store.view_dietInfoStatus,
textCount: store.dietInfo.count,
warningText: "100자 미만으둜 μž…λ ₯ν•΄μ£Όμ„Έμš”"
)
}
)
.focused($focusedField, equals: .dietInfo)
VStack(alignment: .leading, spacing: 8) {
TTextField.Header(isRequired: true, title: "λ©”λͺ¨ν•˜κΈ°", limitCount: nil, textCount: nil)

TTextEditor(
placeholder: "식단에 λŒ€ν•œ 정보λ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”!",
text: $store.dietInfo,
textEditorStatus: $store.view_dietInfoStatus,
footer: {
.init(
textLimit: 100,
status: $store.view_dietInfoStatus,
textCount: store.dietInfo.count,
warningText: "100자 미만으둜 μž…λ ₯ν•΄μ£Όμ„Έμš”"
)
}
)
.focused($focusedField, equals: .dietInfo)
}
}

@ViewBuilder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ public struct TraineeMainFlowFeature {
// νŠΉμ • ν™”λ©΄ append
return .none

/// 식단 기둝 ν™”λ©΄ 등둝 -> ν™ˆν™”λ©΄μœΌλ‘œ 이동
case .element(id: _, action: .addDietRecordPage(.setNavigating)):
state.path.removeLast()
return .none

/// λ§ˆμ΄νŽ˜μ΄μ§€ μ΄ˆλŒ€μ½”λ“œ μž…λ ₯ν™”λ©΄ λ‹€μŒ λ²„νŠΌ νƒ­ - > PT 정보 μž…λ ₯ ν™”λ©΄ or ν™ˆ 이동
case .element(_, action: .traineeInvitationCodeInput(.setNavigating(let screen))):
switch screen {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ extension DietType {
/// ν•΄λ‹Ή 식단을 ν‘œμ‹œν•˜λŠ” 이λͺ¨μ§€ μž…λ‹ˆλ‹€
var emoji: String {
switch self {
case .morning:
case .breakfast:
"🌞"
case .lunch:
"β›…"
Expand Down

0 comments on commit 97ce6bf

Please sign in to comment.