-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #73 from YAPP-Github/TNT-227-traineeDietPageScreen
[TNT-227] ํธ๋ ์ด๋ ์๋จ ๊ธฐ๋ก ํ๋ฉด ์์ฑ
- Loading branch information
Showing
12 changed files
with
672 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
337 changes: 337 additions & 0 deletions
337
TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordFeature.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,337 @@ | ||
// | ||
// TraineeAddDietRecordFeature.swift | ||
// Presentation | ||
// | ||
// Created by ๋ฐ๋ฏผ์ on 2/10/25. | ||
// Copyright ยฉ 2025 yapp25thTeamTnT. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
import _PhotosUI_SwiftUI | ||
import ComposableArchitecture | ||
|
||
import Domain | ||
import DesignSystem | ||
|
||
@Reducer | ||
public struct TraineeAddDietRecordFeature { | ||
|
||
public typealias FocusField = TraineeAddDietRecordView.Field | ||
|
||
@ObservableState | ||
public struct State: Equatable { | ||
// MARK: Data related state | ||
/// ์๋จ ๋ ์ง | ||
var dietDate: Date? | ||
/// ์๋จ ์๊ฐ | ||
var dietTime: Date? | ||
/// ์๋จ ํ์ | ||
var dietType: DietType? | ||
/// ์๋จ ์ฌ์ง | ||
var dietImageData: Data? | ||
/// ์๋จ ์ ๋ณด | ||
var dietInfo: String | ||
|
||
// MARK: UI related state | ||
/// ํ ์คํธ ํ๋ ์ํ (๋น ๊ฐ / ์ ๋ ฅ๋จ / ์ ํจํ์ง ์์) | ||
var view_dietDateStatus: TTextField.Status | ||
var view_dietTimeStatus: TTextField.Status | ||
var view_dietInfoStatus: TTextEditor.Status | ||
/// ํ์ฌ ํฌ์ปค์ค๋ ํ๋ | ||
var view_focusField: FocusField? | ||
/// BottomSheet์ ํ์ํ ์์ดํ | ||
var view_bottomSheetItem: BottomSheetItem? | ||
/// "์๋ฃ" ๋ฒํผ ํ์ฑํ ์ฌ๋ถ | ||
var view_isSubmitButtonEnabled: Bool | ||
/// ํ์ฌ ์ ํ๋ ์ด๋ฏธ์ง (PhotosPickerItem) | ||
var view_photoPickerItem: PhotosPickerItem? | ||
/// ํ์๋๋ ํ์ | ||
var view_popUp: PopUp? | ||
/// ํ์ ํ์ ์ฌ๋ถ | ||
var view_isPopUpPresented: Bool | ||
|
||
public init( | ||
dietDate: Date? = nil, | ||
dietTime: Date? = nil, | ||
dietType: DietType? = nil, | ||
dietImageData: Data? = nil, | ||
dietInfo: String = "", | ||
view_dietDateStatus: TTextField.Status = .empty, | ||
view_dietTimeStatus: TTextField.Status = .empty, | ||
view_dietInfoStatus: TTextEditor.Status = .empty, | ||
view_focusField: FocusField? = nil, | ||
view_bottomSheetItem: BottomSheetItem? = nil, | ||
view_isSubmitButtonEnabled: Bool = false, | ||
view_photoPickerItem: PhotosPickerItem? = nil, | ||
view_popUp: PopUp? = nil, | ||
view_isPopUpPresented: Bool = false | ||
) { | ||
self.dietDate = dietDate | ||
self.dietTime = dietTime | ||
self.dietType = dietType | ||
self.dietImageData = dietImageData | ||
self.dietInfo = dietInfo | ||
self.view_dietDateStatus = view_dietDateStatus | ||
self.view_dietTimeStatus = view_dietTimeStatus | ||
self.view_dietInfoStatus = view_dietInfoStatus | ||
self.view_focusField = view_focusField | ||
self.view_bottomSheetItem = view_bottomSheetItem | ||
self.view_isSubmitButtonEnabled = view_isSubmitButtonEnabled | ||
self.view_photoPickerItem = view_photoPickerItem | ||
self.view_popUp = view_popUp | ||
self.view_isPopUpPresented = view_isPopUpPresented | ||
} | ||
} | ||
|
||
@Dependency(\.dismiss) private var dismiss | ||
|
||
public enum Action: Sendable, ViewAction { | ||
/// ๋ทฐ์์ ๋ฐ์ํ ์ก์ ์ ์ฒ๋ฆฌํฉ๋๋ค. | ||
case view(View) | ||
/// ์ ํ๋ ์ด๋ฏธ์ง ๋ฐ์ดํฐ ์ ์ฅ | ||
case imagePicked(Data?) | ||
/// ๋ค๋น๊ฒ์ด์ ์ฌ๋ถ ์ค์ | ||
case setNavigating | ||
|
||
@CasePathable | ||
public enum View: Sendable, BindableAction { | ||
/// ๋ฐ์ธ๋ฉํ ์ก์ ์ ์ฒ๋ฆฌ | ||
case binding(BindingAction<State>) | ||
/// ๋ค๋น๋ฐ ๋ฐฑ๋ฒํผ ํญ๋์์ ๋ | ||
case tapNavBackButton | ||
/// ์ด๋ฏธ์ง ํผ์ปค ์ญ์ ๋ฒํผ ํญ๋์์ ๋ | ||
case tapPhotoPickerDeleteButton | ||
/// ์๋จ ๋ ์ง ๋๋กญ๋ค์ด์ด ํญ๋์์ ๋ (DatePicker ํ์) | ||
case tapDietDateDropDown | ||
/// ์๋จ ์๊ฐ ๋๋กญ๋ค์ด์ด ํญ๋์์ ๋ (TimePicker ํ์) | ||
case tapDietTimeDropDown | ||
/// DatePicker / TimePicker ๋ฐํ ์ํธ์์ ๋ ์ง๋ฅผ ์ ํํ์ ๋ | ||
case tapBottomSheetSubmitButton(FocusField, Date) | ||
/// ์๋จ ํ์ ๋ฒํผ์ด ํญ๋์์ ๋ | ||
case tapDietTypeButton(DietType) | ||
/// "์๋ฃ" ๋ฒํผ์ด ๋๋ ธ์ ๋ | ||
case tapSubmitButton | ||
/// ํ์ ์ข์ธก secondary ๋ฒํผ ํญ | ||
case tapPopUpSecondaryButton(popUp: PopUp?) | ||
/// ํ์ ์ฐ์ธก primary ๋ฒํผ ํญ | ||
case tapPopUpPrimaryButton(popUp: PopUp?) | ||
/// ํฌ์ปค์ค ์ํ ๋ณ๊ฒฝ | ||
case setFocus(FocusField?, FocusField?) | ||
} | ||
} | ||
|
||
public init() {} | ||
|
||
public var body: some ReducerOf<Self> { | ||
BindingReducer(action: \.view) | ||
|
||
Reduce { state, action in | ||
switch action { | ||
case .view(let action): | ||
switch action { | ||
case .binding(\.dietDate), .binding(\.dietTime), .binding(\.dietType): | ||
return self.validateAllFields(&state) | ||
|
||
case .binding(\.dietInfo): | ||
state.view_dietInfoStatus = validateDietInfo(state.dietInfo) | ||
return self.validateAllFields(&state) | ||
|
||
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) { | ||
await send(.imagePicked(data)) | ||
} | ||
} | ||
|
||
case .binding: | ||
return .none | ||
|
||
case .tapNavBackButton: | ||
if state.view_isSubmitButtonEnabled { | ||
return self.setPopUpStatus(&state, status: .cancelDietAdd) | ||
} else { | ||
return .run { send in | ||
await self.dismiss() | ||
} | ||
} | ||
|
||
case .tapPhotoPickerDeleteButton: | ||
state.dietImageData = nil | ||
state.view_photoPickerItem = nil | ||
return .none | ||
|
||
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 | ||
|
||
switch field { | ||
case .dietDate: | ||
state.dietDate = date | ||
state.view_dietDateStatus = .filled | ||
case .dietTime: | ||
state.dietTime = date | ||
state.view_dietTimeStatus = .filled | ||
default: | ||
return .none | ||
} | ||
|
||
return .concatenate( | ||
.send(.view(.setFocus(field, nil))), | ||
self.validateAllFields(&state) | ||
) | ||
|
||
case .tapDietTypeButton(let type): | ||
state.dietType = type | ||
return self.validateAllFields(&state) | ||
|
||
case .tapSubmitButton: | ||
return .send(.setNavigating) | ||
|
||
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 } | ||
return setPopUpStatus(&state, status: nil) | ||
|
||
case let .setFocus(oldFocus, newFocus): | ||
guard oldFocus != newFocus else { return .none } | ||
state.view_focusField = newFocus | ||
return .none | ||
} | ||
|
||
case .imagePicked(let imgData): | ||
state.dietImageData = imgData | ||
return self.validateAllFields(&state) | ||
|
||
case .setNavigating: | ||
return .none | ||
} | ||
} | ||
} | ||
} | ||
|
||
// MARK: Internal Logic | ||
private extension TraineeAddDietRecordFeature { | ||
/// ์๋จ ์ ๋ณด ์ํ ๊ฒ์ฆ | ||
func validateDietInfo(_ info: String) -> TTextEditor.Status { | ||
guard !info.isEmpty else { return .empty } | ||
return info.count > 100 ? .invalid : .filled | ||
} | ||
|
||
/// ๋ชจ๋ ํ๋์ ์ํ๋ฅผ ๊ฒ์ฆํ์ฌ "๋ค์" ๋ฒํผ ํ์ฑํ ์ฌ๋ถ๋ฅผ ๊ฒฐ์ | ||
func validateAllFields(_ state: inout State) -> Effect<Action> { | ||
|
||
guard state.dietImageData != nil else { return .none } | ||
guard state.dietDate != nil else { return .none } | ||
guard state.dietTime != nil else { return .none } | ||
guard state.dietType != nil else { return .none } | ||
|
||
state.view_isSubmitButtonEnabled = true | ||
return .none | ||
} | ||
|
||
/// ํ์ ์ํ, ํ์ ์ํ๋ฅผ ์ ๋ฐ์ดํธ | ||
/// status nil ์ ๋ ฅ์ธ ๊ฒฝ์ฐ ํ์ ํ์ ํด์ | ||
func setPopUpStatus(_ state: inout State, status: PopUp?) -> Effect<Action> { | ||
state.view_popUp = status | ||
state.view_isPopUpPresented = status != nil | ||
return .none | ||
} | ||
} | ||
|
||
// MARK: BottomSheet | ||
public extension TraineeAddDietRecordFeature { | ||
enum BottomSheetItem: Equatable, Identifiable { | ||
case datePicker(FocusField) | ||
case timePicker(FocusField) | ||
|
||
public var id: String { | ||
switch self { | ||
case .datePicker(let field): | ||
return "datePicker" + field.title | ||
case .timePicker(let field): | ||
return "timePicker" + field.title | ||
} | ||
} | ||
|
||
public var field: FocusField? { | ||
switch self { | ||
case .datePicker(let field): | ||
return field | ||
case .timePicker(let field): | ||
return field | ||
} | ||
} | ||
} | ||
} | ||
|
||
// MARK: PopUp | ||
public extension TraineeAddDietRecordFeature { | ||
/// ๋ณธ ํ๋ฉด์ ํ์ ์ผ๋ก ํ์๋๋ ๋ชฉ๋ก | ||
enum PopUp: Equatable, Sendable { | ||
/// ์๋จ์ ๊ธฐ๋กํ์ด์! | ||
case dietAdded | ||
/// ์๋จ ๊ธฐ๋ก์ ์ข ๋ฃํ ๊น์? | ||
case cancelDietAdd | ||
|
||
var title: String { | ||
switch self { | ||
case .dietAdded: | ||
return "์๋จ์ ๊ธฐ๋กํ์ด์!" | ||
case .cancelDietAdd: | ||
return "์๋จ ๊ธฐ๋ก์ ์ข ๋ฃํ ๊น์?" | ||
} | ||
} | ||
|
||
var message: String { | ||
switch self { | ||
case .dietAdded: | ||
return "๋ด์ผ๋ ๊ธฐ๋กํด ์ฃผ์ค ๊ฑฐ์ฃ ?" | ||
case .cancelDietAdd: | ||
return "๊ธฐ๋ก์ด ์ ์ฅ๋์ง ์์์!" | ||
} | ||
} | ||
|
||
var showAlertIcon: Bool { | ||
switch self { | ||
case .dietAdded: | ||
return false | ||
case .cancelDietAdd: | ||
return true | ||
} | ||
} | ||
|
||
var secondaryAction: Action.View? { | ||
switch self { | ||
case .dietAdded: | ||
return nil | ||
case .cancelDietAdd: | ||
return .tapPopUpSecondaryButton(popUp: self) | ||
return nil | ||
} | ||
} | ||
|
||
var primaryTitle: String { | ||
switch self { | ||
case .dietAdded: | ||
return "ํ์ธ" | ||
case .cancelDietAdd: | ||
return "๊ณ์ ์์ " | ||
} | ||
} | ||
|
||
var primaryAction: Action.View { | ||
return .tapPopUpPrimaryButton(popUp: self) | ||
} | ||
} | ||
} |
Oops, something went wrong.