Skip to content

Commit 97ce6bf

Browse files
authored
Merge pull request #75 from YAPP-Github/TNT-227-traineeDietPageAPI
[TNT-227] ํŠธ๋ ˆ์ด๋‹ˆ ์‹๋‹จ ๋“ฑ๋ก ํ™”๋ฉด API ์—ฐ๊ฒฐ
2 parents bfb1af4 + a3c67a6 commit 97ce6bf

File tree

12 files changed

+146
-30
lines changed

12 files changed

+146
-30
lines changed

โ€ŽTnT/Projects/Data/Sources/Network/Service/Trainee/TraineeRepositoryImpl.swiftโ€Ž

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,8 @@ public struct TraineeRepositoryImpl: TraineeRepository {
2323
decodingType: PostConnectTrainerResDTO.self
2424
)
2525
}
26+
27+
public func postTraineeDietRecord(_ reqDTO: PostTraineeDietRecordReqDTO, imgData: Data?) async throws -> PostTraineeDietRecordResDTO {
28+
return try await networkService.request(TraineeTargetType.postTraineeDietRecord(reqDto: reqDTO, imgData: imgData), decodingType: PostTraineeDietRecordResDTO.self)
29+
}
2630
}

โ€ŽTnT/Projects/Data/Sources/Network/Service/Trainee/TraineeTargetType.swiftโ€Ž

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import Domain
1414
public enum TraineeTargetType {
1515
/// ํŠธ๋ ˆ์ด๋„ˆ ์—ฐ๊ฒฐ ์š”์ฒญ
1616
case postConnectTrainer(reqDto: PostConnectTrainerReqDTO)
17+
/// ํŠธ๋ ˆ์ด๋‹ˆ ์‹๋‹จ ๊ธฐ๋ก ์ž‘์„ฑ
18+
case postTraineeDietRecord(reqDto: PostTraineeDietRecordReqDTO, imgData: Data?)
1719
}
1820

1921
extension TraineeTargetType: TargetType {
@@ -26,12 +28,14 @@ extension TraineeTargetType: TargetType {
2628
switch self {
2729
case .postConnectTrainer:
2830
return "/connect-trainer"
31+
case .postTraineeDietRecord:
32+
return "/diets"
2933
}
3034
}
3135

3236
var method: HTTPMethod {
3337
switch self {
34-
case .postConnectTrainer:
38+
case .postConnectTrainer, .postTraineeDietRecord:
3539
return .post
3640
}
3741
}
@@ -40,13 +44,25 @@ extension TraineeTargetType: TargetType {
4044
switch self {
4145
case .postConnectTrainer(let reqDto):
4246
return .requestJSONEncodable(encodable: reqDto)
47+
case let .postTraineeDietRecord(reqDto, imgData):
48+
let files: [MultipartFile] = imgData.map {
49+
[.init(fieldName: "dietImage", fileName: "dietImage.png", mimeType: "image/png", data: $0)]
50+
} ?? []
51+
52+
return .uploadMultipart(
53+
jsons: [.init(jsonName: "request", json: reqDto)],
54+
files: files,
55+
additionalFields: [:]
56+
)
4357
}
4458
}
4559

4660
var headers: [String: String]? {
4761
switch self {
4862
case .postConnectTrainer:
4963
return ["Content-Type": "application/json"]
64+
case .postTraineeDietRecord:
65+
return ["Content-Type": "multipart/form-data"]
5066
}
5167
}
5268

@@ -56,7 +72,7 @@ extension TraineeTargetType: TargetType {
5672
AuthTokenInterceptor(),
5773
ProgressIndicatorInterceptor(),
5874
ResponseValidatorInterceptor(),
59-
RetryInterceptor(maxRetryCount: 0)
75+
RetryInterceptor(maxRetryCount: 2)
6076
]
6177
}
6278
}

โ€ŽTnT/Projects/Domain/Sources/DTO/Trainee/TraineeRequestDTO.swiftโ€Ž

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,23 @@ public struct PostConnectTrainerReqDTO: Encodable {
3131
self.finishedPtCount = finishedPtCount
3232
}
3333
}
34+
35+
/// ํŠธ๋ ˆ์ด๋‹ˆ ์‹๋‹จ ๊ธฐ๋ก ์š”์ฒญ DTO
36+
public struct PostTraineeDietRecordReqDTO: Encodable {
37+
/// ์‹๋‹จ dateTime
38+
public let date: String
39+
/// ์‹๋‹จ ํƒ€์ž…
40+
public let dietType: String
41+
/// ์‹๋‹จ ๋ฉ”๋ชจ
42+
public let memo: String
43+
44+
public init(
45+
date: String,
46+
dietType: String,
47+
memo: String
48+
) {
49+
self.date = date
50+
self.dietType = dietType
51+
self.memo = memo
52+
}
53+
}

