diff --git a/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordFeature.swift b/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordFeature.swift index 24885147..91e80b45 100644 --- a/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordFeature.swift +++ b/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordFeature.swift @@ -138,7 +138,8 @@ public struct TraineeAddDietRecordFeature { switch action { case .view(let action): switch action { - case .binding(\.dietDate), + case .binding(\.dietImageData), + .binding(\.dietDate), .binding(\.dietTime), .binding(\.dietType): return self.validateAllFields(&state) @@ -209,7 +210,10 @@ public struct TraineeAddDietRecordFeature { case .tapPopUpSecondaryButton(let popUp): guard popUp != nil else { return .none } - return setPopUpStatus(&state, status: nil) + return .concatenate( + setPopUpStatus(&state, status: nil), + .run{ _ in await self.dismiss() } + ) case .tapPopUpPrimaryButton(let popUp): guard popUp != nil else { return .none } diff --git a/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordView.swift b/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordView.swift index 7973fbc7..f09cf0c2 100644 --- a/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordView.swift +++ b/TnT/Projects/Presentation/Sources/AddDietRecord/TraineeAddDietRecordView.swift @@ -67,13 +67,6 @@ public struct TraineeAddDietRecordView: View { send(.tapSubmitButton) } .padding(.horizontal, 16) - - // TBottomButton( - // title: "완료", - // isEnable: store.view_isSubmitButtonEnabled - // ) { - // send(.tapSubmitButton) - // } } } .sheet(item: $store.view_bottomSheetItem) { item in @@ -115,17 +108,6 @@ public struct TraineeAddDietRecordView: View { } // MARK: - Sections - // @ViewBuilder - // private func Header() -> some View { - // VStack(alignment: .leading, spacing: 8) { - // Text("오늘의 식단을 기록해 주세요") - // .typographyStyle(.heading2, with: .neutral950) - // Text("식단을 기록하면 트레이너가 피드백을 남길 수 있어요") - // .typographyStyle(.body2Medium, with: .neutral500) - // } - // .padding(20) - // } - @ViewBuilder private func DietPhotoSection() -> some View { PhotosPicker( @@ -133,46 +115,48 @@ public struct TraineeAddDietRecordView: View { matching: .images, photoLibrary: .shared() ) { - if let imageData = store.dietImageData, - let uiImage = UIImage(data: imageData) { - Image(uiImage: uiImage) - .resizable() - .aspectRatio(1, contentMode: .fill) - .frame(maxWidth: .infinity, maxHeight: .infinity) - .clipShape(.rect(cornerRadius: 20)) - .overlay(alignment: .topTrailing) { - Button(action: { send(.tapPhotoPickerDeleteButton)}) { - ZStack { - Circle() - .fill(Color.common100.opacity(0.5)) - .frame(width: 24, height: 24) - Image(.icnDelete) - .renderingMode(.template) - .resizable() - .tint(.common0) - .frame(width: 12, height: 12) + GeometryReader { geometry in + if let imageData = store.dietImageData, + let uiImage = UIImage(data: imageData) { + Image(uiImage: uiImage) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: geometry.size.width, height: geometry.size.width) + .clipShape(.rect(cornerRadius: 20)) + .overlay(alignment: .topTrailing) { + Button(action: { send(.tapPhotoPickerDeleteButton)}) { + ZStack { + Circle() + .fill(Color.common100.opacity(0.5)) + .frame(width: 24, height: 24) + Image(.icnDelete) + .renderingMode(.template) + .resizable() + .tint(.common0) + .frame(width: 12, height: 12) + } + .padding(8) } - .padding(8) } - } - } else { - ZStack { - RoundedRectangle(cornerRadius: 8) - .frame(maxWidth: .infinity, maxHeight: .infinity) - .aspectRatio(1, contentMode: .fit) - - VStack(spacing: 8) { - Image(.icnImage) - .resizable() - .frame(width: 48, height: 48) + } else { + ZStack { + RoundedRectangle(cornerRadius: 8) + .frame(width: geometry.size.width, height: geometry.size.width) - Text("오늘 먹은 식단을 추가해보세요") - .typographyStyle(.body2Medium, with: .neutral400) + VStack(spacing: 8) { + Image(.icnImage) + .resizable() + .frame(width: 48, height: 48) + + Text("오늘 먹은 식단을 추가해보세요") + .typographyStyle(.body2Medium, with: .neutral400) + } } } } + .tint(Color.neutral100) + .aspectRatio(1.0, contentMode: .fit) } - .tint(Color.neutral100) .padding(20) } diff --git a/TnT/Projects/Presentation/Sources/DietRecordDetail/TraineeDietRecordDetailFeature.swift b/TnT/Projects/Presentation/Sources/DietRecordDetail/TraineeDietRecordDetailFeature.swift new file mode 100644 index 00000000..90b44852 --- /dev/null +++ b/TnT/Projects/Presentation/Sources/DietRecordDetail/TraineeDietRecordDetailFeature.swift @@ -0,0 +1,98 @@ +// +// TraineeDietRecordDetailFeature.swift +// Presentation +// +// Created by 박민서 on 2/12/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import Foundation +import ComposableArchitecture + +import Domain + +@Reducer +public struct TraineeDietRecordDetailFeature { + + @ObservableState + public struct State: Equatable { + // MARK: Data related state + /// 식단 ID + var dietId: Int + /// 식단 사진 URL + var dietImageURL: URL? + /// 식단 유형 + var dietType: DietType? + /// 식단 날짜 + var dietDate: Date? + /// 식단 메모 + var dietInfo: String + + public init( + dietId: Int, + dietImageURL: URL? = nil, + dietType: DietType? = nil, + dietDate: Date? = nil, + dietInfo: String = "" + ) { + self.dietId = dietId + self.dietImageURL = dietImageURL + self.dietType = dietType + self.dietDate = dietDate + self.dietInfo = dietInfo + } + } + + public enum Action: Sendable, ViewAction { + /// 뷰에서 발생한 액션을 처리합니다. + case view(View) + /// api 콜 액션을 처리합니다 + case api(APIAction) + /// 네비게이션 여부 설정 + case setNavigating + + @CasePathable + public enum View: Sendable, BindableAction { + /// 바인딩할 액션을 처리 + case binding(BindingAction) + /// 우측 상단 ellipsis 버튼 탭 + case tapEllipsisButton + } + + @CasePathable + public enum APIAction: Sendable { + /// 식단 정보 가져오기 APi + case getDietRecordDetail + } + } + + public init() {} + + public var body: some ReducerOf { + BindingReducer(action: \.view) + + Reduce { state, action in + switch action { + + case .view(let action): + switch action { + case .binding: + return .none + + case .tapEllipsisButton: + return .none + } + + case .api(let action): + switch action { + case .getDietRecordDetail: + // TODO: API 나오면 추후 연결 + return .none + } + + case .setNavigating: + return .none + } + } + } +} diff --git a/TnT/Projects/Presentation/Sources/DietRecordDetail/TraineeDietRecordDetailView.swift b/TnT/Projects/Presentation/Sources/DietRecordDetail/TraineeDietRecordDetailView.swift new file mode 100644 index 00000000..8a2d1655 --- /dev/null +++ b/TnT/Projects/Presentation/Sources/DietRecordDetail/TraineeDietRecordDetailView.swift @@ -0,0 +1,116 @@ +// +// TraineeDietRecordDetailView.swift +// Presentation +// +// Created by 박민서 on 2/12/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import SwiftUI +import ComposableArchitecture +import PhotosUI + +import Domain +import DesignSystem + +/// 식단 기록을 추가하는 화면 +@ViewAction(for: TraineeDietRecordDetailFeature.self) +public struct TraineeDietRecordDetailView: View { + + @Bindable public var store: StoreOf + @Environment(\.dismiss) var dismiss: DismissAction + + /// `TraineeDietRecordDetailView` 생성자 + /// - Parameter store: `TraineeDietRecordDetailFeature`와 연결된 Store + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + VStack(spacing: 0) { + TNavigation( + type: .LRButtonWithTitle( + leftImage: .icnArrowLeft, + centerTitle: "\(store.dietDate?.toString(format: .M월_d일) ?? "")", + rightImage: .icnEllipsis + ), + leftAction: { + dismiss() + }, + rightAction: { + send(.tapEllipsisButton) + } + ) + + VStack(spacing: 8) { + ImageSection() + + ContentSection() + + Spacer() + } + } + .navigationBarBackButtonHidden() + .keyboardDismissOnTap() + } + + // MARK: - Sections + @ViewBuilder + private func ImageSection() -> some View { + AsyncImage(url: store.dietImageURL) { phase in + switch phase { + case .empty: + if store.dietImageURL != nil { + ProgressView() + .tint(.red500) + .padding(20) + } else { + EmptyView() + } + + case .success(let image): + GeometryReader { geometry in + image + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: geometry.size.width, height: geometry.size.width) + .clipShape(.rect(cornerRadius: 20)) + } + .aspectRatio(1.0, contentMode: .fit) + .padding(20) + + case .failure(let error): + EmptyView() + + @unknown default: + EmptyView() + } + } + } + + @ViewBuilder + private func ContentSection() -> some View { + VStack(spacing: 8) { + VStack(alignment: .leading, spacing: 0) { + if let chipInfo = store.dietType?.chipInfo { + TChip(uiInfo: chipInfo) + } + HStack(spacing: 8) { + Text(store.dietDate?.toString(format: .yyyyMMddSlash) ?? "") + .typographyStyle(.body2Medium, with: .neutral600) + Text(store.dietDate?.toString(format: .a_HHmm) ?? "") + .typographyStyle(.body2Medium, with: .neutral600) + Spacer() + } + .frame(height: 42) + } + + TDivider(height: 2, color: .neutral100) + .padding(.vertical, 8) + + Text(store.dietInfo) + .typographyStyle(.body1Medium, with: .neutral800) + } + .padding(.horizontal, 20) + } +} diff --git a/TnT/Projects/Presentation/Sources/PresentationSupport/DietType.swift b/TnT/Projects/Presentation/Sources/PresentationSupport/DietType.swift index 6add7225..dce870e6 100644 --- a/TnT/Projects/Presentation/Sources/PresentationSupport/DietType.swift +++ b/TnT/Projects/Presentation/Sources/PresentationSupport/DietType.swift @@ -23,4 +23,8 @@ extension DietType { "🍰" } } + + var chipInfo: TChip.UIInfo { + return RecordType.diet(type: self).chipInfo + } }