diff --git a/Dietto/Dietto.xcodeproj/project.pbxproj b/Dietto/Dietto.xcodeproj/project.pbxproj index aa489a2..f673913 100644 --- a/Dietto/Dietto.xcodeproj/project.pbxproj +++ b/Dietto/Dietto.xcodeproj/project.pbxproj @@ -268,7 +268,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = UUYBHY988H; + DEVELOPMENT_TEAM = 5664C7G97A; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Dietto/Info.plist; @@ -301,7 +301,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = UUYBHY988H; + DEVELOPMENT_TEAM = 5664C7G97A; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Dietto/Info.plist; diff --git a/Dietto/Dietto/Apps/DIContainer.swift b/Dietto/Dietto/Apps/DIContainer.swift index 8f2e737..23befbe 100644 --- a/Dietto/Dietto/Apps/DIContainer.swift +++ b/Dietto/Dietto/Apps/DIContainer.swift @@ -11,28 +11,26 @@ import Observation final class DIContainer { private let alanUsecase: AlanUsecase private let pedometerUsecase: PedometerUsecase - private var interestsUsecase: InterestsUsecase - private var userStorageUsecase: UserStorageUsecase - private var weightHistoryUsecase: WeightHistoryUsecase + private let interestsUsecase: InterestsUsecase + private let userStorageUsecase: UserStorageUsecase + private let weightHistoryUsecase: WeightHistoryUsecase + private let ingredientUsecase: IngredientUsecase init() { self.alanUsecase = AlanUsecaseImpl(repository: NetworkRepositoryImpl()) self.pedometerUsecase = PedometerUsecaseImpl(pedometer: PedometerRepositoryImpl()) -// self.interestsUsecase = InterestsUsecaseImpl(repository: StorageRepositoryImpl()) -// self.userStorageUsecase = UserStorageUsecaseImpl(storage: StorageRepositoryImpl()) -// self.weightHistoryUsecase = WeightHistoryUsecaseImpl(repository: StorageRepositoryImpl()) - self.interestsUsecase = InterestsUsecaseImpl(repository: AnotherStorageRepositoryImpl()) self.userStorageUsecase = UserStorageUsecaseImpl(storage: AnotherStorageRepositoryImpl()) self.weightHistoryUsecase = WeightHistoryUsecaseImpl(repository: AnotherStorageRepositoryImpl()) + self.ingredientUsecase = IngredientUsecaseImpl(repository: AnotherStorageRepositoryImpl()) + + // Task.detached(priority: .background) { [weak self] in + // self?.interestsUsecase = InterestsUsecaseImpl(repository: AnotherStorageRepositoryImpl()) + // self?.userStorageUsecase = UserStorageUsecaseImpl(storage: AnotherStorageRepositoryImpl()) + // self?.weightHistoryUsecase = WeightHistoryUsecaseImpl(repository: AnotherStorageRepositoryImpl()) + // } -// Task.detached(priority: .background) { [weak self] in -// self?.interestsUsecase = InterestsUsecaseImpl(repository: AnotherStorageRepositoryImpl()) -// self?.userStorageUsecase = UserStorageUsecaseImpl(storage: AnotherStorageRepositoryImpl()) -// self?.weightHistoryUsecase = WeightHistoryUsecaseImpl(repository: AnotherStorageRepositoryImpl()) -// } - } func getHomeViewModel() -> HomeViewModel { @@ -51,7 +49,10 @@ final class DIContainer { } func getDietaryViewModel() -> DietaryViewModel { - DietaryViewModel(usecase: alanUsecase) + DietaryViewModel( + alanUsecase: alanUsecase, + ingredientUsecase: ingredientUsecase + ) } func getOnboardingViewModel() -> OnboardingViewModel { diff --git a/Dietto/Dietto/Data/DTO/IngredientDTO.swift b/Dietto/Dietto/Data/DTO/IngredientDTO.swift new file mode 100644 index 0000000..cf515c3 --- /dev/null +++ b/Dietto/Dietto/Data/DTO/IngredientDTO.swift @@ -0,0 +1,21 @@ +// +// IngredientDTO.swift +// Dietto +// +// Created by 안정흠 on 6/9/25. +// + + +import Foundation +import SwiftData + +@Model +final class IngredientDTO { + var id: UUID + var ingredient: String + + init(id: UUID, ingredient: String) { + self.id = id + self.ingredient = ingredient + } +} \ No newline at end of file diff --git a/Dietto/Dietto/Data/Network/NetworkError.swift b/Dietto/Dietto/Data/Network/NetworkError.swift index 68aab1c..df7e933 100644 --- a/Dietto/Dietto/Data/Network/NetworkError.swift +++ b/Dietto/Dietto/Data/Network/NetworkError.swift @@ -50,7 +50,7 @@ extension NetworkError { return ToastEntity( type: .error, title: "오류 발생", - message: self.errorDescription ?? "알 수 없는 오류입니다.", + message: self.errorDescription ?? "알 수 없는 에러가 발생했습니다.", duration: 3 ) } diff --git a/Dietto/Dietto/Domain/Entities/IngredientEntity.swift b/Dietto/Dietto/Domain/Entities/IngredientEntity.swift index fb65d7a..2d6543d 100644 --- a/Dietto/Dietto/Domain/Entities/IngredientEntity.swift +++ b/Dietto/Dietto/Domain/Entities/IngredientEntity.swift @@ -9,6 +9,11 @@ import Foundation //MARK: - 식재료는 저장합니다. struct IngredientEntity: Identifiable, Hashable { - let id = UUID() + let id: UUID let ingredient: String + + init(id: UUID = UUID(), ingredient: String) { + self.id = id + self.ingredient = ingredient + } } diff --git a/Dietto/Dietto/Domain/Usecases/IngredientUsecase.swift b/Dietto/Dietto/Domain/Usecases/IngredientUsecase.swift new file mode 100644 index 0000000..8164166 --- /dev/null +++ b/Dietto/Dietto/Domain/Usecases/IngredientUsecase.swift @@ -0,0 +1,59 @@ +// +// IngredientUsecase.swift +// Dietto +// +// Created by 안세훈 on 6/9/25. +// +import Foundation + +//MARK: - Interface +protocol IngredientUsecase{ + func insertIngredient(_ ingredient: IngredientEntity) async throws + func deleteIngredient(_ ingredient: IngredientEntity) async throws + func fetchIngredient() async throws -> [IngredientEntity] +} + +//MARK: - Implement +final class IngredientUsecaseImpl: IngredientUsecase where Repository.T == IngredientDTO { + private let repository: Repository + + init(repository: Repository) { + self.repository = repository + } + + func insertIngredient(_ ingredient: IngredientEntity) async throws { + do { + try await repository.insertData( + data: IngredientDTO(id: ingredient.id, ingredient: ingredient.ingredient) + ) + } + catch { + print(#function, error.localizedDescription) + throw StorageError.insertError + } + } + + func deleteIngredient(_ ingredient: IngredientEntity) async throws { + let id = ingredient.id + let predicate = #Predicate { $0.id == id } + do { try await repository.deleteData(where: predicate) } + catch { + print(#function, error.localizedDescription) + throw StorageError.deleteError + } + } + + func fetchIngredient() async throws -> [IngredientEntity] { + do { + let result = try await repository.fetchData(where: nil, sort: []) + return result.map{ IngredientEntity(id: $0.id, ingredient: $0.ingredient) } + } + catch { + print(#function, error.localizedDescription) + throw StorageError.fetchError + } + } + + +} + diff --git a/Dietto/Dietto/Presentation/Common/LogoProgress.swift b/Dietto/Dietto/Presentation/Common/LogoProgress.swift index 3e1e933..983e809 100644 --- a/Dietto/Dietto/Presentation/Common/LogoProgress.swift +++ b/Dietto/Dietto/Presentation/Common/LogoProgress.swift @@ -75,7 +75,7 @@ struct LogoProgressModifier: ViewModifier { if isPresented { Rectangle() .fill(Color.black.opacity(0.3)) -// .ignoresSafeArea() + .ignoresSafeArea() LogoProgress(isAnimated: $isAnimated, message: message) .onAppear { diff --git a/Dietto/Dietto/Presentation/Dietary/View/DietaryView.swift b/Dietto/Dietto/Presentation/Dietary/View/DietaryView.swift index 6bda697..a9c668b 100644 --- a/Dietto/Dietto/Presentation/Dietary/View/DietaryView.swift +++ b/Dietto/Dietto/Presentation/Dietary/View/DietaryView.swift @@ -18,8 +18,6 @@ struct DietaryView: View { @State private var myRefrigerlatorflowlayout : CGFloat = 0 //마이냉장고 @State private var isFoldMyRefrigerlator : Bool = false //마이냉장고 펼쳤다 접었다. - @State private var PushToRecommandView : Bool = false // 화면이동 - var body: some View { NavigationStack{ ZStack{ @@ -136,16 +134,20 @@ struct DietaryView: View { Spacer() - Button{ - withAnimation(.bouncy) { - isFoldMyRefrigerlator.toggle() + if !dietartViewModel.pastIngredients.isEmpty{ + Button{ + withAnimation(.bouncy) { + isFoldMyRefrigerlator.toggle() + } + }label: { + Image(systemName: isFoldMyRefrigerlator ? "chevron.down" : "chevron.up") + .frame(width: 10, height: 10) + .font(.pretendardBold20) } - }label: { - Image(systemName: isFoldMyRefrigerlator ? "chevron.down" : "chevron.up") - .frame(width: 10, height: 10) - .font(.pretendardBold20) + .foregroundStyle(.appMain) + .padding(.bottom, 8) } - .padding(.bottom, 8) + } } .padding() @@ -154,13 +156,8 @@ struct DietaryView: View { } HStack { Button("식단 추천받기") { - print("식단 추천 받기 버튼이 클릭댐") - if !dietartViewModel.presentIngredients.isEmpty { - dietartViewModel.fetchRecommendations(ingredients: dietartViewModel.presentIngredients) - PushToRecommandView = true - }else{ - print("비어있음 현재 식재료가 ") - } + dietartViewModel.fetchRecommendations(ingredients: dietartViewModel.presentIngredients) + } .font(.pretendardBold16) .foregroundStyle(.white) @@ -174,13 +171,13 @@ struct DietaryView: View { } } - .navigationDestination(isPresented: $PushToRecommandView) { + .navigationDestination(isPresented: $dietartViewModel.pushToRecommend) { RecommendView().environmentObject(dietartViewModel) } } } } -#Preview { - DietaryView(dietartViewModel: DietaryViewModel()) -} +//#Preview { +// DietaryView(dietartViewModel: DietaryViewModel()) +//} diff --git a/Dietto/Dietto/Presentation/Dietary/View/RecommendView.swift b/Dietto/Dietto/Presentation/Dietary/View/RecommendView.swift index e48a56b..fa7a98f 100644 --- a/Dietto/Dietto/Presentation/Dietary/View/RecommendView.swift +++ b/Dietto/Dietto/Presentation/Dietary/View/RecommendView.swift @@ -14,12 +14,11 @@ struct RecommendView: View { // @StateObject private var viewModel = DietaryViewModel() //디버깅용 @State private var isFoldRecommand : Bool = false // true : 펼친상태로 시작 , false: 가려진 채로 시작. -// @State private var contentHeight : CGFloat = 0 + // @State private var contentHeight : CGFloat = 0 var body: some View { ZStack { Color(.backGround).ignoresSafeArea(edges: .all) - VStack { ContainerView(paddingSize: 16) { HStack { @@ -27,7 +26,7 @@ struct RecommendView: View { Text("추천 레시피에 등록된 재료를 이용해 식사를 추천합니다.") .font(.pretendardSemiBold10) .foregroundStyle(.textFieldGray) - + ScrollView { LazyVStack(alignment: .leading, spacing: 16) { ForEach(viewModel.recommendList, id: \.title) { item in @@ -46,20 +45,6 @@ struct RecommendView: View { .padding(.horizontal) } .clipped() - -// Spacer() -// -// Button { -// withAnimation(.bouncy) { -// isFoldRecommand.toggle() -// } -// } label: { -// Image(systemName: isFoldRecommand ? "chevron.up" : "chevron.down") -// .frame(width: 10, height: 10) -// .font(.pretendardBold20) -// } -// .padding(.bottom, 8) -// .border(.black) } } .padding() @@ -67,15 +52,13 @@ struct RecommendView: View { .padding(.bottom, 10) } .padding(.top, 16) - - + .opacity(viewModel.recommendList.isEmpty ? 0 : 1) } .navigationTitle("추천 식사") .navigationBarTitleDisplayMode(.inline) .font(.pretendardBold16) - .progressOverlay(isPresented: $viewModel.isPresneted, message: "Alan이 식단을 생성하고 있어요 !") .toastView(toast: $viewModel.toast) - + .progressOverlay(isPresented: $viewModel.isPresneted, message: "Alan이 식단을 생성하고 있어요 !") } } ////디버깅용 diff --git a/Dietto/Dietto/Presentation/Dietary/ViewModel/DietaryViewModel.swift b/Dietto/Dietto/Presentation/Dietary/ViewModel/DietaryViewModel.swift index 6ea7804..4a118fd 100644 --- a/Dietto/Dietto/Presentation/Dietary/ViewModel/DietaryViewModel.swift +++ b/Dietto/Dietto/Presentation/Dietary/ViewModel/DietaryViewModel.swift @@ -9,43 +9,50 @@ import SwiftUI class DietaryViewModel: ObservableObject { @Published var isPresneted : Bool = false //로딩 여부 + @Published var pushToRecommend : Bool = false //push여부 @Published var toast: ToastEntity? //toast팝업 @Published var presentIngredients: [IngredientEntity] = [] //현재 - @Published var pastIngredients : [IngredientEntity] = [ - IngredientEntity(ingredient: "오징어"), - IngredientEntity(ingredient: "꼴뚜기"), - IngredientEntity(ingredient: "홍합"), - IngredientEntity(ingredient: "닭다리"), - IngredientEntity(ingredient: "연어머리"), - IngredientEntity(ingredient: "마늘"), - IngredientEntity(ingredient: "올리브유"), - IngredientEntity(ingredient: "양파"), - IngredientEntity(ingredient: "국간장"), - IngredientEntity(ingredient: "밀가루"), - IngredientEntity(ingredient: "참기름"), - IngredientEntity(ingredient: "들기름"), - IngredientEntity(ingredient: "통후추"), - IngredientEntity(ingredient: "미역"), - IngredientEntity(ingredient: "감자"), - IngredientEntity(ingredient: "와인"), - IngredientEntity(ingredient: "당근"), - IngredientEntity(ingredient: "배추") - ] + @Published var pastIngredients : [IngredientEntity] = [] //추천 리스트 @Published var recommendList : [RecommendEntity] = [] -// RecommendEntity(title: "asd", description: "21124387923784235879235897"), -// RecommendEntity(title: "321123132132", description: "4539259340830459378345890"), -// RecommendEntity(title: "32112321131233132132", description: "453925934083041233123213213211231313259378345890"), -// RecommendEntity(title: "엔티티", description: "카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 카리나 ") - private let usecase : AlanUsecase + + private let alanUsecase : AlanUsecase + private let ingredientUsecase : IngredientUsecase //MARK: - init - init(usecase: AlanUsecase = AlanUsecaseImpl(repository: NetworkRepositoryImpl())) { - self.usecase = usecase + init( + alanUsecase: AlanUsecase , + ingredientUsecase : IngredientUsecase + ) { + self.alanUsecase = alanUsecase + self.ingredientUsecase = ingredientUsecase + + loadPastIngredients() + } + + //MARK: - 과거에 있던 식재료 가져오기. + func loadPastIngredients() { + Task{ + do { + let result = try await ingredientUsecase.fetchIngredient() + await MainActor.run { + self.pastIngredients = result + } + } + catch { + await MainActor.run { + self.toast = ToastEntity( + type: .error, + title: "조회 실패", + message: "과거 식재료 조회에 실패하였습니다." + ) + } + } + } } - //MARK: - 현재 식단에 있는거 생성 + //MARK: - 현재 식재료 생성 func addpresentIngredients(_ ingredient: String) { let trimmed = ingredient.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty, @@ -53,41 +60,85 @@ class DietaryViewModel: ObservableObject { presentIngredients.append(IngredientEntity(ingredient: trimmed)) } + //MARK: - 현재 식단에 있는거 삭제 func removepresentIngredients(_ ingredient: IngredientEntity) { presentIngredients.removeAll { $0.id == ingredient.id } } - //MARK: - 과거 식단으로 추가 - // func addpastIngredients(ingredients: [IngredientEntity]) { - // for ingredient in ingredients { - // if let index = presentIngredients.firstIndex(where: { $0.name == ingredient.name }) { - // let removed = presentIngredients.remove(at: index) - // if !pastIngredients.contains(where: { $0.name == removed.name }) { - // pastIngredients.append(removed) - // } - // } - // } - // } + //MARK: - 현재 식재료를 삭제하고 현재 식재료를 과거 식재료로 추가 + func addpastIngredients() { + Task { + for ingredient in self.presentIngredients { + if !self.pastIngredients.contains(where: { $0.ingredient == ingredient.ingredient }) { + do { + try await ingredientUsecase.insertIngredient(ingredient) + await MainActor.run { + self.pastIngredients.append(ingredient) + } + } catch { + await MainActor.run { + self.toast = ToastEntity( + type: .error, + title: "저장 실패", + message: "식재료 저장 중 오류가 발생했습니다." + ) + } + } + } + } + await MainActor.run { + self.presentIngredients.removeAll() + } + } + } + //MARK: - 과거 식단에 있던거 삭제 func removepastIngredients(_ ingredient: IngredientEntity) { - pastIngredients.removeAll { $0.id == ingredient.id } + Task { + do { + try await ingredientUsecase.deleteIngredient(ingredient) + await MainActor.run { + self.pastIngredients.removeAll { $0.id == ingredient.id } + self.toast = ToastEntity( + type: .success, + title: "삭제 완료", + message: "선택한 식재료가 삭제되었습니다.", + duration: 2 + ) + } + } catch { + await MainActor.run { + self.toast = ToastEntity( + type: .error, + title: "삭제 실패", + message: "식재료 삭제 중 오류가 발생했습니다." + ) + } + } + } } + //MARK: - 현재 재료를 통해 식단 추천 받기. func fetchRecommendations(ingredients: [IngredientEntity]) { + self.recommendList.removeAll() isPresneted = true + pushToRecommend = true Task { do { - let result = try await usecase.fetchRecommend(ingredients: ingredients) + let result = try await alanUsecase.fetchRecommend(ingredients: ingredients) await MainActor.run { self.recommendList = result + + addpastIngredients() + self.toast = ToastEntity( type: .success, - title: "완료", + title: "추천 완료", message: "식단 추천을 완료하였습니다.", duration: 2 ) @@ -102,13 +153,12 @@ class DietaryViewModel: ObservableObject { await MainActor.run { self.toast = ToastEntity( type: .error, - title: "에러", + title: "추천 에러", message: error.localizedDescription ) isPresneted = false } } } - } } diff --git a/Dietto/Dietto/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/Dietto/Dietto/Resources/Assets.xcassets/AccentColor.colorset/Contents.json index eb87897..1e73b01 100644 --- a/Dietto/Dietto/Resources/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/Dietto/Dietto/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -1,6 +1,15 @@ { "colors" : [ { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x76", + "green" : "0x76", + "red" : "0xEC" + } + }, "idiom" : "universal" } ], diff --git a/Dietto/Dietto/Resources/keyContainer.plist b/Dietto/Dietto/Resources/keyContainer.plist index c3eb09a..4fb8329 100644 --- a/Dietto/Dietto/Resources/keyContainer.plist +++ b/Dietto/Dietto/Resources/keyContainer.plist @@ -3,6 +3,6 @@ AlanClientKey - 4b744a65-832d-4f67-868f-ad7130c02db1 + 72cbf1bc-c25f-40d4-abf0-e85e675fb7e4