โ€ŽTnT/Projects/Domain/Sources/DTO/Trainee/TraineeResponseDTO.swiftโ€Ž

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,6 @@ public struct PostConnectTrainerResDTO: Decodable {
1919
/// ํŠธ๋ ˆ์ด๋‹ˆ ํ”„๋กœํ•„ ์ด๋ฏธ์ง€ URL
2020
public let traineeProfileImageUrl: String
2121
}
22+
23+
/// ํŠธ๋ ˆ์ด๋‹ˆ ์‹๋‹จ ๊ธฐ๋ก ์‘๋‹ต DTO
24+
public typealias PostTraineeDietRecordResDTO = EmptyResponse

โ€ŽTnT/Projects/Domain/Sources/Entity/DietType.swiftโ€Ž

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,16 @@
99
import Foundation
1010

1111
/// ์•ฑ์—์„œ ์กด์žฌํ•˜๋Š” ์‹๋‹จ ์œ ํ˜•์„ ์ •์˜ํ•œ ์—ด๊ฑฐํ˜•
12-
public enum DietType: Sendable, CaseIterable {
13-
case morning
12+
public enum DietType: String, Sendable, CaseIterable {
13+
case breakfast
1414
case lunch
1515
case dinner
1616
case snack
1717

1818
/// ์‹์‚ฌ ์œ ํ˜•์„ ํ•œ๊ธ€๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ๋ฐ˜ํ™˜
1919
public var koreanName: String {
2020
switch self {
21-
case .morning: return "์•„์นจ"
21+
case .breakfast: return "์•„์นจ"
2222
case .lunch: return "์ ์‹ฌ"
2323
case .dinner: return "์ €๋…"
2424
case .snack: return "๊ฐ„์‹"

โ€ŽTnT/Projects/Domain/Sources/Policy/TDateFormat.swiftโ€Ž

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,6 @@ public enum TDateFormat: String {
3434
case a_HHmm = "a HH:mm"
3535
/// "17:00"
3636
case HHmm = "HH:mm"
37+
/// "2024-02-12T15:30:00Z"
38+
case ISO8601 = "yyyy-MM-dd'T'HH:mm:ss'Z'"
3739
}

โ€ŽTnT/Projects/Domain/Sources/Repository/TraineeRepository.swiftโ€Ž

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,12 @@ public protocol TraineeRepository {
1616
/// - Returns: ์—ฐ๊ฒฐ ์„ฑ๊ณต ์‹œ, ์—ฐ๊ฒฐ๋œ ํŠธ๋ ˆ์ด๋„ˆ ์ •๋ณด๊ฐ€ ํฌํ•จ๋œ ์‘๋‹ต DTO (`PostConnectTrainerResDTO`)
1717
/// - Throws: ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜ ๋˜๋Š” ์ž˜๋ชป๋œ ์š”์ฒญ ๋ฐ์ดํ„ฐ๋กœ ์ธํ•œ ์„œ๋ฒ„ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ๊ฐ€๋Šฅ
1818
func postConnectTrainer(_ reqDTO: PostConnectTrainerReqDTO) async throws -> PostConnectTrainerResDTO
19+
20+
/// ํšŒ์›๊ฐ€์ž… ์š”์ฒญ
21+
/// - Parameters:
22+
/// - reqDTO: ์‹๋‹จ ๋“ฑ๋ก ์š”์ฒญ์— ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ
23+
/// - imgData: ์‚ฌ์šฉ์ž๊ฐ€ ์—…๋กœ๋“œํ•œ ์‹๋‹จ ์ด๋ฏธ์ง€ (์˜ต์…˜)
24+
/// - Returns: ๋“ฑ๋ก ์„ฑ๊ณต ์‹œ, ์‘๋‹ต DTO (empty) (`PostTraineeDietRecordResDTO`)
25+
/// - Throws: ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜ ๋˜๋Š” ์„œ๋ฒ„์—์„œ ๋ฐ˜ํ™˜ํ•œ ์˜ค๋ฅ˜๋ฅผ ๋ฐœ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Œ
26+
func postTraineeDietRecord(_ reqDTO: PostTraineeDietRecordReqDTO, imgData: Data?) async throws -> PostTraineeDietRecordResDTO
1927
}

โ€ŽTnT/Projects/Domain/Sources/UseCase/TraineeUseCase.swiftโ€Ž

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
// Copyright ยฉ 2025 yapp25thTeamTnT. All rights reserved.
77
//
88

9+
import Foundation
10+
911
// MARK: - TraineeUseCase ํ”„๋กœํ† ์ฝœ
1012
public protocol TraineeUseCase {
1113
/// ์ž…๋ ฅ ์ดˆ๋Œ€ ์ฝ”๋“œ ๊ฒ€์ฆ
@@ -66,4 +68,8 @@ extension DefaultTraineeUseCase: TraineeRepository {
6668
public func postConnectTrainer(_ reqDTO: PostConnectTrainerReqDTO) async throws -> PostConnectTrainerResDTO {
6769
return try await traineeRepository.postConnectTrainer(reqDTO)
6870
}
71+
72+
public func postTraineeDietRecord(_ reqDTO: PostTraineeDietRecordReqDTO, imgData: Data?) async throws -> PostTraineeDietRecordResDTO {
73+
return try await traineeRepository.postTraineeDietRecord(reqDTO, imgData: imgData)
74+
}
6975
}

โ€ŽTnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordFeature.swiftโ€Ž

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,14 @@ public struct TraineeAddDietRecordFeature {
8383
}
8484
}
8585

86+
@Dependency(\.traineeRepoUseCase) private var traineeRepoUseCase
8687
@Dependency(\.dismiss) private var dismiss
8788

8889
public enum Action: Sendable, ViewAction {
8990
/// ๋ทฐ์—์„œ ๋ฐœ์ƒํ•œ ์•ก์…˜์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
9091
case view(View)
92+
/// api ์ฝœ ์•ก์…˜์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค
93+
case api(APIAction)
9194
/// ์„ ํƒ๋œ ์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ ์ €์žฅ
9295
case imagePicked(Data?)
9396
/// ๋„ค๋น„๊ฒŒ์ด์…˜ ์—ฌ๋ถ€ ์„ค์ •
@@ -118,6 +121,12 @@ public struct TraineeAddDietRecordFeature {
118121
/// ํฌ์ปค์Šค ์ƒํƒœ ๋ณ€๊ฒฝ
119122
case setFocus(FocusField?, FocusField?)
120123
}
124+
125+
@CasePathable
126+
public enum APIAction: Sendable {
127+
/// ์‹๋‹จ ๋“ฑ๋ก API
128+
case registerDietRecord
129+
}
121130
}
122131

123132
public init() {}
@@ -129,7 +138,9 @@ public struct TraineeAddDietRecordFeature {
129138
switch action {
130139
case .view(let action):
131140
switch action {
132-
case .binding(\.dietDate), .binding(\.dietTime), .binding(\.dietType):
141+
case .binding(\.dietDate),
142+
.binding(\.dietTime),
143+
.binding(\.dietType):
133144
return self.validateAllFields(&state)
134145

135146
case .binding(\.dietInfo):
@@ -139,14 +150,15 @@ public struct TraineeAddDietRecordFeature {
139150
case .binding(\.view_photoPickerItem):
140151
let item: PhotosPickerItem? = state.view_photoPickerItem
141152
return .run { [item] send in
142-
if let item, let data = try? await item.loadTransferable(type: Data.self) {
153+
if let item,
154+
let data = try? await item.loadTransferable(type: Data.self) {
143155
await send(.imagePicked(data))
144156
}
145157
}
146-
158+
147159
case .binding:
148160
return .none
149-
161+
150162
case .tapNavBackButton:
151163
if state.view_isSubmitButtonEnabled {
152164
return self.setPopUpStatus(&state, status: .cancelDietAdd)
@@ -164,11 +176,11 @@ public struct TraineeAddDietRecordFeature {
164176
case .tapDietDateDropDown:
165177
state.view_bottomSheetItem = .datePicker(.dietDate)
166178
return .send(.view(.setFocus(state.view_focusField, .dietDate)))
167-
179+
168180
case .tapDietTimeDropDown:
169181
state.view_bottomSheetItem = .timePicker(.dietTime)
170182
return .send(.view(.setFocus(state.view_focusField, .dietTime)))
171-
183+
172184
case let .tapBottomSheetSubmitButton(field, date):
173185
state.view_bottomSheetItem = nil
174186

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

195207
case .tapSubmitButton:
196-
return .send(.setNavigating)
208+
return .send(.api(.registerDietRecord))
197209

198210
case .tapPopUpSecondaryButton(let popUp):
199211
guard popUp != nil else { return .none }
@@ -202,13 +214,31 @@ public struct TraineeAddDietRecordFeature {
202214
case .tapPopUpPrimaryButton(let popUp):
203215
guard popUp != nil else { return .none }
204216
return setPopUpStatus(&state, status: nil)
205-
217+
206218
case let .setFocus(oldFocus, newFocus):
207219
guard oldFocus != newFocus else { return .none }
208220
state.view_focusField = newFocus
209221
return .none
210222
}
211223

224+
case .api(let action):
225+
switch action {
226+
case .registerDietRecord:
227+
guard let date = combinedDietDateTime(date: state.dietDate, time: state.dietTime)?.toString(format: .ISO8601),
228+
let dietType = state.dietType?.rawValue else { return .none }
229+
return .run { [state] send in
230+
let result = try await traineeRepoUseCase.postTraineeDietRecord(
231+
.init(
232+
date: date,
233+
dietType: dietType,
234+
memo: state.dietInfo
235+
),
236+
imgData: state.dietImageData
237+
)
238+
await send(.setNavigating)
239+
}
240+
}
241+
212242
case .imagePicked(let imgData):
213243
state.dietImageData = imgData
214244
return self.validateAllFields(&state)
@@ -235,7 +265,8 @@ private extension TraineeAddDietRecordFeature {
235265
guard state.dietDate != nil else { return .none }
236266
guard state.dietTime != nil else { return .none }
237267
guard state.dietType != nil else { return .none }
238-
268+
guard state.dietInfo != nil else { return .none }
269+
239270
state.view_isSubmitButtonEnabled = true
240271
return .none
241272
}
@@ -247,6 +278,24 @@ private extension TraineeAddDietRecordFeature {
247278
state.view_isPopUpPresented = status != nil
248279
return .none
249280
}
281+
282+
/// dietDate์™€ dietTime์„ ๊ฒฐํ•ฉํ•˜์—ฌ ์ตœ์ข… `Date`๋ฅผ ์ƒ์„ฑ
283+
func combinedDietDateTime(date: Date?, time: Date?) -> Date? {
284+
guard let date = date, let time = time else { return nil }
285+
286+
let calendar = Calendar.current
287+
let dateComponents = calendar.dateComponents([.year, .month, .day], from: date)
288+
let timeComponents = calendar.dateComponents([.hour, .minute, .second], from: time)
289+
290+
return calendar.date(from: DateComponents(
291+
year: dateComponents.year,
292+
month: dateComponents.month,
293+
day: dateComponents.day,
294+
hour: timeComponents.hour,
295+
minute: timeComponents.minute,
296+
second: timeComponents.second
297+
))
298+
}
250299
}
251300

252301
// MARK: BottomSheet
@@ -317,7 +366,6 @@ public extension TraineeAddDietRecordFeature {
317366
return nil
318367
case .cancelDietAdd:
319368
return .tapPopUpSecondaryButton(popUp: self)
320-
return nil
321369
}
322370
}
323371

โ€ŽTnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordView.swiftโ€Ž

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -254,20 +254,24 @@ public struct TraineeAddDietRecordView: View {
254254

255255
@ViewBuilder
256256
private func DietInfoSection() -> some View {
257-
TTextEditor(
258-
placeholder: "์‹๋‹จ์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”!",
259-
text: $store.dietInfo,
260-
textEditorStatus: $store.view_dietInfoStatus,
261-
footer: {
262-
.init(
263-
textLimit: 100,
264-
status: $store.view_dietInfoStatus,
265-
textCount: store.dietInfo.count,
266-
warningText: "100์ž ๋ฏธ๋งŒ์œผ๋กœ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”"
267-
)
268-
}
269-
)
270-
.focused($focusedField, equals: .dietInfo)
257+
VStack(alignment: .leading, spacing: 8) {
258+
TTextField.Header(isRequired: true, title: "๋ฉ”๋ชจํ•˜๊ธฐ", limitCount: nil, textCount: nil)
259+
260+
TTextEditor(
261+
placeholder: "์‹๋‹จ์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”!",
262+
text: $store.dietInfo,
263+
textEditorStatus: $store.view_dietInfoStatus,
264+
footer: {
265+
.init(
266+
textLimit: 100,
267+
status: $store.view_dietInfoStatus,
268+
textCount: store.dietInfo.count,
269+
warningText: "100์ž ๋ฏธ๋งŒ์œผ๋กœ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”"
270+
)
271+
}
272+
)
273+
.focused($focusedField, equals: .dietInfo)
274+
}
271275
}
272276

273277
@ViewBuilder

0 commit comments

Comments
ย (0)