From f01278079dae37a8b0bf719137f6197c35edf401 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=BC=ED=98=81?= Date: Sun, 31 May 2026 07:27:16 +0900 Subject: [PATCH 1/4] =?UTF-8?q?refactor:=20=EB=A9=80=ED=8B=B0=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=EC=95=84=ED=82=A4=ED=85=8D=EC=B2=98=20=EB=8F=84?= =?UTF-8?q?=EC=9E=85=20=EB=B0=8F=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EC=9E=AC=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 프로젝트 구조를 단일 모듈에서 멀티 모듈(feature, core) 체제로 전환 - core 레이어 분리 및 모듈화: - **core:model**: 도메인 엔티티 및 공통 데이터 모델 이동 - **core:domain**: Repository 인터페이스 정의 이동 - **core:network**: Retrofit 서비스, DTO, Interceptor 등 통신 로직 이동 - **core:data**: Repository 구현체 및 Mapper 로직 이동 - **core:designsystem**: 공통 UI 컴포넌트, 테마(Color, Type, Theme), 리소스(폰트, 이미지) 이동 - **core:datastore**: TokenManager 등 로컬 저장소 로직 이동 - feature 레이어 분리 및 모듈화: - 기능 단위로 모듈 분리 (auth, home, combination, profile, onboarding) - 각 모듈로 Screen, ViewModel, 기능 전용 컴포넌트 이동 - 모듈화에 따른 패키지 경로 수정 및 Import 문 최적화 - 더 이상 사용하지 않는 Swagger API 예제 코드(ExampleApiUsageViewModel) 삭제 --- .../viewmodel/ExampleApiUsageViewModel.kt | 272 ------------------ .../core/data}/di/RepositoryModule.kt | 18 +- .../core}/data/mapper/CombinationMapper.kt | 14 +- .../core}/data/mapper/RecipeMapper.kt | 36 +-- .../data/repositoryimpl/AuthRepositoryImpl.kt | 39 +-- .../CombinationRepositoryImpl.kt | 40 +-- .../repositoryimpl/RecipeRepositoryImpl.kt | 14 +- .../data/repositoryimpl/UserRepositoryImpl.kt | 18 +- .../hackathon/core/datastore}/TokenManager.kt | 2 +- .../designsystem}/component/CategoryChip.kt | 4 +- .../component/CombinationCard.kt | 16 +- .../designsystem}/component/IngredientRow.kt | 4 +- .../core/designsystem}/component/SearchBar.kt | 2 +- .../core/designsystem}/component/TagChip.kt | 4 +- .../designsystem}/component/TopAppLogoBar.kt | 6 +- .../component/tag/TagConvenience.kt | 2 +- .../designsystem}/component/tag/TagEtc.kt | 2 +- .../component/tag/TagHaidirao.kt | 2 +- .../designsystem}/component/tag/TagSubway.kt | 2 +- .../core/designsystem}/theme/Color.kt | 2 +- .../core/designsystem}/theme/Theme.kt | 2 +- .../core/designsystem}/theme/Type.kt | 4 +- .../core/designsystem}/util/ModifierExt.kt | 2 +- .../src/main/res/drawable/ic_add.xml | 0 .../src/main/res/drawable/ic_camera.xml | 0 .../src/main/res/drawable/ic_home.xml | 0 .../src/main/res/drawable/ic_hot.png | Bin .../res/drawable/ic_launcher_background.xml | 0 .../src/main/res/drawable/ic_logo.png | Bin .../src/main/res/drawable/ic_logo_rec.png | Bin .../src/main/res/drawable/ic_my.xml | 0 .../src/main/res/drawable/ic_search.xml | 0 .../src/main/res/drawable/ic_splash_inset.png | Bin .../src/main/res/font/pretendard_bold.ttf | Bin .../src/main/res/font/pretendard_medium.ttf | Bin .../src/main/res/font/pretendard_semibold.ttf | Bin .../core}/domain/repository/AuthRepository.kt | 4 +- .../repository/CombinationRepository.kt | 8 +- .../domain/repository/RecipeRepository.kt | 6 +- .../core}/domain/repository/UserRepository.kt | 4 +- .../example/hackathon/core/model}/Category.kt | 2 +- .../hackathon/core/model}/Combination.kt | 2 +- .../hackathon/core/model}/DummyData.kt | 16 +- .../hackathon/core/model}/RecipeDetail.kt | 2 +- .../hackathon/core/model}/RecipeRanking.kt | 2 +- .../com/example/hackathon/core/model}/User.kt | 2 +- .../core}/network/AuthInterceptor.kt | 8 +- .../hackathon/core/network}/di/ApiModule.kt | 10 +- .../core/network}/di/NetworkModule.kt | 8 +- .../hackathon/core/network}/di/Qualifiers.kt | 2 +- .../dto/request/CreateCombinationRequest.kt | 2 +- .../core/network}/dto/request/LoginRequest.kt | 2 +- .../network}/dto/request/LogoutRequest.kt | 2 +- .../network}/dto/request/RegisterRequest.kt | 4 +- .../network}/dto/request/ReissueRequest.kt | 2 +- .../network}/dto/request/SignupRequest.kt | 2 +- .../dto/request/UpdateCombinationRequest.kt | 2 +- .../dto/request/UpdateProfileRequest.kt | 2 +- .../network}/dto/response/BaseResponse.kt | 2 +- .../dto/response/CombinationResponse.kt | 2 +- .../network}/dto/response/DetailResponse.kt | 2 +- .../network}/dto/response/LoginResponse.kt | 2 +- .../dto/response/MyPageRecipeResponse.kt | 2 +- .../network}/dto/response/PostPreviewDto.kt | 2 +- .../dto/response/RecipeDetailResponse.kt | 2 +- .../dto/response/RecipeRankingResponse.kt | 2 +- .../network}/dto/response/ReissueResponse.kt | 2 +- .../network}/dto/response/SignupResponse.kt | 2 +- .../dto/response/UserProfileResponse.kt | 2 +- .../network}/dto/response/UserResponse.kt | 2 +- .../core/network}/service/AuthService.kt | 16 +- .../network}/service/CombinationService.kt | 14 +- .../core/network}/service/HomeService.kt | 2 +- .../core/network}/service/RecipeService.kt | 8 +- .../core/network}/service/UserService.kt | 10 +- .../hackathon/feature}/auth/LoginScreen.kt | 16 +- .../hackathon/feature/auth}/LoginViewModel.kt | 8 +- .../feature}/auth/RegistrationScreen.kt | 12 +- .../auth/RegistrationSuccessScreen.kt | 10 +- .../feature/auth}/RegistrationViewModel.kt | 8 +- .../combination/CreateCombinationScreen.kt | 22 +- .../CreateCombinationViewModel.kt | 10 +- .../feature}/combination/DetailScreen.kt | 12 +- .../feature/combination}/DetailViewModel.kt | 8 +- .../component/HashTagInputSection.kt | 11 +- .../component/ImageUploadSection.kt | 10 +- .../component/IngredientInputSection.kt | 6 +- .../component/VisibilitySelectionSection.kt | 6 +- .../CreateCombinationViewModelTest.kt | 11 +- .../combination}/MainDispatcherRule.kt | 2 +- .../feature}/home/HomeCombinationList.kt | 12 +- .../hackathon/feature}/home/HomeContent.kt | 14 +- .../hackathon/feature}/home/HomeScreen.kt | 14 +- .../feature}/home/HomeStateContent.kt | 6 +- .../hackathon/feature}/home/HomeTopBar.kt | 6 +- .../hackathon/feature/home}/HomeViewModel.kt | 8 +- .../hackathon/feature}/home/SearchScreen.kt | 12 +- .../feature/home}/SearchViewModel.kt | 6 +- .../home}/component/CombinationList.kt | 10 +- .../feature/home}/component/FilterBar.kt | 10 +- .../home}/component/SearchComponent.kt | 8 +- .../feature}/onboarding/OnboardingScreen.kt | 6 +- .../feature}/profile/EditProfileScreen.kt | 10 +- .../feature/profile}/EditProfileViewModel.kt | 4 +- .../feature/profile}/MyPageViewModel.kt | 12 +- .../hackathon/feature}/profile/MyScreen.kt | 20 +- 106 files changed, 358 insertions(+), 627 deletions(-) delete mode 100644 app/src/main/java/com/example/hackathon/presentation/viewmodel/ExampleApiUsageViewModel.kt rename {app/src/main/java/com/example/hackathon => core/data/src/main/java/com/example/hackathon/core/data}/di/RepositoryModule.kt (55%) rename {app/src/main/java/com/example/hackathon => core/data/src/main/java/com/example/hackathon/core}/data/mapper/CombinationMapper.kt (60%) rename {app/src/main/java/com/example/hackathon => core/data/src/main/java/com/example/hackathon/core}/data/mapper/RecipeMapper.kt (72%) rename {app/src/main/java/com/example/hackathon => core/data/src/main/java/com/example/hackathon/core}/data/repositoryimpl/AuthRepositoryImpl.kt (86%) rename {app/src/main/java/com/example/hackathon => core/data/src/main/java/com/example/hackathon/core}/data/repositoryimpl/CombinationRepositoryImpl.kt (95%) rename {app/src/main/java/com/example/hackathon => core/data/src/main/java/com/example/hackathon/core}/data/repositoryimpl/RecipeRepositoryImpl.kt (93%) rename {app/src/main/java/com/example/hackathon => core/data/src/main/java/com/example/hackathon/core}/data/repositoryimpl/UserRepositoryImpl.kt (87%) rename {app/src/main/java/com/example/hackathon/data/local => core/datastore/src/main/java/com/example/hackathon/core/datastore}/TokenManager.kt (98%) rename {app/src/main/java/com/example/hackathon/core => core/designsystem/src/main/java/com/example/hackathon/core/designsystem}/component/CategoryChip.kt (94%) rename {app/src/main/java/com/example/hackathon/core => core/designsystem/src/main/java/com/example/hackathon/core/designsystem}/component/CombinationCard.kt (96%) rename {app/src/main/java/com/example/hackathon/core => core/designsystem/src/main/java/com/example/hackathon/core/designsystem}/component/IngredientRow.kt (89%) rename {app/src/main/java/com/example/hackathon/core => core/designsystem/src/main/java/com/example/hackathon/core/designsystem}/component/SearchBar.kt (95%) rename {app/src/main/java/com/example/hackathon/presentation => core/designsystem/src/main/java/com/example/hackathon/core/designsystem}/component/TagChip.kt (94%) rename {app/src/main/java/com/example/hackathon/core => core/designsystem/src/main/java/com/example/hackathon/core/designsystem}/component/TopAppLogoBar.kt (88%) rename {app/src/main/java/com/example/hackathon/core => core/designsystem/src/main/java/com/example/hackathon/core/designsystem}/component/tag/TagConvenience.kt (70%) rename {app/src/main/java/com/example/hackathon/core => core/designsystem/src/main/java/com/example/hackathon/core/designsystem}/component/tag/TagEtc.kt (69%) rename {app/src/main/java/com/example/hackathon/core => core/designsystem/src/main/java/com/example/hackathon/core/designsystem}/component/tag/TagHaidirao.kt (69%) rename {app/src/main/java/com/example/hackathon/core => core/designsystem/src/main/java/com/example/hackathon/core/designsystem}/component/tag/TagSubway.kt (69%) rename {app/src/main/java/com/example/hackathon/ui => core/designsystem/src/main/java/com/example/hackathon/core/designsystem}/theme/Color.kt (97%) rename {app/src/main/java/com/example/hackathon/ui => core/designsystem/src/main/java/com/example/hackathon/core/designsystem}/theme/Theme.kt (95%) rename {app/src/main/java/com/example/hackathon/ui => core/designsystem/src/main/java/com/example/hackathon/core/designsystem}/theme/Type.kt (97%) rename {app/src/main/java/com/example/hackathon/core => core/designsystem/src/main/java/com/example/hackathon/core/designsystem}/util/ModifierExt.kt (98%) rename {app => core/designsystem}/src/main/res/drawable/ic_add.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_camera.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_home.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_hot.png (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_launcher_background.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_logo.png (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_logo_rec.png (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_my.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_search.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_splash_inset.png (100%) rename {app => core/designsystem}/src/main/res/font/pretendard_bold.ttf (100%) rename {app => core/designsystem}/src/main/res/font/pretendard_medium.ttf (100%) rename {app => core/designsystem}/src/main/res/font/pretendard_semibold.ttf (100%) rename {app/src/main/java/com/example/hackathon => core/domain/src/main/java/com/example/hackathon/core}/domain/repository/AuthRepository.kt (88%) rename {app/src/main/java/com/example/hackathon => core/domain/src/main/java/com/example/hackathon/core}/domain/repository/CombinationRepository.kt (85%) rename {app/src/main/java/com/example/hackathon => core/domain/src/main/java/com/example/hackathon/core}/domain/repository/RecipeRepository.kt (80%) rename {app/src/main/java/com/example/hackathon => core/domain/src/main/java/com/example/hackathon/core}/domain/repository/UserRepository.kt (66%) rename {app/src/main/java/com/example/hackathon/domain/entity => core/model/src/main/java/com/example/hackathon/core/model}/Category.kt (78%) rename {app/src/main/java/com/example/hackathon/domain/entity => core/model/src/main/java/com/example/hackathon/core/model}/Combination.kt (90%) rename {app/src/main/java/com/example/hackathon/data/local => core/model/src/main/java/com/example/hackathon/core/model}/DummyData.kt (97%) rename {app/src/main/java/com/example/hackathon/domain/entity => core/model/src/main/java/com/example/hackathon/core/model}/RecipeDetail.kt (92%) rename {app/src/main/java/com/example/hackathon/domain/entity => core/model/src/main/java/com/example/hackathon/core/model}/RecipeRanking.kt (86%) rename {app/src/main/java/com/example/hackathon/domain/entity => core/model/src/main/java/com/example/hackathon/core/model}/User.kt (69%) rename {app/src/main/java/com/example/hackathon/data => core/network/src/main/java/com/example/hackathon/core}/network/AuthInterceptor.kt (94%) rename {app/src/main/java/com/example/hackathon => core/network/src/main/java/com/example/hackathon/core/network}/di/ApiModule.kt (74%) rename {app/src/main/java/com/example/hackathon => core/network/src/main/java/com/example/hackathon/core/network}/di/NetworkModule.kt (91%) rename {app/src/main/java/com/example/hackathon => core/network/src/main/java/com/example/hackathon/core/network}/di/Qualifiers.kt (80%) rename {app/src/main/java/com/example/hackathon/data => core/network/src/main/java/com/example/hackathon/core/network}/dto/request/CreateCombinationRequest.kt (79%) rename {app/src/main/java/com/example/hackathon/data => core/network/src/main/java/com/example/hackathon/core/network}/dto/request/LoginRequest.kt (82%) rename {app/src/main/java/com/example/hackathon/data => core/network/src/main/java/com/example/hackathon/core/network}/dto/request/LogoutRequest.kt (67%) rename {app/src/main/java/com/example/hackathon/data => core/network/src/main/java/com/example/hackathon/core/network}/dto/request/RegisterRequest.kt (88%) rename {app/src/main/java/com/example/hackathon/data => core/network/src/main/java/com/example/hackathon/core/network}/dto/request/ReissueRequest.kt (88%) rename {app/src/main/java/com/example/hackathon/data => core/network/src/main/java/com/example/hackathon/core/network}/dto/request/SignupRequest.kt (83%) rename {app/src/main/java/com/example/hackathon/data => core/network/src/main/java/com/example/hackathon/core/network}/dto/request/UpdateCombinationRequest.kt (79%) rename {app/src/main/java/com/example/hackathon/data => core/network/src/main/java/com/example/hackathon/core/network}/dto/request/UpdateProfileRequest.kt (74%) rename {app/src/main/java/com/example/hackathon/data => core/network/src/main/java/com/example/hackathon/core/network}/dto/response/BaseResponse.kt (72%) rename {app/src/main/java/com/example/hackathon/data => core/network/src/main/java/com/example/hackathon/core/network}/dto/response/CombinationResponse.kt (85%) rename {app/src/main/java/com/example/hackathon/data => core/network/src/main/java/com/example/hackathon/core/network}/dto/response/DetailResponse.kt (91%) rename {app/src/main/java/com/example/hackathon/data => core/network/src/main/java/com/example/hackathon/core/network}/dto/response/LoginResponse.kt (85%) rename {app/src/main/java/com/example/hackathon/data => core/network/src/main/java/com/example/hackathon/core/network}/dto/response/MyPageRecipeResponse.kt (88%) rename {app/src/main/java/com/example/hackathon/data => core/network/src/main/java/com/example/hackathon/core/network}/dto/response/PostPreviewDto.kt (90%) rename {app/src/main/java/com/example/hackathon/data => core/network/src/main/java/com/example/hackathon/core/network}/dto/response/RecipeDetailResponse.kt (92%) rename {app/src/main/java/com/example/hackathon/data => core/network/src/main/java/com/example/hackathon/core/network}/dto/response/RecipeRankingResponse.kt (87%) rename {app/src/main/java/com/example/hackathon/data => core/network/src/main/java/com/example/hackathon/core/network}/dto/response/ReissueResponse.kt (91%) rename {app/src/main/java/com/example/hackathon/data => core/network/src/main/java/com/example/hackathon/core/network}/dto/response/SignupResponse.kt (84%) rename {app/src/main/java/com/example/hackathon/data => core/network/src/main/java/com/example/hackathon/core/network}/dto/response/UserProfileResponse.kt (88%) rename {app/src/main/java/com/example/hackathon/data => core/network/src/main/java/com/example/hackathon/core/network}/dto/response/UserResponse.kt (75%) rename {app/src/main/java/com/example/hackathon/data => core/network/src/main/java/com/example/hackathon/core/network}/service/AuthService.kt (69%) rename {app/src/main/java/com/example/hackathon/data => core/network/src/main/java/com/example/hackathon/core/network}/service/CombinationService.kt (83%) rename {app/src/main/java/com/example/hackathon/data => core/network/src/main/java/com/example/hackathon/core/network}/service/HomeService.kt (65%) rename {app/src/main/java/com/example/hackathon/data => core/network/src/main/java/com/example/hackathon/core/network}/service/RecipeService.kt (86%) rename {app/src/main/java/com/example/hackathon/data => core/network/src/main/java/com/example/hackathon/core/network}/service/UserService.kt (69%) rename {app/src/main/java/com/example/hackathon/presentation/screen => feature/auth/src/main/java/com/example/hackathon/feature}/auth/LoginScreen.kt (97%) rename {app/src/main/java/com/example/hackathon/presentation/viewmodel => feature/auth/src/main/java/com/example/hackathon/feature/auth}/LoginViewModel.kt (96%) rename {app/src/main/java/com/example/hackathon/presentation/screen => feature/auth/src/main/java/com/example/hackathon/feature}/auth/RegistrationScreen.kt (98%) rename {app/src/main/java/com/example/hackathon/presentation/screen => feature/auth/src/main/java/com/example/hackathon/feature}/auth/RegistrationSuccessScreen.kt (91%) rename {app/src/main/java/com/example/hackathon/presentation/viewmodel => feature/auth/src/main/java/com/example/hackathon/feature/auth}/RegistrationViewModel.kt (94%) rename {app/src/main/java/com/example/hackathon/presentation/screen => feature/combination/src/main/java/com/example/hackathon/feature}/combination/CreateCombinationScreen.kt (94%) rename {app/src/main/java/com/example/hackathon/presentation/viewmodel => feature/combination/src/main/java/com/example/hackathon/feature/combination}/CreateCombinationViewModel.kt (95%) rename {app/src/main/java/com/example/hackathon/presentation/screen => feature/combination/src/main/java/com/example/hackathon/feature}/combination/DetailScreen.kt (97%) rename {app/src/main/java/com/example/hackathon/presentation/viewmodel => feature/combination/src/main/java/com/example/hackathon/feature/combination}/DetailViewModel.kt (93%) rename {app/src/main/java/com/example/hackathon/presentation => feature/combination/src/main/java/com/example/hackathon/feature/combination}/component/HashTagInputSection.kt (91%) rename {app/src/main/java/com/example/hackathon/presentation => feature/combination/src/main/java/com/example/hackathon/feature/combination}/component/ImageUploadSection.kt (97%) rename {app/src/main/java/com/example/hackathon/presentation => feature/combination/src/main/java/com/example/hackathon/feature/combination}/component/IngredientInputSection.kt (96%) rename {app/src/main/java/com/example/hackathon/presentation => feature/combination/src/main/java/com/example/hackathon/feature/combination}/component/VisibilitySelectionSection.kt (94%) rename {app/src/test/java/com/example/hackathon/presentation/viewmodel => feature/combination/src/test/java/com/example/hackathon/feature/combination}/CreateCombinationViewModelTest.kt (94%) rename {app/src/test/java/com/example/hackathon/util => feature/combination/src/test/java/com/example/hackathon/feature/combination}/MainDispatcherRule.kt (93%) rename {app/src/main/java/com/example/hackathon/presentation/screen => feature/home/src/main/java/com/example/hackathon/feature}/home/HomeCombinationList.kt (88%) rename {app/src/main/java/com/example/hackathon/presentation/screen => feature/home/src/main/java/com/example/hackathon/feature}/home/HomeContent.kt (93%) rename {app/src/main/java/com/example/hackathon/presentation/screen => feature/home/src/main/java/com/example/hackathon/feature}/home/HomeScreen.kt (88%) rename {app/src/main/java/com/example/hackathon/presentation/screen => feature/home/src/main/java/com/example/hackathon/feature}/home/HomeStateContent.kt (93%) rename {app/src/main/java/com/example/hackathon/presentation/screen => feature/home/src/main/java/com/example/hackathon/feature}/home/HomeTopBar.kt (90%) rename {app/src/main/java/com/example/hackathon/presentation/viewmodel => feature/home/src/main/java/com/example/hackathon/feature/home}/HomeViewModel.kt (97%) rename {app/src/main/java/com/example/hackathon/presentation/screen => feature/home/src/main/java/com/example/hackathon/feature}/home/SearchScreen.kt (95%) rename {app/src/main/java/com/example/hackathon/presentation/viewmodel => feature/home/src/main/java/com/example/hackathon/feature/home}/SearchViewModel.kt (96%) rename {app/src/main/java/com/example/hackathon/presentation => feature/home/src/main/java/com/example/hackathon/feature/home}/component/CombinationList.kt (85%) rename {app/src/main/java/com/example/hackathon/presentation => feature/home/src/main/java/com/example/hackathon/feature/home}/component/FilterBar.kt (87%) rename {app/src/main/java/com/example/hackathon/presentation => feature/home/src/main/java/com/example/hackathon/feature/home}/component/SearchComponent.kt (91%) rename {app/src/main/java/com/example/hackathon/presentation/screen => feature/onboarding/src/main/java/com/example/hackathon/feature}/onboarding/OnboardingScreen.kt (95%) rename {app/src/main/java/com/example/hackathon/presentation/screen => feature/profile/src/main/java/com/example/hackathon/feature}/profile/EditProfileScreen.kt (97%) rename {app/src/main/java/com/example/hackathon/presentation/viewmodel => feature/profile/src/main/java/com/example/hackathon/feature/profile}/EditProfileViewModel.kt (96%) rename {app/src/main/java/com/example/hackathon/presentation/viewmodel => feature/profile/src/main/java/com/example/hackathon/feature/profile}/MyPageViewModel.kt (95%) rename {app/src/main/java/com/example/hackathon/presentation/screen => feature/profile/src/main/java/com/example/hackathon/feature}/profile/MyScreen.kt (94%) diff --git a/app/src/main/java/com/example/hackathon/presentation/viewmodel/ExampleApiUsageViewModel.kt b/app/src/main/java/com/example/hackathon/presentation/viewmodel/ExampleApiUsageViewModel.kt deleted file mode 100644 index c9cda8a..0000000 --- a/app/src/main/java/com/example/hackathon/presentation/viewmodel/ExampleApiUsageViewModel.kt +++ /dev/null @@ -1,272 +0,0 @@ -package com.example.hackathon.presentation.viewmodel - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.example.hackathon.data.dto.request.LoginRequest -import com.example.hackathon.data.dto.request.UpdateProfileRequest -import com.example.hackathon.data.local.TokenManager -import com.example.hackathon.data.service.AuthService -import com.example.hackathon.data.service.RecipeService -import com.example.hackathon.data.service.UserService -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch -import javax.inject.Inject - -/** - * Swagger API 연동 예시 ViewModel - * - * 이 파일은 실제 사용 예시를 보여주는 참고용 코드입니다. - * 실제 프로젝트에서는 Repository 패턴을 사용하는 것을 권장합니다. - */ -@HiltViewModel -class ExampleApiUsageViewModel - @Inject - constructor( - private val authService: AuthService, - private val userService: UserService, - private val recipeService: RecipeService, - private val tokenManager: TokenManager, - ) : ViewModel() { - - // ========== 1. 로그인 플로우 예시 ========== - private val _loginState = MutableStateFlow(LoginExampleState()) - val loginState: StateFlow = _loginState.asStateFlow() - - /** - * 로그인 → 토큰 저장 → 마이페이지 조회 플로우 - * @param nickname 닉네임 (이메일이 아닌 닉네임 사용) - * @param password 비밀번호 - */ - fun loginAndLoadProfile(nickname: String, password: String) { - viewModelScope.launch { - _loginState.value = _loginState.value.copy(isLoading = true, error = null) - - try { - // 1. 로그인 API 호출 (nickname으로 로그인) - // 서버 응답: {"accessToken":"...","refreshToken":"..."} (BaseResponse 래퍼 없음) - val loginResponse = authService.login(LoginRequest(nickname = nickname, password = password)) - - // 2. 토큰 저장 - tokenManager.saveTokens( - accessToken = loginResponse.accessToken, - refreshToken = loginResponse.refreshToken, - ) - - // 3. 마이페이지 조회 (토큰이 자동으로 헤더에 추가됨) - // 서버 응답: {"nickname":"...","myRecipeCount":0} (BaseResponse 래퍼 없음) - val profile = userService.getMyPage() - _loginState.value = _loginState.value.copy( - isLoading = false, - isSuccess = true, - userProfile = profile, - ) - } catch (e: Exception) { - _loginState.value = _loginState.value.copy( - isLoading = false, - error = e.message ?: "로그인에 실패했습니다", - ) - } - } - } - - // ========== 2. 레시피 목록 조회 예시 ========== - private val _recipeListState = MutableStateFlow(RecipeListState()) - val recipeListState: StateFlow = _recipeListState.asStateFlow() - - /** - * 레시피 목록 조회 (홈 화면용) - */ - fun loadRecipes(page: Int? = null, category: String? = null) { - viewModelScope.launch { - _recipeListState.value = _recipeListState.value.copy(isLoading = true, error = null) - - try { - val response = recipeService.getRecipes(page = page, category = category) - - if (response.code == 200 && response.data != null) { - _recipeListState.value = _recipeListState.value.copy( - isLoading = false, - recipes = response.data, - ) - } else { - _recipeListState.value = _recipeListState.value.copy( - isLoading = false, - error = response.message, - ) - } - } catch (e: Exception) { - _recipeListState.value = _recipeListState.value.copy( - isLoading = false, - error = e.message ?: "레시피 목록을 불러오는데 실패했습니다", - ) - } - } - } - - /** - * 레시피 검색 - */ - fun searchRecipes(keyword: String) { - viewModelScope.launch { - _recipeListState.value = _recipeListState.value.copy(isLoading = true, error = null) - - try { - val response = recipeService.searchRecipes(keyword) - - if (response.code == 200 && response.data != null) { - _recipeListState.value = _recipeListState.value.copy( - isLoading = false, - recipes = response.data, - ) - } else { - _recipeListState.value = _recipeListState.value.copy( - isLoading = false, - error = response.message, - ) - } - } catch (e: Exception) { - _recipeListState.value = _recipeListState.value.copy( - isLoading = false, - error = e.message ?: "검색에 실패했습니다", - ) - } - } - } - - /** - * 레시피 좋아요 - */ - fun likeRecipe(postId: Long) { - viewModelScope.launch { - try { - val response = recipeService.likeRecipe(postId) - - if (response.isSuccessful) { - // 성공 시 목록 새로고침 또는 로컬 상태 업데이트 - loadRecipes() - } else { - _recipeListState.value = _recipeListState.value.copy( - error = "서버 오류: ${response.code()} ${response.message()}", - ) - } - } catch (e: Exception) { - _recipeListState.value = _recipeListState.value.copy( - error = e.message ?: "좋아요에 실패했습니다", - ) - } - } - } - - // ========== 3. 마이페이지 프로필 수정 예시 ========== - private val _profileState = MutableStateFlow(ProfileState()) - val profileState: StateFlow = _profileState.asStateFlow() - - /** - * 프로필 조회 - */ - fun loadProfile() { - viewModelScope.launch { - _profileState.value = _profileState.value.copy(isLoading = true, error = null) - - try { - // 서버 응답: {"nickname":"...","myRecipeCount":0} (BaseResponse 래퍼 없음) - val profile = userService.getMyPage() - _profileState.value = _profileState.value.copy( - isLoading = false, - profile = profile, - ) - } catch (e: Exception) { - _profileState.value = _profileState.value.copy( - isLoading = false, - error = e.message ?: "프로필을 불러오는데 실패했습니다", - ) - } - } - } - - /** - * 프로필 수정 - */ - fun updateProfile(nickname: String, profileImageUrl: String? = null) { - viewModelScope.launch { - _profileState.value = _profileState.value.copy(isLoading = true, error = null) - - try { - val request = UpdateProfileRequest(nickname = nickname, profileImageUrl = profileImageUrl) - val response = userService.updateMyPage(request) - - if (response.code == 200 && response.data != null) { - _profileState.value = _profileState.value.copy( - isLoading = false, - profile = response.data, - ) - } else { - _profileState.value = _profileState.value.copy( - isLoading = false, - error = response.message, - ) - } - } catch (e: Exception) { - _profileState.value = _profileState.value.copy( - isLoading = false, - error = e.message ?: "프로필 수정에 실패했습니다", - ) - } - } - } - - /** - * 내가 작성한 레시피 목록 조회 - */ - fun loadMyRecipes() { - viewModelScope.launch { - _profileState.value = _profileState.value.copy(isLoading = true, error = null) - - try { - val response = userService.getMyRecipes() - - if (response.code == 200 && response.data != null) { - _profileState.value = _profileState.value.copy( - isLoading = false, - myRecipes = response.data, - ) - } else { - _profileState.value = _profileState.value.copy( - isLoading = false, - error = response.message, - ) - } - } catch (e: Exception) { - _profileState.value = _profileState.value.copy( - isLoading = false, - error = e.message ?: "레시피 목록을 불러오는데 실패했습니다", - ) - } - } - } - } - -// ========== UI State 데이터 클래스 ========== - -data class LoginExampleState( - val isLoading: Boolean = false, - val isSuccess: Boolean = false, - val error: String? = null, - val userProfile: com.example.hackathon.data.dto.response.UserProfileResponse? = null, -) - -data class RecipeListState( - val isLoading: Boolean = false, - val error: String? = null, - val recipes: List = emptyList(), -) - -data class ProfileState( - val isLoading: Boolean = false, - val error: String? = null, - val profile: com.example.hackathon.data.dto.response.UserProfileResponse? = null, - val myRecipes: List = emptyList(), -) diff --git a/app/src/main/java/com/example/hackathon/di/RepositoryModule.kt b/core/data/src/main/java/com/example/hackathon/core/data/di/RepositoryModule.kt similarity index 55% rename from app/src/main/java/com/example/hackathon/di/RepositoryModule.kt rename to core/data/src/main/java/com/example/hackathon/core/data/di/RepositoryModule.kt index c0a3523..a79879c 100644 --- a/app/src/main/java/com/example/hackathon/di/RepositoryModule.kt +++ b/core/data/src/main/java/com/example/hackathon/core/data/di/RepositoryModule.kt @@ -1,13 +1,13 @@ -package com.example.hackathon.di +package com.example.hackathon.core.data.di -import com.example.hackathon.data.repositoryimpl.AuthRepositoryImpl -import com.example.hackathon.data.repositoryimpl.CombinationRepositoryImpl -import com.example.hackathon.data.repositoryimpl.RecipeRepositoryImpl -import com.example.hackathon.data.repositoryimpl.UserRepositoryImpl -import com.example.hackathon.domain.repository.AuthRepository -import com.example.hackathon.domain.repository.CombinationRepository -import com.example.hackathon.domain.repository.RecipeRepository -import com.example.hackathon.domain.repository.UserRepository +import com.example.hackathon.core.data.repositoryimpl.AuthRepositoryImpl +import com.example.hackathon.core.data.repositoryimpl.CombinationRepositoryImpl +import com.example.hackathon.core.data.repositoryimpl.RecipeRepositoryImpl +import com.example.hackathon.core.data.repositoryimpl.UserRepositoryImpl +import com.example.hackathon.core.domain.repository.AuthRepository +import com.example.hackathon.core.domain.repository.CombinationRepository +import com.example.hackathon.core.domain.repository.RecipeRepository +import com.example.hackathon.core.domain.repository.UserRepository import dagger.Binds import dagger.Module import dagger.hilt.InstallIn diff --git a/app/src/main/java/com/example/hackathon/data/mapper/CombinationMapper.kt b/core/data/src/main/java/com/example/hackathon/core/data/mapper/CombinationMapper.kt similarity index 60% rename from app/src/main/java/com/example/hackathon/data/mapper/CombinationMapper.kt rename to core/data/src/main/java/com/example/hackathon/core/data/mapper/CombinationMapper.kt index 02e2e90..f4f7094 100644 --- a/app/src/main/java/com/example/hackathon/data/mapper/CombinationMapper.kt +++ b/core/data/src/main/java/com/example/hackathon/core/data/mapper/CombinationMapper.kt @@ -1,11 +1,11 @@ -package com.example.hackathon.data.mapper +package com.example.hackathon.core.data.mapper -import com.example.hackathon.data.dto.request.CreateCombinationRequest -import com.example.hackathon.data.dto.response.CombinationResponse -import com.example.hackathon.data.dto.response.UserResponse -import com.example.hackathon.domain.entity.Category -import com.example.hackathon.domain.entity.Combination -import com.example.hackathon.domain.entity.User +import com.example.hackathon.core.network.dto.request.CreateCombinationRequest +import com.example.hackathon.core.network.dto.response.CombinationResponse +import com.example.hackathon.core.network.dto.response.UserResponse +import com.example.hackathon.core.model.Category +import com.example.hackathon.core.model.Combination +import com.example.hackathon.core.model.User fun CombinationResponse.toEntity(): Combination { return Combination( diff --git a/app/src/main/java/com/example/hackathon/data/mapper/RecipeMapper.kt b/core/data/src/main/java/com/example/hackathon/core/data/mapper/RecipeMapper.kt similarity index 72% rename from app/src/main/java/com/example/hackathon/data/mapper/RecipeMapper.kt rename to core/data/src/main/java/com/example/hackathon/core/data/mapper/RecipeMapper.kt index 9e11025..ec94ef0 100644 --- a/app/src/main/java/com/example/hackathon/data/mapper/RecipeMapper.kt +++ b/core/data/src/main/java/com/example/hackathon/core/data/mapper/RecipeMapper.kt @@ -1,20 +1,20 @@ -package com.example.hackathon.data.mapper +package com.example.hackathon.core.data.mapper -import com.example.hackathon.data.dto.response.AuthorDto -import com.example.hackathon.data.dto.response.DetailResponse -import com.example.hackathon.data.dto.response.IngredientDto -import com.example.hackathon.data.dto.response.PostPreviewDto -import com.example.hackathon.data.dto.response.RecipeDetailResponse -import com.example.hackathon.data.dto.response.RecipeRankingItemResponse -import com.example.hackathon.data.dto.response.RecipeRankingResponse -import com.example.hackathon.data.dto.response.UserResponse -import com.example.hackathon.data.mapper.toEntity -import com.example.hackathon.domain.entity.Ingredient -import com.example.hackathon.domain.entity.RecipeDetail -import com.example.hackathon.domain.entity.RecipeRanking -import com.example.hackathon.domain.entity.RecipeRankingItem -import com.example.hackathon.domain.entity.Stats -import com.example.hackathon.domain.entity.UserInteraction +import com.example.hackathon.core.network.dto.response.AuthorDto +import com.example.hackathon.core.network.dto.response.DetailResponse +import com.example.hackathon.core.network.dto.response.IngredientDto +import com.example.hackathon.core.network.dto.response.PostPreviewDto +import com.example.hackathon.core.network.dto.response.RecipeDetailResponse +import com.example.hackathon.core.network.dto.response.RecipeRankingItemResponse +import com.example.hackathon.core.network.dto.response.RecipeRankingResponse +import com.example.hackathon.core.network.dto.response.UserResponse +import com.example.hackathon.core.data.mapper.toEntity +import com.example.hackathon.core.model.Ingredient +import com.example.hackathon.core.model.RecipeDetail +import com.example.hackathon.core.model.RecipeRanking +import com.example.hackathon.core.model.RecipeRankingItem +import com.example.hackathon.core.model.Stats +import com.example.hackathon.core.model.UserInteraction // ========== 기존 DTO Mapper (하위 호환성) ========== @@ -123,8 +123,8 @@ fun DetailResponse.toDomain(): RecipeDetail = /** * AuthorDto를 User로 변환 */ -fun AuthorDto.toEntity(): com.example.hackathon.domain.entity.User = - com.example.hackathon.domain.entity.User( +fun AuthorDto.toEntity(): com.example.hackathon.core.model.User = + com.example.hackathon.core.model.User( id = userId, nickname = nickname, profileImageUrl = profileImageUrl, diff --git a/app/src/main/java/com/example/hackathon/data/repositoryimpl/AuthRepositoryImpl.kt b/core/data/src/main/java/com/example/hackathon/core/data/repositoryimpl/AuthRepositoryImpl.kt similarity index 86% rename from app/src/main/java/com/example/hackathon/data/repositoryimpl/AuthRepositoryImpl.kt rename to core/data/src/main/java/com/example/hackathon/core/data/repositoryimpl/AuthRepositoryImpl.kt index feeccf7..e7a0e75 100644 --- a/app/src/main/java/com/example/hackathon/data/repositoryimpl/AuthRepositoryImpl.kt +++ b/core/data/src/main/java/com/example/hackathon/core/data/repositoryimpl/AuthRepositoryImpl.kt @@ -1,20 +1,20 @@ -package com.example.hackathon.data.repositoryimpl +package com.example.hackathon.core.data.repositoryimpl -import com.example.hackathon.BuildConfig -import com.example.hackathon.data.dto.request.LoginRequest -import com.example.hackathon.data.dto.request.LogoutRequest -import com.example.hackathon.data.dto.request.ReissueRequest -import com.example.hackathon.data.dto.request.SignupRequest -import com.example.hackathon.data.local.DummyData -import com.example.hackathon.data.local.TokenManager -import com.example.hackathon.data.mapper.toEntity -import com.example.hackathon.data.service.AuthService -import com.example.hackathon.data.service.UserService -import com.example.hackathon.domain.entity.User -import com.example.hackathon.domain.repository.AuthRepository -import com.example.hackathon.domain.repository.LoginResult -import com.example.hackathon.domain.repository.ReissueResult -import com.example.hackathon.domain.repository.SignupResult +import com.example.hackathon.core.data.BuildConfig +import com.example.hackathon.core.network.dto.request.LoginRequest +import com.example.hackathon.core.network.dto.request.LogoutRequest +import com.example.hackathon.core.network.dto.request.ReissueRequest +import com.example.hackathon.core.network.dto.request.SignupRequest +import com.example.hackathon.core.model.DummyData +import com.example.hackathon.core.datastore.TokenManager +import com.example.hackathon.core.data.mapper.toEntity +import com.example.hackathon.core.network.service.AuthService +import com.example.hackathon.core.network.service.UserService +import com.example.hackathon.core.model.User +import com.example.hackathon.core.domain.repository.AuthRepository +import com.example.hackathon.core.domain.repository.LoginResult +import com.example.hackathon.core.domain.repository.ReissueResult +import com.example.hackathon.core.domain.repository.SignupResult import kotlinx.coroutines.delay import javax.inject.Inject @@ -110,8 +110,11 @@ class AuthRepositoryImpl tokenManager.saveTokens(loginResponse.accessToken, loginResponse.refreshToken) // 사용자 정보 가져오기 (로그인 응답에 user가 없으면 /users/mypage로 조회) - val user = if (loginResponse.user != null) { - loginResponse.user.toEntity() + // loginResponse.user 는 다른 모듈(core:network)의 nullable public property 라 + // smart cast 불가 → 지역변수로 스냅샷 후 분기. + val responseUser = loginResponse.user + val user = if (responseUser != null) { + responseUser.toEntity() } else { // 사용자 정보를 별도로 조회 // 서버 응답: {"nickname":"...","myRecipeCount":0} (BaseResponse 래퍼 없음) diff --git a/app/src/main/java/com/example/hackathon/data/repositoryimpl/CombinationRepositoryImpl.kt b/core/data/src/main/java/com/example/hackathon/core/data/repositoryimpl/CombinationRepositoryImpl.kt similarity index 95% rename from app/src/main/java/com/example/hackathon/data/repositoryimpl/CombinationRepositoryImpl.kt rename to core/data/src/main/java/com/example/hackathon/core/data/repositoryimpl/CombinationRepositoryImpl.kt index 18774cd..891960d 100644 --- a/app/src/main/java/com/example/hackathon/data/repositoryimpl/CombinationRepositoryImpl.kt +++ b/core/data/src/main/java/com/example/hackathon/core/data/repositoryimpl/CombinationRepositoryImpl.kt @@ -1,26 +1,26 @@ -package com.example.hackathon.data.repositoryimpl +package com.example.hackathon.core.data.repositoryimpl import android.content.Context import android.net.Uri -import com.example.hackathon.BuildConfig -import com.example.hackathon.data.dto.request.CreateCombinationRequest -import com.example.hackathon.data.dto.request.RegisterRequest -import com.example.hackathon.data.dto.request.UpdateCombinationRequest -import com.example.hackathon.data.dto.request.UpdateRegisterRequest -import com.example.hackathon.data.dto.response.IngredientDto -import com.example.hackathon.data.local.DummyData -import com.example.hackathon.data.mapper.toEntity -import com.example.hackathon.data.local.TokenManager -import com.example.hackathon.data.service.CombinationService -import com.example.hackathon.data.service.RecipeService -import com.example.hackathon.data.service.UserService -import com.example.hackathon.domain.entity.Category -import com.example.hackathon.domain.entity.Combination -import com.example.hackathon.domain.entity.Ingredient -import com.example.hackathon.domain.entity.RecipeDetail -import com.example.hackathon.domain.entity.Stats -import com.example.hackathon.domain.entity.UserInteraction -import com.example.hackathon.domain.repository.CombinationRepository +import com.example.hackathon.core.data.BuildConfig +import com.example.hackathon.core.network.dto.request.CreateCombinationRequest +import com.example.hackathon.core.network.dto.request.RegisterRequest +import com.example.hackathon.core.network.dto.request.UpdateCombinationRequest +import com.example.hackathon.core.network.dto.request.UpdateRegisterRequest +import com.example.hackathon.core.network.dto.response.IngredientDto +import com.example.hackathon.core.model.DummyData +import com.example.hackathon.core.data.mapper.toEntity +import com.example.hackathon.core.datastore.TokenManager +import com.example.hackathon.core.network.service.CombinationService +import com.example.hackathon.core.network.service.RecipeService +import com.example.hackathon.core.network.service.UserService +import com.example.hackathon.core.model.Category +import com.example.hackathon.core.model.Combination +import com.example.hackathon.core.model.Ingredient +import com.example.hackathon.core.model.RecipeDetail +import com.example.hackathon.core.model.Stats +import com.example.hackathon.core.model.UserInteraction +import com.example.hackathon.core.domain.repository.CombinationRepository import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.delay import java.util.Base64 diff --git a/app/src/main/java/com/example/hackathon/data/repositoryimpl/RecipeRepositoryImpl.kt b/core/data/src/main/java/com/example/hackathon/core/data/repositoryimpl/RecipeRepositoryImpl.kt similarity index 93% rename from app/src/main/java/com/example/hackathon/data/repositoryimpl/RecipeRepositoryImpl.kt rename to core/data/src/main/java/com/example/hackathon/core/data/repositoryimpl/RecipeRepositoryImpl.kt index e8f513f..f322e57 100644 --- a/app/src/main/java/com/example/hackathon/data/repositoryimpl/RecipeRepositoryImpl.kt +++ b/core/data/src/main/java/com/example/hackathon/core/data/repositoryimpl/RecipeRepositoryImpl.kt @@ -1,11 +1,11 @@ -package com.example.hackathon.data.repositoryimpl +package com.example.hackathon.core.data.repositoryimpl -import com.example.hackathon.BuildConfig -import com.example.hackathon.data.mapper.toDomain -import com.example.hackathon.data.service.RecipeService -import com.example.hackathon.domain.entity.RecipeDetail -import com.example.hackathon.domain.entity.RecipeRanking -import com.example.hackathon.domain.repository.RecipeRepository +import com.example.hackathon.core.data.BuildConfig +import com.example.hackathon.core.data.mapper.toDomain +import com.example.hackathon.core.network.service.RecipeService +import com.example.hackathon.core.model.RecipeDetail +import com.example.hackathon.core.model.RecipeRanking +import com.example.hackathon.core.domain.repository.RecipeRepository import kotlinx.coroutines.delay import retrofit2.HttpException import java.io.IOException diff --git a/app/src/main/java/com/example/hackathon/data/repositoryimpl/UserRepositoryImpl.kt b/core/data/src/main/java/com/example/hackathon/core/data/repositoryimpl/UserRepositoryImpl.kt similarity index 87% rename from app/src/main/java/com/example/hackathon/data/repositoryimpl/UserRepositoryImpl.kt rename to core/data/src/main/java/com/example/hackathon/core/data/repositoryimpl/UserRepositoryImpl.kt index f4c1d8e..952a396 100644 --- a/app/src/main/java/com/example/hackathon/data/repositoryimpl/UserRepositoryImpl.kt +++ b/core/data/src/main/java/com/example/hackathon/core/data/repositoryimpl/UserRepositoryImpl.kt @@ -1,12 +1,12 @@ -package com.example.hackathon.data.repositoryimpl +package com.example.hackathon.core.data.repositoryimpl -import com.example.hackathon.BuildConfig -import com.example.hackathon.data.local.DummyData -import com.example.hackathon.data.local.TokenManager -import com.example.hackathon.data.mapper.toEntity -import com.example.hackathon.data.service.UserService -import com.example.hackathon.domain.entity.User -import com.example.hackathon.domain.repository.UserRepository +import com.example.hackathon.core.data.BuildConfig +import com.example.hackathon.core.model.DummyData +import com.example.hackathon.core.datastore.TokenManager +import com.example.hackathon.core.data.mapper.toEntity +import com.example.hackathon.core.network.service.UserService +import com.example.hackathon.core.model.User +import com.example.hackathon.core.domain.repository.UserRepository import javax.inject.Inject class UserRepositoryImpl @@ -66,7 +66,7 @@ class UserRepositoryImpl Result.success(updatedUser) } else { // 실제 API 호출 - val request = com.example.hackathon.data.dto.request.UpdateProfileRequest( + val request = com.example.hackathon.core.network.dto.request.UpdateProfileRequest( nickname = nickname, profileImageUrl = profileImageUrl, ) diff --git a/app/src/main/java/com/example/hackathon/data/local/TokenManager.kt b/core/datastore/src/main/java/com/example/hackathon/core/datastore/TokenManager.kt similarity index 98% rename from app/src/main/java/com/example/hackathon/data/local/TokenManager.kt rename to core/datastore/src/main/java/com/example/hackathon/core/datastore/TokenManager.kt index 305defc..21fbbc0 100644 --- a/app/src/main/java/com/example/hackathon/data/local/TokenManager.kt +++ b/core/datastore/src/main/java/com/example/hackathon/core/datastore/TokenManager.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.data.local +package com.example.hackathon.core.datastore import android.content.Context import androidx.datastore.core.DataStore diff --git a/app/src/main/java/com/example/hackathon/core/component/CategoryChip.kt b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/CategoryChip.kt similarity index 94% rename from app/src/main/java/com/example/hackathon/core/component/CategoryChip.kt rename to core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/CategoryChip.kt index 9ef663b..2f9d145 100644 --- a/app/src/main/java/com/example/hackathon/core/component/CategoryChip.kt +++ b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/CategoryChip.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.core.component +package com.example.hackathon.core.designsystem.component import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape @@ -7,7 +7,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.example.hackathon.domain.entity.Category +import com.example.hackathon.core.model.Category @Composable fun CategoryChip( diff --git a/app/src/main/java/com/example/hackathon/core/component/CombinationCard.kt b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/CombinationCard.kt similarity index 96% rename from app/src/main/java/com/example/hackathon/core/component/CombinationCard.kt rename to core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/CombinationCard.kt index e2c9e87..32565db 100644 --- a/app/src/main/java/com/example/hackathon/core/component/CombinationCard.kt +++ b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/CombinationCard.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.core.component +package com.example.hackathon.core.designsystem.component import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -30,13 +30,13 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage -import com.example.hackathon.domain.entity.Category -import com.example.hackathon.domain.entity.Combination -import com.example.hackathon.domain.entity.User -import com.example.hackathon.ui.theme.Gray50 -import com.example.hackathon.ui.theme.Gray700 -import com.example.hackathon.ui.theme.HackathonTheme -import com.example.hackathon.ui.theme.Primary +import com.example.hackathon.core.model.Category +import com.example.hackathon.core.model.Combination +import com.example.hackathon.core.model.User +import com.example.hackathon.core.designsystem.theme.Gray50 +import com.example.hackathon.core.designsystem.theme.Gray700 +import com.example.hackathon.core.designsystem.theme.HackathonTheme +import com.example.hackathon.core.designsystem.theme.Primary private const val SHADOW_SIZE = 8 diff --git a/app/src/main/java/com/example/hackathon/core/component/IngredientRow.kt b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/IngredientRow.kt similarity index 89% rename from app/src/main/java/com/example/hackathon/core/component/IngredientRow.kt rename to core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/IngredientRow.kt index fc7fa4c..0540cca 100644 --- a/app/src/main/java/com/example/hackathon/core/component/IngredientRow.kt +++ b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/IngredientRow.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.core.component +package com.example.hackathon.core.designsystem.component import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -9,7 +9,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.example.hackathon.ui.theme.HackathonTheme +import com.example.hackathon.core.designsystem.theme.HackathonTheme @Composable fun IngredientRow( diff --git a/app/src/main/java/com/example/hackathon/core/component/SearchBar.kt b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/SearchBar.kt similarity index 95% rename from app/src/main/java/com/example/hackathon/core/component/SearchBar.kt rename to core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/SearchBar.kt index 75d72d5..fe29fe9 100644 --- a/app/src/main/java/com/example/hackathon/core/component/SearchBar.kt +++ b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/SearchBar.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.core.component +package com.example.hackathon.core.designsystem.component import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape diff --git a/app/src/main/java/com/example/hackathon/presentation/component/TagChip.kt b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/TagChip.kt similarity index 94% rename from app/src/main/java/com/example/hackathon/presentation/component/TagChip.kt rename to core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/TagChip.kt index 833d11c..6006f33 100644 --- a/app/src/main/java/com/example/hackathon/presentation/component/TagChip.kt +++ b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/TagChip.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.presentation.component +package com.example.hackathon.core.designsystem.component import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -18,7 +18,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.example.hackathon.ui.theme.Primary +import com.example.hackathon.core.designsystem.theme.Primary @Composable fun TagChip( diff --git a/app/src/main/java/com/example/hackathon/core/component/TopAppLogoBar.kt b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/TopAppLogoBar.kt similarity index 88% rename from app/src/main/java/com/example/hackathon/core/component/TopAppLogoBar.kt rename to core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/TopAppLogoBar.kt index 258bb52..f4a522d 100644 --- a/app/src/main/java/com/example/hackathon/core/component/TopAppLogoBar.kt +++ b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/TopAppLogoBar.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.core.component +package com.example.hackathon.core.designsystem.component import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -15,8 +15,8 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.example.hackathon.R -import com.example.hackathon.ui.theme.HackathonTheme +import com.example.hackathon.core.designsystem.R +import com.example.hackathon.core.designsystem.theme.HackathonTheme @Composable fun TopAppLogoBar(modifier: Modifier = Modifier) { diff --git a/app/src/main/java/com/example/hackathon/core/component/tag/TagConvenience.kt b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/tag/TagConvenience.kt similarity index 70% rename from app/src/main/java/com/example/hackathon/core/component/tag/TagConvenience.kt rename to core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/tag/TagConvenience.kt index 238da11..c04aef5 100644 --- a/app/src/main/java/com/example/hackathon/core/component/tag/TagConvenience.kt +++ b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/tag/TagConvenience.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.core.component.tag +package com.example.hackathon.core.designsystem.component.tag import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier diff --git a/app/src/main/java/com/example/hackathon/core/component/tag/TagEtc.kt b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/tag/TagEtc.kt similarity index 69% rename from app/src/main/java/com/example/hackathon/core/component/tag/TagEtc.kt rename to core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/tag/TagEtc.kt index d92bc05..38da7b5 100644 --- a/app/src/main/java/com/example/hackathon/core/component/tag/TagEtc.kt +++ b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/tag/TagEtc.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.core.component.tag +package com.example.hackathon.core.designsystem.component.tag import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier diff --git a/app/src/main/java/com/example/hackathon/core/component/tag/TagHaidirao.kt b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/tag/TagHaidirao.kt similarity index 69% rename from app/src/main/java/com/example/hackathon/core/component/tag/TagHaidirao.kt rename to core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/tag/TagHaidirao.kt index 29859d9..55eda1f 100644 --- a/app/src/main/java/com/example/hackathon/core/component/tag/TagHaidirao.kt +++ b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/tag/TagHaidirao.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.core.component.tag +package com.example.hackathon.core.designsystem.component.tag import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier diff --git a/app/src/main/java/com/example/hackathon/core/component/tag/TagSubway.kt b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/tag/TagSubway.kt similarity index 69% rename from app/src/main/java/com/example/hackathon/core/component/tag/TagSubway.kt rename to core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/tag/TagSubway.kt index 605a8a7..451d766 100644 --- a/app/src/main/java/com/example/hackathon/core/component/tag/TagSubway.kt +++ b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/tag/TagSubway.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.core.component.tag +package com.example.hackathon.core.designsystem.component.tag import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier diff --git a/app/src/main/java/com/example/hackathon/ui/theme/Color.kt b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/theme/Color.kt similarity index 97% rename from app/src/main/java/com/example/hackathon/ui/theme/Color.kt rename to core/designsystem/src/main/java/com/example/hackathon/core/designsystem/theme/Color.kt index cfb0266..12fe2ec 100644 --- a/app/src/main/java/com/example/hackathon/ui/theme/Color.kt +++ b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/theme/Color.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.ui.theme +package com.example.hackathon.core.designsystem.theme import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.graphics.Color diff --git a/app/src/main/java/com/example/hackathon/ui/theme/Theme.kt b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/theme/Theme.kt similarity index 95% rename from app/src/main/java/com/example/hackathon/ui/theme/Theme.kt rename to core/designsystem/src/main/java/com/example/hackathon/core/designsystem/theme/Theme.kt index d7c82ba..56afeea 100644 --- a/app/src/main/java/com/example/hackathon/ui/theme/Theme.kt +++ b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/theme/Theme.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.ui.theme +package com.example.hackathon.core.designsystem.theme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.lightColorScheme diff --git a/app/src/main/java/com/example/hackathon/ui/theme/Type.kt b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/theme/Type.kt similarity index 97% rename from app/src/main/java/com/example/hackathon/ui/theme/Type.kt rename to core/designsystem/src/main/java/com/example/hackathon/core/designsystem/theme/Type.kt index 64e81ed..75d1ea2 100644 --- a/app/src/main/java/com/example/hackathon/ui/theme/Type.kt +++ b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/theme/Type.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.ui.theme +package com.example.hackathon.core.designsystem.theme import androidx.compose.material3.Typography import androidx.compose.runtime.staticCompositionLocalOf @@ -7,7 +7,7 @@ import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp -import com.example.hackathon.R +import com.example.hackathon.core.designsystem.R // Figma Design System Typography // TODO: Pretendard 폰트 파일 추가 후 FontFamily 적용 diff --git a/app/src/main/java/com/example/hackathon/core/util/ModifierExt.kt b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/util/ModifierExt.kt similarity index 98% rename from app/src/main/java/com/example/hackathon/core/util/ModifierExt.kt rename to core/designsystem/src/main/java/com/example/hackathon/core/designsystem/util/ModifierExt.kt index c345d1b..30a27df 100644 --- a/app/src/main/java/com/example/hackathon/core/util/ModifierExt.kt +++ b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/util/ModifierExt.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.core.util +package com.example.hackathon.core.designsystem.util import android.R.attr.layoutDirection import android.graphics.BlurMaskFilter diff --git a/app/src/main/res/drawable/ic_add.xml b/core/designsystem/src/main/res/drawable/ic_add.xml similarity index 100% rename from app/src/main/res/drawable/ic_add.xml rename to core/designsystem/src/main/res/drawable/ic_add.xml diff --git a/app/src/main/res/drawable/ic_camera.xml b/core/designsystem/src/main/res/drawable/ic_camera.xml similarity index 100% rename from app/src/main/res/drawable/ic_camera.xml rename to core/designsystem/src/main/res/drawable/ic_camera.xml diff --git a/app/src/main/res/drawable/ic_home.xml b/core/designsystem/src/main/res/drawable/ic_home.xml similarity index 100% rename from app/src/main/res/drawable/ic_home.xml rename to core/designsystem/src/main/res/drawable/ic_home.xml diff --git a/app/src/main/res/drawable/ic_hot.png b/core/designsystem/src/main/res/drawable/ic_hot.png similarity index 100% rename from app/src/main/res/drawable/ic_hot.png rename to core/designsystem/src/main/res/drawable/ic_hot.png diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/core/designsystem/src/main/res/drawable/ic_launcher_background.xml similarity index 100% rename from app/src/main/res/drawable/ic_launcher_background.xml rename to core/designsystem/src/main/res/drawable/ic_launcher_background.xml diff --git a/app/src/main/res/drawable/ic_logo.png b/core/designsystem/src/main/res/drawable/ic_logo.png similarity index 100% rename from app/src/main/res/drawable/ic_logo.png rename to core/designsystem/src/main/res/drawable/ic_logo.png diff --git a/app/src/main/res/drawable/ic_logo_rec.png b/core/designsystem/src/main/res/drawable/ic_logo_rec.png similarity index 100% rename from app/src/main/res/drawable/ic_logo_rec.png rename to core/designsystem/src/main/res/drawable/ic_logo_rec.png diff --git a/app/src/main/res/drawable/ic_my.xml b/core/designsystem/src/main/res/drawable/ic_my.xml similarity index 100% rename from app/src/main/res/drawable/ic_my.xml rename to core/designsystem/src/main/res/drawable/ic_my.xml diff --git a/app/src/main/res/drawable/ic_search.xml b/core/designsystem/src/main/res/drawable/ic_search.xml similarity index 100% rename from app/src/main/res/drawable/ic_search.xml rename to core/designsystem/src/main/res/drawable/ic_search.xml diff --git a/app/src/main/res/drawable/ic_splash_inset.png b/core/designsystem/src/main/res/drawable/ic_splash_inset.png similarity index 100% rename from app/src/main/res/drawable/ic_splash_inset.png rename to core/designsystem/src/main/res/drawable/ic_splash_inset.png diff --git a/app/src/main/res/font/pretendard_bold.ttf b/core/designsystem/src/main/res/font/pretendard_bold.ttf similarity index 100% rename from app/src/main/res/font/pretendard_bold.ttf rename to core/designsystem/src/main/res/font/pretendard_bold.ttf diff --git a/app/src/main/res/font/pretendard_medium.ttf b/core/designsystem/src/main/res/font/pretendard_medium.ttf similarity index 100% rename from app/src/main/res/font/pretendard_medium.ttf rename to core/designsystem/src/main/res/font/pretendard_medium.ttf diff --git a/app/src/main/res/font/pretendard_semibold.ttf b/core/designsystem/src/main/res/font/pretendard_semibold.ttf similarity index 100% rename from app/src/main/res/font/pretendard_semibold.ttf rename to core/designsystem/src/main/res/font/pretendard_semibold.ttf diff --git a/app/src/main/java/com/example/hackathon/domain/repository/AuthRepository.kt b/core/domain/src/main/java/com/example/hackathon/core/domain/repository/AuthRepository.kt similarity index 88% rename from app/src/main/java/com/example/hackathon/domain/repository/AuthRepository.kt rename to core/domain/src/main/java/com/example/hackathon/core/domain/repository/AuthRepository.kt index 17ee5d8..22bae01 100644 --- a/app/src/main/java/com/example/hackathon/domain/repository/AuthRepository.kt +++ b/core/domain/src/main/java/com/example/hackathon/core/domain/repository/AuthRepository.kt @@ -1,6 +1,6 @@ -package com.example.hackathon.domain.repository +package com.example.hackathon.core.domain.repository -import com.example.hackathon.domain.entity.User +import com.example.hackathon.core.model.User interface AuthRepository { // 1. 회원가입 diff --git a/app/src/main/java/com/example/hackathon/domain/repository/CombinationRepository.kt b/core/domain/src/main/java/com/example/hackathon/core/domain/repository/CombinationRepository.kt similarity index 85% rename from app/src/main/java/com/example/hackathon/domain/repository/CombinationRepository.kt rename to core/domain/src/main/java/com/example/hackathon/core/domain/repository/CombinationRepository.kt index be14c7e..d8be919 100644 --- a/app/src/main/java/com/example/hackathon/domain/repository/CombinationRepository.kt +++ b/core/domain/src/main/java/com/example/hackathon/core/domain/repository/CombinationRepository.kt @@ -1,8 +1,8 @@ -package com.example.hackathon.domain.repository +package com.example.hackathon.core.domain.repository -import com.example.hackathon.domain.entity.Category -import com.example.hackathon.domain.entity.Combination -import com.example.hackathon.domain.entity.RecipeDetail +import com.example.hackathon.core.model.Category +import com.example.hackathon.core.model.Combination +import com.example.hackathon.core.model.RecipeDetail interface CombinationRepository { suspend fun getCombinations(category: Category? = null, page: Int = 1, pageSize: Int = 10): Result> diff --git a/app/src/main/java/com/example/hackathon/domain/repository/RecipeRepository.kt b/core/domain/src/main/java/com/example/hackathon/core/domain/repository/RecipeRepository.kt similarity index 80% rename from app/src/main/java/com/example/hackathon/domain/repository/RecipeRepository.kt rename to core/domain/src/main/java/com/example/hackathon/core/domain/repository/RecipeRepository.kt index 6c31dd1..a94dc31 100644 --- a/app/src/main/java/com/example/hackathon/domain/repository/RecipeRepository.kt +++ b/core/domain/src/main/java/com/example/hackathon/core/domain/repository/RecipeRepository.kt @@ -1,7 +1,7 @@ -package com.example.hackathon.domain.repository +package com.example.hackathon.core.domain.repository -import com.example.hackathon.domain.entity.RecipeDetail -import com.example.hackathon.domain.entity.RecipeRanking +import com.example.hackathon.core.model.RecipeDetail +import com.example.hackathon.core.model.RecipeRanking interface RecipeRepository { // 1. 전체 랭킹 조회(홈화면) diff --git a/app/src/main/java/com/example/hackathon/domain/repository/UserRepository.kt b/core/domain/src/main/java/com/example/hackathon/core/domain/repository/UserRepository.kt similarity index 66% rename from app/src/main/java/com/example/hackathon/domain/repository/UserRepository.kt rename to core/domain/src/main/java/com/example/hackathon/core/domain/repository/UserRepository.kt index 56b5dd9..c5c15ca 100644 --- a/app/src/main/java/com/example/hackathon/domain/repository/UserRepository.kt +++ b/core/domain/src/main/java/com/example/hackathon/core/domain/repository/UserRepository.kt @@ -1,6 +1,6 @@ -package com.example.hackathon.domain.repository +package com.example.hackathon.core.domain.repository -import com.example.hackathon.domain.entity.User +import com.example.hackathon.core.model.User interface UserRepository { suspend fun getProfile(): Result diff --git a/app/src/main/java/com/example/hackathon/domain/entity/Category.kt b/core/model/src/main/java/com/example/hackathon/core/model/Category.kt similarity index 78% rename from app/src/main/java/com/example/hackathon/domain/entity/Category.kt rename to core/model/src/main/java/com/example/hackathon/core/model/Category.kt index c87885f..28e9850 100644 --- a/app/src/main/java/com/example/hackathon/domain/entity/Category.kt +++ b/core/model/src/main/java/com/example/hackathon/core/model/Category.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.domain.entity +package com.example.hackathon.core.model enum class Category(val displayName: String) { ALL("#전체"), diff --git a/app/src/main/java/com/example/hackathon/domain/entity/Combination.kt b/core/model/src/main/java/com/example/hackathon/core/model/Combination.kt similarity index 90% rename from app/src/main/java/com/example/hackathon/domain/entity/Combination.kt rename to core/model/src/main/java/com/example/hackathon/core/model/Combination.kt index b8e9c11..ae99968 100644 --- a/app/src/main/java/com/example/hackathon/domain/entity/Combination.kt +++ b/core/model/src/main/java/com/example/hackathon/core/model/Combination.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.domain.entity +package com.example.hackathon.core.model data class Combination( val id: String, diff --git a/app/src/main/java/com/example/hackathon/data/local/DummyData.kt b/core/model/src/main/java/com/example/hackathon/core/model/DummyData.kt similarity index 97% rename from app/src/main/java/com/example/hackathon/data/local/DummyData.kt rename to core/model/src/main/java/com/example/hackathon/core/model/DummyData.kt index bd663bf..d5f3508 100644 --- a/app/src/main/java/com/example/hackathon/data/local/DummyData.kt +++ b/core/model/src/main/java/com/example/hackathon/core/model/DummyData.kt @@ -1,12 +1,12 @@ -package com.example.hackathon.data.local +package com.example.hackathon.core.model -import com.example.hackathon.domain.entity.Category -import com.example.hackathon.domain.entity.Combination -import com.example.hackathon.domain.entity.Ingredient -import com.example.hackathon.domain.entity.RecipeDetail -import com.example.hackathon.domain.entity.Stats -import com.example.hackathon.domain.entity.User -import com.example.hackathon.domain.entity.UserInteraction +import com.example.hackathon.core.model.Category +import com.example.hackathon.core.model.Combination +import com.example.hackathon.core.model.Ingredient +import com.example.hackathon.core.model.RecipeDetail +import com.example.hackathon.core.model.Stats +import com.example.hackathon.core.model.User +import com.example.hackathon.core.model.UserInteraction object DummyData { val dummyUser = diff --git a/app/src/main/java/com/example/hackathon/domain/entity/RecipeDetail.kt b/core/model/src/main/java/com/example/hackathon/core/model/RecipeDetail.kt similarity index 92% rename from app/src/main/java/com/example/hackathon/domain/entity/RecipeDetail.kt rename to core/model/src/main/java/com/example/hackathon/core/model/RecipeDetail.kt index 622e414..6cf5361 100644 --- a/app/src/main/java/com/example/hackathon/domain/entity/RecipeDetail.kt +++ b/core/model/src/main/java/com/example/hackathon/core/model/RecipeDetail.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.domain.entity +package com.example.hackathon.core.model data class RecipeDetail( val id: Long, diff --git a/app/src/main/java/com/example/hackathon/domain/entity/RecipeRanking.kt b/core/model/src/main/java/com/example/hackathon/core/model/RecipeRanking.kt similarity index 86% rename from app/src/main/java/com/example/hackathon/domain/entity/RecipeRanking.kt rename to core/model/src/main/java/com/example/hackathon/core/model/RecipeRanking.kt index e05f81e..52782a3 100644 --- a/app/src/main/java/com/example/hackathon/domain/entity/RecipeRanking.kt +++ b/core/model/src/main/java/com/example/hackathon/core/model/RecipeRanking.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.domain.entity +package com.example.hackathon.core.model data class RecipeRanking( val currentCategory: String, diff --git a/app/src/main/java/com/example/hackathon/domain/entity/User.kt b/core/model/src/main/java/com/example/hackathon/core/model/User.kt similarity index 69% rename from app/src/main/java/com/example/hackathon/domain/entity/User.kt rename to core/model/src/main/java/com/example/hackathon/core/model/User.kt index b70e1fa..796c902 100644 --- a/app/src/main/java/com/example/hackathon/domain/entity/User.kt +++ b/core/model/src/main/java/com/example/hackathon/core/model/User.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.domain.entity +package com.example.hackathon.core.model data class User( val id: String, diff --git a/app/src/main/java/com/example/hackathon/data/network/AuthInterceptor.kt b/core/network/src/main/java/com/example/hackathon/core/network/AuthInterceptor.kt similarity index 94% rename from app/src/main/java/com/example/hackathon/data/network/AuthInterceptor.kt rename to core/network/src/main/java/com/example/hackathon/core/network/AuthInterceptor.kt index 04800cc..f91abb7 100644 --- a/app/src/main/java/com/example/hackathon/data/network/AuthInterceptor.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/AuthInterceptor.kt @@ -1,8 +1,8 @@ -package com.example.hackathon.data.network +package com.example.hackathon.core.network -import com.example.hackathon.data.dto.request.ReissueRequest -import com.example.hackathon.data.local.TokenManager -import com.example.hackathon.data.service.AuthService +import com.example.hackathon.core.network.dto.request.ReissueRequest +import com.example.hackathon.core.datastore.TokenManager +import com.example.hackathon.core.network.service.AuthService import kotlinx.coroutines.runBlocking import okhttp3.Interceptor import okhttp3.Request diff --git a/app/src/main/java/com/example/hackathon/di/ApiModule.kt b/core/network/src/main/java/com/example/hackathon/core/network/di/ApiModule.kt similarity index 74% rename from app/src/main/java/com/example/hackathon/di/ApiModule.kt rename to core/network/src/main/java/com/example/hackathon/core/network/di/ApiModule.kt index d7029ee..6f10713 100644 --- a/app/src/main/java/com/example/hackathon/di/ApiModule.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/di/ApiModule.kt @@ -1,9 +1,9 @@ -package com.example.hackathon.di +package com.example.hackathon.core.network.di -import com.example.hackathon.data.service.AuthService -import com.example.hackathon.data.service.CombinationService -import com.example.hackathon.data.service.HomeService -import com.example.hackathon.data.service.UserService +import com.example.hackathon.core.network.service.AuthService +import com.example.hackathon.core.network.service.CombinationService +import com.example.hackathon.core.network.service.HomeService +import com.example.hackathon.core.network.service.UserService import dagger.Module import dagger.Provides import dagger.hilt.InstallIn diff --git a/app/src/main/java/com/example/hackathon/di/NetworkModule.kt b/core/network/src/main/java/com/example/hackathon/core/network/di/NetworkModule.kt similarity index 91% rename from app/src/main/java/com/example/hackathon/di/NetworkModule.kt rename to core/network/src/main/java/com/example/hackathon/core/network/di/NetworkModule.kt index bc17ef0..6af9cec 100644 --- a/app/src/main/java/com/example/hackathon/di/NetworkModule.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/di/NetworkModule.kt @@ -1,8 +1,8 @@ -package com.example.hackathon.di +package com.example.hackathon.core.network.di -import com.example.hackathon.BuildConfig -import com.example.hackathon.data.network.AuthInterceptor -import com.example.hackathon.data.service.RecipeService +import com.example.hackathon.core.network.BuildConfig +import com.example.hackathon.core.network.AuthInterceptor +import com.example.hackathon.core.network.service.RecipeService import dagger.Module import dagger.Provides import dagger.hilt.InstallIn diff --git a/app/src/main/java/com/example/hackathon/di/Qualifiers.kt b/core/network/src/main/java/com/example/hackathon/core/network/di/Qualifiers.kt similarity index 80% rename from app/src/main/java/com/example/hackathon/di/Qualifiers.kt rename to core/network/src/main/java/com/example/hackathon/core/network/di/Qualifiers.kt index 5131393..2d6984a 100644 --- a/app/src/main/java/com/example/hackathon/di/Qualifiers.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/di/Qualifiers.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.di +package com.example.hackathon.core.network.di import javax.inject.Qualifier diff --git a/app/src/main/java/com/example/hackathon/data/dto/request/CreateCombinationRequest.kt b/core/network/src/main/java/com/example/hackathon/core/network/dto/request/CreateCombinationRequest.kt similarity index 79% rename from app/src/main/java/com/example/hackathon/data/dto/request/CreateCombinationRequest.kt rename to core/network/src/main/java/com/example/hackathon/core/network/dto/request/CreateCombinationRequest.kt index cb541da..24c6a57 100644 --- a/app/src/main/java/com/example/hackathon/data/dto/request/CreateCombinationRequest.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/dto/request/CreateCombinationRequest.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.data.dto.request +package com.example.hackathon.core.network.dto.request import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/example/hackathon/data/dto/request/LoginRequest.kt b/core/network/src/main/java/com/example/hackathon/core/network/dto/request/LoginRequest.kt similarity index 82% rename from app/src/main/java/com/example/hackathon/data/dto/request/LoginRequest.kt rename to core/network/src/main/java/com/example/hackathon/core/network/dto/request/LoginRequest.kt index ac24d2b..f473abe 100644 --- a/app/src/main/java/com/example/hackathon/data/dto/request/LoginRequest.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/dto/request/LoginRequest.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.data.dto.request +package com.example.hackathon.core.network.dto.request import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/example/hackathon/data/dto/request/LogoutRequest.kt b/core/network/src/main/java/com/example/hackathon/core/network/dto/request/LogoutRequest.kt similarity index 67% rename from app/src/main/java/com/example/hackathon/data/dto/request/LogoutRequest.kt rename to core/network/src/main/java/com/example/hackathon/core/network/dto/request/LogoutRequest.kt index ab02399..b28e5ea 100644 --- a/app/src/main/java/com/example/hackathon/data/dto/request/LogoutRequest.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/dto/request/LogoutRequest.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.data.dto.request +package com.example.hackathon.core.network.dto.request import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/example/hackathon/data/dto/request/RegisterRequest.kt b/core/network/src/main/java/com/example/hackathon/core/network/dto/request/RegisterRequest.kt similarity index 88% rename from app/src/main/java/com/example/hackathon/data/dto/request/RegisterRequest.kt rename to core/network/src/main/java/com/example/hackathon/core/network/dto/request/RegisterRequest.kt index c8f8c0a..b65410a 100644 --- a/app/src/main/java/com/example/hackathon/data/dto/request/RegisterRequest.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/dto/request/RegisterRequest.kt @@ -1,6 +1,6 @@ -package com.example.hackathon.data.dto.request +package com.example.hackathon.core.network.dto.request -import com.example.hackathon.data.dto.response.IngredientDto +import com.example.hackathon.core.network.dto.response.IngredientDto import kotlinx.serialization.Serializable /** diff --git a/app/src/main/java/com/example/hackathon/data/dto/request/ReissueRequest.kt b/core/network/src/main/java/com/example/hackathon/core/network/dto/request/ReissueRequest.kt similarity index 88% rename from app/src/main/java/com/example/hackathon/data/dto/request/ReissueRequest.kt rename to core/network/src/main/java/com/example/hackathon/core/network/dto/request/ReissueRequest.kt index 9305638..905b78b 100644 --- a/app/src/main/java/com/example/hackathon/data/dto/request/ReissueRequest.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/dto/request/ReissueRequest.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.data.dto.request +package com.example.hackathon.core.network.dto.request import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/example/hackathon/data/dto/request/SignupRequest.kt b/core/network/src/main/java/com/example/hackathon/core/network/dto/request/SignupRequest.kt similarity index 83% rename from app/src/main/java/com/example/hackathon/data/dto/request/SignupRequest.kt rename to core/network/src/main/java/com/example/hackathon/core/network/dto/request/SignupRequest.kt index 1df7cc1..8860a98 100644 --- a/app/src/main/java/com/example/hackathon/data/dto/request/SignupRequest.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/dto/request/SignupRequest.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.data.dto.request +package com.example.hackathon.core.network.dto.request import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/example/hackathon/data/dto/request/UpdateCombinationRequest.kt b/core/network/src/main/java/com/example/hackathon/core/network/dto/request/UpdateCombinationRequest.kt similarity index 79% rename from app/src/main/java/com/example/hackathon/data/dto/request/UpdateCombinationRequest.kt rename to core/network/src/main/java/com/example/hackathon/core/network/dto/request/UpdateCombinationRequest.kt index 00d301e..ccb65a4 100644 --- a/app/src/main/java/com/example/hackathon/data/dto/request/UpdateCombinationRequest.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/dto/request/UpdateCombinationRequest.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.data.dto.request +package com.example.hackathon.core.network.dto.request import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/example/hackathon/data/dto/request/UpdateProfileRequest.kt b/core/network/src/main/java/com/example/hackathon/core/network/dto/request/UpdateProfileRequest.kt similarity index 74% rename from app/src/main/java/com/example/hackathon/data/dto/request/UpdateProfileRequest.kt rename to core/network/src/main/java/com/example/hackathon/core/network/dto/request/UpdateProfileRequest.kt index 0384c54..bd6690c 100644 --- a/app/src/main/java/com/example/hackathon/data/dto/request/UpdateProfileRequest.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/dto/request/UpdateProfileRequest.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.data.dto.request +package com.example.hackathon.core.network.dto.request import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/example/hackathon/data/dto/response/BaseResponse.kt b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/BaseResponse.kt similarity index 72% rename from app/src/main/java/com/example/hackathon/data/dto/response/BaseResponse.kt rename to core/network/src/main/java/com/example/hackathon/core/network/dto/response/BaseResponse.kt index 91e22c1..6117ea6 100644 --- a/app/src/main/java/com/example/hackathon/data/dto/response/BaseResponse.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/BaseResponse.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.data.dto.response +package com.example.hackathon.core.network.dto.response import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/example/hackathon/data/dto/response/CombinationResponse.kt b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/CombinationResponse.kt similarity index 85% rename from app/src/main/java/com/example/hackathon/data/dto/response/CombinationResponse.kt rename to core/network/src/main/java/com/example/hackathon/core/network/dto/response/CombinationResponse.kt index 8fa29f0..459d039 100644 --- a/app/src/main/java/com/example/hackathon/data/dto/response/CombinationResponse.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/CombinationResponse.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.data.dto.response +package com.example.hackathon.core.network.dto.response import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/example/hackathon/data/dto/response/DetailResponse.kt b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/DetailResponse.kt similarity index 91% rename from app/src/main/java/com/example/hackathon/data/dto/response/DetailResponse.kt rename to core/network/src/main/java/com/example/hackathon/core/network/dto/response/DetailResponse.kt index 4c82026..47c3d13 100644 --- a/app/src/main/java/com/example/hackathon/data/dto/response/DetailResponse.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/DetailResponse.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.data.dto.response +package com.example.hackathon.core.network.dto.response import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/example/hackathon/data/dto/response/LoginResponse.kt b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/LoginResponse.kt similarity index 85% rename from app/src/main/java/com/example/hackathon/data/dto/response/LoginResponse.kt rename to core/network/src/main/java/com/example/hackathon/core/network/dto/response/LoginResponse.kt index 47e2dd4..20d3388 100644 --- a/app/src/main/java/com/example/hackathon/data/dto/response/LoginResponse.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/LoginResponse.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.data.dto.response +package com.example.hackathon.core.network.dto.response import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/example/hackathon/data/dto/response/MyPageRecipeResponse.kt b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/MyPageRecipeResponse.kt similarity index 88% rename from app/src/main/java/com/example/hackathon/data/dto/response/MyPageRecipeResponse.kt rename to core/network/src/main/java/com/example/hackathon/core/network/dto/response/MyPageRecipeResponse.kt index f023f05..9387b8f 100644 --- a/app/src/main/java/com/example/hackathon/data/dto/response/MyPageRecipeResponse.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/MyPageRecipeResponse.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.data.dto.response +package com.example.hackathon.core.network.dto.response import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/example/hackathon/data/dto/response/PostPreviewDto.kt b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/PostPreviewDto.kt similarity index 90% rename from app/src/main/java/com/example/hackathon/data/dto/response/PostPreviewDto.kt rename to core/network/src/main/java/com/example/hackathon/core/network/dto/response/PostPreviewDto.kt index 9840b1e..343505d 100644 --- a/app/src/main/java/com/example/hackathon/data/dto/response/PostPreviewDto.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/PostPreviewDto.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.data.dto.response +package com.example.hackathon.core.network.dto.response import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/example/hackathon/data/dto/response/RecipeDetailResponse.kt b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/RecipeDetailResponse.kt similarity index 92% rename from app/src/main/java/com/example/hackathon/data/dto/response/RecipeDetailResponse.kt rename to core/network/src/main/java/com/example/hackathon/core/network/dto/response/RecipeDetailResponse.kt index 7e5408e..c5587a5 100644 --- a/app/src/main/java/com/example/hackathon/data/dto/response/RecipeDetailResponse.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/RecipeDetailResponse.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.data.dto.response +package com.example.hackathon.core.network.dto.response import kotlinx.serialization.Serializable @Serializable diff --git a/app/src/main/java/com/example/hackathon/data/dto/response/RecipeRankingResponse.kt b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/RecipeRankingResponse.kt similarity index 87% rename from app/src/main/java/com/example/hackathon/data/dto/response/RecipeRankingResponse.kt rename to core/network/src/main/java/com/example/hackathon/core/network/dto/response/RecipeRankingResponse.kt index ec83514..bed7c18 100644 --- a/app/src/main/java/com/example/hackathon/data/dto/response/RecipeRankingResponse.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/RecipeRankingResponse.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.data.dto.response +package com.example.hackathon.core.network.dto.response import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/example/hackathon/data/dto/response/ReissueResponse.kt b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/ReissueResponse.kt similarity index 91% rename from app/src/main/java/com/example/hackathon/data/dto/response/ReissueResponse.kt rename to core/network/src/main/java/com/example/hackathon/core/network/dto/response/ReissueResponse.kt index 14b5fae..d9d44af 100644 --- a/app/src/main/java/com/example/hackathon/data/dto/response/ReissueResponse.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/ReissueResponse.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.data.dto.response +package com.example.hackathon.core.network.dto.response import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/example/hackathon/data/dto/response/SignupResponse.kt b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/SignupResponse.kt similarity index 84% rename from app/src/main/java/com/example/hackathon/data/dto/response/SignupResponse.kt rename to core/network/src/main/java/com/example/hackathon/core/network/dto/response/SignupResponse.kt index 180f4ee..e6c8e19 100644 --- a/app/src/main/java/com/example/hackathon/data/dto/response/SignupResponse.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/SignupResponse.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.data.dto.response +package com.example.hackathon.core.network.dto.response import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/example/hackathon/data/dto/response/UserProfileResponse.kt b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/UserProfileResponse.kt similarity index 88% rename from app/src/main/java/com/example/hackathon/data/dto/response/UserProfileResponse.kt rename to core/network/src/main/java/com/example/hackathon/core/network/dto/response/UserProfileResponse.kt index 64fa50d..2fc1e69 100644 --- a/app/src/main/java/com/example/hackathon/data/dto/response/UserProfileResponse.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/UserProfileResponse.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.data.dto.response +package com.example.hackathon.core.network.dto.response import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/example/hackathon/data/dto/response/UserResponse.kt b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/UserResponse.kt similarity index 75% rename from app/src/main/java/com/example/hackathon/data/dto/response/UserResponse.kt rename to core/network/src/main/java/com/example/hackathon/core/network/dto/response/UserResponse.kt index b44cd42..e3a2537 100644 --- a/app/src/main/java/com/example/hackathon/data/dto/response/UserResponse.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/UserResponse.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.data.dto.response +package com.example.hackathon.core.network.dto.response import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/example/hackathon/data/service/AuthService.kt b/core/network/src/main/java/com/example/hackathon/core/network/service/AuthService.kt similarity index 69% rename from app/src/main/java/com/example/hackathon/data/service/AuthService.kt rename to core/network/src/main/java/com/example/hackathon/core/network/service/AuthService.kt index 4a3cd65..608e6e0 100644 --- a/app/src/main/java/com/example/hackathon/data/service/AuthService.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/service/AuthService.kt @@ -1,12 +1,12 @@ -package com.example.hackathon.data.service +package com.example.hackathon.core.network.service -import com.example.hackathon.data.dto.request.LoginRequest -import com.example.hackathon.data.dto.request.LogoutRequest -import com.example.hackathon.data.dto.request.ReissueRequest -import com.example.hackathon.data.dto.request.SignupRequest -import com.example.hackathon.data.dto.response.BaseResponse -import com.example.hackathon.data.dto.response.LoginResponse -import com.example.hackathon.data.dto.response.ReissueResponse +import com.example.hackathon.core.network.dto.request.LoginRequest +import com.example.hackathon.core.network.dto.request.LogoutRequest +import com.example.hackathon.core.network.dto.request.ReissueRequest +import com.example.hackathon.core.network.dto.request.SignupRequest +import com.example.hackathon.core.network.dto.response.BaseResponse +import com.example.hackathon.core.network.dto.response.LoginResponse +import com.example.hackathon.core.network.dto.response.ReissueResponse import okhttp3.ResponseBody import retrofit2.Response import retrofit2.http.Body diff --git a/app/src/main/java/com/example/hackathon/data/service/CombinationService.kt b/core/network/src/main/java/com/example/hackathon/core/network/service/CombinationService.kt similarity index 83% rename from app/src/main/java/com/example/hackathon/data/service/CombinationService.kt rename to core/network/src/main/java/com/example/hackathon/core/network/service/CombinationService.kt index e5ee05f..1e19037 100644 --- a/app/src/main/java/com/example/hackathon/data/service/CombinationService.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/service/CombinationService.kt @@ -1,11 +1,11 @@ -package com.example.hackathon.data.service +package com.example.hackathon.core.network.service -import com.example.hackathon.data.dto.request.CreateCombinationRequest -import com.example.hackathon.data.dto.request.RegisterRequest -import com.example.hackathon.data.dto.request.UpdateCombinationRequest -import com.example.hackathon.data.dto.request.UpdateRegisterRequest -import com.example.hackathon.data.dto.response.BaseResponse -import com.example.hackathon.data.dto.response.CombinationResponse +import com.example.hackathon.core.network.dto.request.CreateCombinationRequest +import com.example.hackathon.core.network.dto.request.RegisterRequest +import com.example.hackathon.core.network.dto.request.UpdateCombinationRequest +import com.example.hackathon.core.network.dto.request.UpdateRegisterRequest +import com.example.hackathon.core.network.dto.response.BaseResponse +import com.example.hackathon.core.network.dto.response.CombinationResponse import retrofit2.Response import retrofit2.http.* diff --git a/app/src/main/java/com/example/hackathon/data/service/HomeService.kt b/core/network/src/main/java/com/example/hackathon/core/network/service/HomeService.kt similarity index 65% rename from app/src/main/java/com/example/hackathon/data/service/HomeService.kt rename to core/network/src/main/java/com/example/hackathon/core/network/service/HomeService.kt index 810d304..3870ef5 100644 --- a/app/src/main/java/com/example/hackathon/data/service/HomeService.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/service/HomeService.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.data.service +package com.example.hackathon.core.network.service import retrofit2.http.GET diff --git a/app/src/main/java/com/example/hackathon/data/service/RecipeService.kt b/core/network/src/main/java/com/example/hackathon/core/network/service/RecipeService.kt similarity index 86% rename from app/src/main/java/com/example/hackathon/data/service/RecipeService.kt rename to core/network/src/main/java/com/example/hackathon/core/network/service/RecipeService.kt index 9d15870..6ef58ed 100644 --- a/app/src/main/java/com/example/hackathon/data/service/RecipeService.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/service/RecipeService.kt @@ -1,8 +1,8 @@ -package com.example.hackathon.data.service +package com.example.hackathon.core.network.service -import com.example.hackathon.data.dto.response.BaseResponse -import com.example.hackathon.data.dto.response.DetailResponse -import com.example.hackathon.data.dto.response.PostPreviewDto +import com.example.hackathon.core.network.dto.response.BaseResponse +import com.example.hackathon.core.network.dto.response.DetailResponse +import com.example.hackathon.core.network.dto.response.PostPreviewDto import retrofit2.Response import retrofit2.http.DELETE import retrofit2.http.GET diff --git a/app/src/main/java/com/example/hackathon/data/service/UserService.kt b/core/network/src/main/java/com/example/hackathon/core/network/service/UserService.kt similarity index 69% rename from app/src/main/java/com/example/hackathon/data/service/UserService.kt rename to core/network/src/main/java/com/example/hackathon/core/network/service/UserService.kt index c39dbbb..8458e2b 100644 --- a/app/src/main/java/com/example/hackathon/data/service/UserService.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/service/UserService.kt @@ -1,9 +1,9 @@ -package com.example.hackathon.data.service +package com.example.hackathon.core.network.service -import com.example.hackathon.data.dto.request.UpdateProfileRequest -import com.example.hackathon.data.dto.response.BaseResponse -import com.example.hackathon.data.dto.response.MyPageRecipeResponse -import com.example.hackathon.data.dto.response.UserProfileResponse +import com.example.hackathon.core.network.dto.request.UpdateProfileRequest +import com.example.hackathon.core.network.dto.response.BaseResponse +import com.example.hackathon.core.network.dto.response.MyPageRecipeResponse +import com.example.hackathon.core.network.dto.response.UserProfileResponse import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.PATCH diff --git a/app/src/main/java/com/example/hackathon/presentation/screen/auth/LoginScreen.kt b/feature/auth/src/main/java/com/example/hackathon/feature/auth/LoginScreen.kt similarity index 97% rename from app/src/main/java/com/example/hackathon/presentation/screen/auth/LoginScreen.kt rename to feature/auth/src/main/java/com/example/hackathon/feature/auth/LoginScreen.kt index c8ad746..0833e93 100644 --- a/app/src/main/java/com/example/hackathon/presentation/screen/auth/LoginScreen.kt +++ b/feature/auth/src/main/java/com/example/hackathon/feature/auth/LoginScreen.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.presentation.screen.auth +package com.example.hackathon.feature.auth import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement @@ -55,13 +55,13 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.example.hackathon.R -import com.example.hackathon.core.component.TopAppLogoBar -import com.example.hackathon.presentation.viewmodel.LoginViewModel -import com.example.hackathon.ui.theme.FontMedium -import com.example.hackathon.ui.theme.Gray700 -import com.example.hackathon.ui.theme.HackathonTheme -import com.example.hackathon.ui.theme.Primary +import com.example.hackathon.core.designsystem.R +import com.example.hackathon.core.designsystem.component.TopAppLogoBar +import com.example.hackathon.feature.auth.LoginViewModel +import com.example.hackathon.core.designsystem.theme.FontMedium +import com.example.hackathon.core.designsystem.theme.Gray700 +import com.example.hackathon.core.designsystem.theme.HackathonTheme +import com.example.hackathon.core.designsystem.theme.Primary import kotlinx.coroutines.delay import kotlinx.coroutines.launch diff --git a/app/src/main/java/com/example/hackathon/presentation/viewmodel/LoginViewModel.kt b/feature/auth/src/main/java/com/example/hackathon/feature/auth/LoginViewModel.kt similarity index 96% rename from app/src/main/java/com/example/hackathon/presentation/viewmodel/LoginViewModel.kt rename to feature/auth/src/main/java/com/example/hackathon/feature/auth/LoginViewModel.kt index 3974d7d..e3d9fb2 100644 --- a/app/src/main/java/com/example/hackathon/presentation/viewmodel/LoginViewModel.kt +++ b/feature/auth/src/main/java/com/example/hackathon/feature/auth/LoginViewModel.kt @@ -1,10 +1,10 @@ -package com.example.hackathon.presentation.viewmodel +package com.example.hackathon.feature.auth import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.example.hackathon.data.local.DummyData -import com.example.hackathon.domain.entity.User -import com.example.hackathon.domain.repository.AuthRepository +import com.example.hackathon.core.model.DummyData +import com.example.hackathon.core.model.User +import com.example.hackathon.core.domain.repository.AuthRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow diff --git a/app/src/main/java/com/example/hackathon/presentation/screen/auth/RegistrationScreen.kt b/feature/auth/src/main/java/com/example/hackathon/feature/auth/RegistrationScreen.kt similarity index 98% rename from app/src/main/java/com/example/hackathon/presentation/screen/auth/RegistrationScreen.kt rename to feature/auth/src/main/java/com/example/hackathon/feature/auth/RegistrationScreen.kt index f738b26..8d8a459 100644 --- a/app/src/main/java/com/example/hackathon/presentation/screen/auth/RegistrationScreen.kt +++ b/feature/auth/src/main/java/com/example/hackathon/feature/auth/RegistrationScreen.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.presentation.screen.auth +package com.example.hackathon.feature.auth import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -46,11 +46,11 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.example.hackathon.core.component.TopAppLogoBar -import com.example.hackathon.presentation.viewmodel.RegistrationViewModel -import com.example.hackathon.ui.theme.Gray700 -import com.example.hackathon.ui.theme.HackathonTheme -import com.example.hackathon.ui.theme.Primary +import com.example.hackathon.core.designsystem.component.TopAppLogoBar +import com.example.hackathon.feature.auth.RegistrationViewModel +import com.example.hackathon.core.designsystem.theme.Gray700 +import com.example.hackathon.core.designsystem.theme.HackathonTheme +import com.example.hackathon.core.designsystem.theme.Primary import kotlinx.coroutines.delay import kotlinx.coroutines.launch diff --git a/app/src/main/java/com/example/hackathon/presentation/screen/auth/RegistrationSuccessScreen.kt b/feature/auth/src/main/java/com/example/hackathon/feature/auth/RegistrationSuccessScreen.kt similarity index 91% rename from app/src/main/java/com/example/hackathon/presentation/screen/auth/RegistrationSuccessScreen.kt rename to feature/auth/src/main/java/com/example/hackathon/feature/auth/RegistrationSuccessScreen.kt index 3871557..1a42858 100644 --- a/app/src/main/java/com/example/hackathon/presentation/screen/auth/RegistrationSuccessScreen.kt +++ b/feature/auth/src/main/java/com/example/hackathon/feature/auth/RegistrationSuccessScreen.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.presentation.screen.auth +package com.example.hackathon.feature.auth import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -21,10 +21,10 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.example.hackathon.core.component.TopAppLogoBar -import com.example.hackathon.ui.theme.Gray700 -import com.example.hackathon.ui.theme.HackathonTheme -import com.example.hackathon.ui.theme.Primary +import com.example.hackathon.core.designsystem.component.TopAppLogoBar +import com.example.hackathon.core.designsystem.theme.Gray700 +import com.example.hackathon.core.designsystem.theme.HackathonTheme +import com.example.hackathon.core.designsystem.theme.Primary @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/app/src/main/java/com/example/hackathon/presentation/viewmodel/RegistrationViewModel.kt b/feature/auth/src/main/java/com/example/hackathon/feature/auth/RegistrationViewModel.kt similarity index 94% rename from app/src/main/java/com/example/hackathon/presentation/viewmodel/RegistrationViewModel.kt rename to feature/auth/src/main/java/com/example/hackathon/feature/auth/RegistrationViewModel.kt index 80e0a20..013c018 100644 --- a/app/src/main/java/com/example/hackathon/presentation/viewmodel/RegistrationViewModel.kt +++ b/feature/auth/src/main/java/com/example/hackathon/feature/auth/RegistrationViewModel.kt @@ -1,10 +1,10 @@ -package com.example.hackathon.presentation.viewmodel +package com.example.hackathon.feature.auth import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.example.hackathon.data.local.DummyData -import com.example.hackathon.domain.entity.User -import com.example.hackathon.domain.repository.AuthRepository +import com.example.hackathon.core.model.DummyData +import com.example.hackathon.core.model.User +import com.example.hackathon.core.domain.repository.AuthRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow diff --git a/app/src/main/java/com/example/hackathon/presentation/screen/combination/CreateCombinationScreen.kt b/feature/combination/src/main/java/com/example/hackathon/feature/combination/CreateCombinationScreen.kt similarity index 94% rename from app/src/main/java/com/example/hackathon/presentation/screen/combination/CreateCombinationScreen.kt rename to feature/combination/src/main/java/com/example/hackathon/feature/combination/CreateCombinationScreen.kt index eeb1041..4b4138b 100644 --- a/app/src/main/java/com/example/hackathon/presentation/screen/combination/CreateCombinationScreen.kt +++ b/feature/combination/src/main/java/com/example/hackathon/feature/combination/CreateCombinationScreen.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.presentation.screen.combination +package com.example.hackathon.feature.combination import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult @@ -48,16 +48,16 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.example.hackathon.R -import com.example.hackathon.core.component.TopAppLogoBar -import com.example.hackathon.presentation.component.HashTagInputSection -import com.example.hackathon.presentation.component.ImageUploadSection -import com.example.hackathon.presentation.component.IngredientInputSection -import com.example.hackathon.presentation.component.VisibilitySelectionSection -import com.example.hackathon.presentation.viewmodel.CreateCombinationViewModel -import com.example.hackathon.ui.theme.Gray700 -import com.example.hackathon.ui.theme.HackathonTheme -import com.example.hackathon.ui.theme.Primary +import com.example.hackathon.core.designsystem.R +import com.example.hackathon.core.designsystem.component.TopAppLogoBar +import com.example.hackathon.feature.combination.component.HashTagInputSection +import com.example.hackathon.feature.combination.component.ImageUploadSection +import com.example.hackathon.feature.combination.component.IngredientInputSection +import com.example.hackathon.feature.combination.component.VisibilitySelectionSection +import com.example.hackathon.feature.combination.CreateCombinationViewModel +import com.example.hackathon.core.designsystem.theme.Gray700 +import com.example.hackathon.core.designsystem.theme.HackathonTheme +import com.example.hackathon.core.designsystem.theme.Primary @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @Composable diff --git a/app/src/main/java/com/example/hackathon/presentation/viewmodel/CreateCombinationViewModel.kt b/feature/combination/src/main/java/com/example/hackathon/feature/combination/CreateCombinationViewModel.kt similarity index 95% rename from app/src/main/java/com/example/hackathon/presentation/viewmodel/CreateCombinationViewModel.kt rename to feature/combination/src/main/java/com/example/hackathon/feature/combination/CreateCombinationViewModel.kt index 81da43f..42c1233 100644 --- a/app/src/main/java/com/example/hackathon/presentation/viewmodel/CreateCombinationViewModel.kt +++ b/feature/combination/src/main/java/com/example/hackathon/feature/combination/CreateCombinationViewModel.kt @@ -1,11 +1,11 @@ -package com.example.hackathon.presentation.viewmodel +package com.example.hackathon.feature.combination import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.example.hackathon.domain.entity.Category -import com.example.hackathon.domain.entity.Combination -import com.example.hackathon.domain.repository.CombinationRepository -import com.example.hackathon.presentation.component.IngredientItem +import com.example.hackathon.core.model.Category +import com.example.hackathon.core.model.Combination +import com.example.hackathon.core.domain.repository.CombinationRepository +import com.example.hackathon.feature.combination.component.IngredientItem import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow diff --git a/app/src/main/java/com/example/hackathon/presentation/screen/combination/DetailScreen.kt b/feature/combination/src/main/java/com/example/hackathon/feature/combination/DetailScreen.kt similarity index 97% rename from app/src/main/java/com/example/hackathon/presentation/screen/combination/DetailScreen.kt rename to feature/combination/src/main/java/com/example/hackathon/feature/combination/DetailScreen.kt index 0a1f6cb..adafcee 100644 --- a/app/src/main/java/com/example/hackathon/presentation/screen/combination/DetailScreen.kt +++ b/feature/combination/src/main/java/com/example/hackathon/feature/combination/DetailScreen.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.presentation.screen.combination +package com.example.hackathon.feature.combination import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -48,11 +48,11 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage -import com.example.hackathon.R -import com.example.hackathon.data.local.DummyData -import com.example.hackathon.presentation.viewmodel.DetailViewModel -import com.example.hackathon.ui.theme.HackathonTheme -import com.example.hackathon.ui.theme.Primary +import com.example.hackathon.core.designsystem.R +import com.example.hackathon.core.model.DummyData +import com.example.hackathon.feature.combination.DetailViewModel +import com.example.hackathon.core.designsystem.theme.HackathonTheme +import com.example.hackathon.core.designsystem.theme.Primary @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/app/src/main/java/com/example/hackathon/presentation/viewmodel/DetailViewModel.kt b/feature/combination/src/main/java/com/example/hackathon/feature/combination/DetailViewModel.kt similarity index 93% rename from app/src/main/java/com/example/hackathon/presentation/viewmodel/DetailViewModel.kt rename to feature/combination/src/main/java/com/example/hackathon/feature/combination/DetailViewModel.kt index b34d15b..f89bfb4 100644 --- a/app/src/main/java/com/example/hackathon/presentation/viewmodel/DetailViewModel.kt +++ b/feature/combination/src/main/java/com/example/hackathon/feature/combination/DetailViewModel.kt @@ -1,10 +1,10 @@ -package com.example.hackathon.presentation.viewmodel +package com.example.hackathon.feature.combination import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.example.hackathon.domain.entity.RecipeDetail -import com.example.hackathon.domain.repository.CombinationRepository -import com.example.hackathon.domain.repository.RecipeRepository +import com.example.hackathon.core.model.RecipeDetail +import com.example.hackathon.core.domain.repository.CombinationRepository +import com.example.hackathon.core.domain.repository.RecipeRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow diff --git a/app/src/main/java/com/example/hackathon/presentation/component/HashTagInputSection.kt b/feature/combination/src/main/java/com/example/hackathon/feature/combination/component/HashTagInputSection.kt similarity index 91% rename from app/src/main/java/com/example/hackathon/presentation/component/HashTagInputSection.kt rename to feature/combination/src/main/java/com/example/hackathon/feature/combination/component/HashTagInputSection.kt index 54b83bb..ec42fd4 100644 --- a/app/src/main/java/com/example/hackathon/presentation/component/HashTagInputSection.kt +++ b/feature/combination/src/main/java/com/example/hackathon/feature/combination/component/HashTagInputSection.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.presentation.component +package com.example.hackathon.feature.combination.component import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -18,6 +18,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.example.hackathon.core.designsystem.component.TagChip @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @Composable @@ -58,7 +59,7 @@ fun HashTagInputSection( Text( text = "띄어쓰기로 입력 (1개~5개)", style = MaterialTheme.typography.bodyMedium, - color = com.example.hackathon.ui.theme.Gray700, + color = com.example.hackathon.core.designsystem.theme.Gray700, ) }, modifier = Modifier.fillMaxWidth(), @@ -66,14 +67,14 @@ fun HashTagInputSection( shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp), colors = androidx.compose.material3.OutlinedTextFieldDefaults.colors( - focusedBorderColor = com.example.hackathon.ui.theme.Primary, - unfocusedBorderColor = com.example.hackathon.ui.theme.Primary.copy(alpha = 0.5f), + focusedBorderColor = com.example.hackathon.core.designsystem.theme.Primary, + unfocusedBorderColor = com.example.hackathon.core.designsystem.theme.Primary.copy(alpha = 0.5f), ), leadingIcon = { Text( text = "#", style = MaterialTheme.typography.bodyLarge, - color = com.example.hackathon.ui.theme.Primary, + color = com.example.hackathon.core.designsystem.theme.Primary, modifier = Modifier.padding(start = 16.dp), ) }, diff --git a/app/src/main/java/com/example/hackathon/presentation/component/ImageUploadSection.kt b/feature/combination/src/main/java/com/example/hackathon/feature/combination/component/ImageUploadSection.kt similarity index 97% rename from app/src/main/java/com/example/hackathon/presentation/component/ImageUploadSection.kt rename to feature/combination/src/main/java/com/example/hackathon/feature/combination/component/ImageUploadSection.kt index 575e81b..36df216 100644 --- a/app/src/main/java/com/example/hackathon/presentation/component/ImageUploadSection.kt +++ b/feature/combination/src/main/java/com/example/hackathon/feature/combination/component/ImageUploadSection.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.presentation.component +package com.example.hackathon.feature.combination.component import android.net.Uri import androidx.compose.foundation.background @@ -37,10 +37,10 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage -import com.example.hackathon.R -import com.example.hackathon.ui.theme.Gray50 -import com.example.hackathon.ui.theme.Gray700 -import com.example.hackathon.ui.theme.HackathonTheme +import com.example.hackathon.core.designsystem.R +import com.example.hackathon.core.designsystem.theme.Gray50 +import com.example.hackathon.core.designsystem.theme.Gray700 +import com.example.hackathon.core.designsystem.theme.HackathonTheme @Composable fun ImageUploadSection( diff --git a/app/src/main/java/com/example/hackathon/presentation/component/IngredientInputSection.kt b/feature/combination/src/main/java/com/example/hackathon/feature/combination/component/IngredientInputSection.kt similarity index 96% rename from app/src/main/java/com/example/hackathon/presentation/component/IngredientInputSection.kt rename to feature/combination/src/main/java/com/example/hackathon/feature/combination/component/IngredientInputSection.kt index e98a008..1c363b6 100644 --- a/app/src/main/java/com/example/hackathon/presentation/component/IngredientInputSection.kt +++ b/feature/combination/src/main/java/com/example/hackathon/feature/combination/component/IngredientInputSection.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.presentation.component +package com.example.hackathon.feature.combination.component import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -16,8 +16,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.example.hackathon.ui.theme.Gray700 -import com.example.hackathon.ui.theme.Primary +import com.example.hackathon.core.designsystem.theme.Gray700 +import com.example.hackathon.core.designsystem.theme.Primary data class IngredientItem( val name: String, diff --git a/app/src/main/java/com/example/hackathon/presentation/component/VisibilitySelectionSection.kt b/feature/combination/src/main/java/com/example/hackathon/feature/combination/component/VisibilitySelectionSection.kt similarity index 94% rename from app/src/main/java/com/example/hackathon/presentation/component/VisibilitySelectionSection.kt rename to feature/combination/src/main/java/com/example/hackathon/feature/combination/component/VisibilitySelectionSection.kt index 25b0599..ca0c732 100644 --- a/app/src/main/java/com/example/hackathon/presentation/component/VisibilitySelectionSection.kt +++ b/feature/combination/src/main/java/com/example/hackathon/feature/combination/component/VisibilitySelectionSection.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.presentation.component +package com.example.hackathon.feature.combination.component import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement @@ -15,8 +15,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.example.hackathon.ui.theme.Gray700 -import com.example.hackathon.ui.theme.Primary +import com.example.hackathon.core.designsystem.theme.Gray700 +import com.example.hackathon.core.designsystem.theme.Primary @Composable fun VisibilitySelectionSection( diff --git a/app/src/test/java/com/example/hackathon/presentation/viewmodel/CreateCombinationViewModelTest.kt b/feature/combination/src/test/java/com/example/hackathon/feature/combination/CreateCombinationViewModelTest.kt similarity index 94% rename from app/src/test/java/com/example/hackathon/presentation/viewmodel/CreateCombinationViewModelTest.kt rename to feature/combination/src/test/java/com/example/hackathon/feature/combination/CreateCombinationViewModelTest.kt index 73ee2b0..3f248c3 100644 --- a/app/src/test/java/com/example/hackathon/presentation/viewmodel/CreateCombinationViewModelTest.kt +++ b/feature/combination/src/test/java/com/example/hackathon/feature/combination/CreateCombinationViewModelTest.kt @@ -1,11 +1,10 @@ -package com.example.hackathon.presentation.viewmodel +package com.example.hackathon.feature.combination import app.cash.turbine.test -import com.example.hackathon.domain.entity.Category -import com.example.hackathon.domain.entity.Combination -import com.example.hackathon.domain.entity.User -import com.example.hackathon.domain.repository.CombinationRepository -import com.example.hackathon.util.MainDispatcherRule +import com.example.hackathon.core.model.Category +import com.example.hackathon.core.model.Combination +import com.example.hackathon.core.model.User +import com.example.hackathon.core.domain.repository.CombinationRepository import io.mockk.coEvery import io.mockk.mockk import kotlinx.coroutines.test.advanceUntilIdle diff --git a/app/src/test/java/com/example/hackathon/util/MainDispatcherRule.kt b/feature/combination/src/test/java/com/example/hackathon/feature/combination/MainDispatcherRule.kt similarity index 93% rename from app/src/test/java/com/example/hackathon/util/MainDispatcherRule.kt rename to feature/combination/src/test/java/com/example/hackathon/feature/combination/MainDispatcherRule.kt index fe10d57..efd8d94 100644 --- a/app/src/test/java/com/example/hackathon/util/MainDispatcherRule.kt +++ b/feature/combination/src/test/java/com/example/hackathon/feature/combination/MainDispatcherRule.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.util +package com.example.hackathon.feature.combination import kotlinx.coroutines.test.TestDispatcher import kotlinx.coroutines.test.UnconfinedTestDispatcher diff --git a/app/src/main/java/com/example/hackathon/presentation/screen/home/HomeCombinationList.kt b/feature/home/src/main/java/com/example/hackathon/feature/home/HomeCombinationList.kt similarity index 88% rename from app/src/main/java/com/example/hackathon/presentation/screen/home/HomeCombinationList.kt rename to feature/home/src/main/java/com/example/hackathon/feature/home/HomeCombinationList.kt index cf1e47b..cfb6aad 100644 --- a/app/src/main/java/com/example/hackathon/presentation/screen/home/HomeCombinationList.kt +++ b/feature/home/src/main/java/com/example/hackathon/feature/home/HomeCombinationList.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.presentation.screen.home +package com.example.hackathon.feature.home import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -8,11 +8,11 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.example.hackathon.core.component.CombinationCard -import com.example.hackathon.domain.entity.Category -import com.example.hackathon.domain.entity.Combination -import com.example.hackathon.domain.entity.User -import com.example.hackathon.ui.theme.HackathonTheme +import com.example.hackathon.core.designsystem.component.CombinationCard +import com.example.hackathon.core.model.Category +import com.example.hackathon.core.model.Combination +import com.example.hackathon.core.model.User +import com.example.hackathon.core.designsystem.theme.HackathonTheme fun LazyListScope.HomeCombinationList( combinations: List, diff --git a/app/src/main/java/com/example/hackathon/presentation/screen/home/HomeContent.kt b/feature/home/src/main/java/com/example/hackathon/feature/home/HomeContent.kt similarity index 93% rename from app/src/main/java/com/example/hackathon/presentation/screen/home/HomeContent.kt rename to feature/home/src/main/java/com/example/hackathon/feature/home/HomeContent.kt index a0352ca..e13067c 100644 --- a/app/src/main/java/com/example/hackathon/presentation/screen/home/HomeContent.kt +++ b/feature/home/src/main/java/com/example/hackathon/feature/home/HomeContent.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.presentation.screen.home +package com.example.hackathon.feature.home import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -13,12 +13,12 @@ import androidx.compose.ui.Modifier import androidx.compose.material3.CircularProgressIndicator import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.example.hackathon.domain.entity.Category -import com.example.hackathon.domain.entity.Combination -import com.example.hackathon.domain.entity.User -import com.example.hackathon.presentation.component.FilterBar -import com.example.hackathon.presentation.viewmodel.HomeUiState -import com.example.hackathon.ui.theme.HackathonTheme +import com.example.hackathon.core.model.Category +import com.example.hackathon.core.model.Combination +import com.example.hackathon.core.model.User +import com.example.hackathon.feature.home.component.FilterBar +import com.example.hackathon.feature.home.HomeUiState +import com.example.hackathon.core.designsystem.theme.HackathonTheme @Composable fun HomeContent( diff --git a/app/src/main/java/com/example/hackathon/presentation/screen/home/HomeScreen.kt b/feature/home/src/main/java/com/example/hackathon/feature/home/HomeScreen.kt similarity index 88% rename from app/src/main/java/com/example/hackathon/presentation/screen/home/HomeScreen.kt rename to feature/home/src/main/java/com/example/hackathon/feature/home/HomeScreen.kt index 2aa2654..3d3e568 100644 --- a/app/src/main/java/com/example/hackathon/presentation/screen/home/HomeScreen.kt +++ b/feature/home/src/main/java/com/example/hackathon/feature/home/HomeScreen.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.presentation.screen.home +package com.example.hackathon.feature.home import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth @@ -11,12 +11,12 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController -import com.example.hackathon.domain.entity.Category -import com.example.hackathon.domain.entity.Combination -import com.example.hackathon.domain.entity.User -import com.example.hackathon.presentation.viewmodel.HomeUiState -import com.example.hackathon.presentation.viewmodel.HomeViewModel -import com.example.hackathon.ui.theme.HackathonTheme +import com.example.hackathon.core.model.Category +import com.example.hackathon.core.model.Combination +import com.example.hackathon.core.model.User +import com.example.hackathon.feature.home.HomeUiState +import com.example.hackathon.feature.home.HomeViewModel +import com.example.hackathon.core.designsystem.theme.HackathonTheme @Composable fun HomeScreen( diff --git a/app/src/main/java/com/example/hackathon/presentation/screen/home/HomeStateContent.kt b/feature/home/src/main/java/com/example/hackathon/feature/home/HomeStateContent.kt similarity index 93% rename from app/src/main/java/com/example/hackathon/presentation/screen/home/HomeStateContent.kt rename to feature/home/src/main/java/com/example/hackathon/feature/home/HomeStateContent.kt index cc3cea5..dc34001 100644 --- a/app/src/main/java/com/example/hackathon/presentation/screen/home/HomeStateContent.kt +++ b/feature/home/src/main/java/com/example/hackathon/feature/home/HomeStateContent.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.presentation.screen.home +package com.example.hackathon.feature.home import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth @@ -12,8 +12,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.example.hackathon.ui.theme.Gray700 -import com.example.hackathon.ui.theme.HackathonTheme +import com.example.hackathon.core.designsystem.theme.Gray700 +import com.example.hackathon.core.designsystem.theme.HackathonTheme fun LazyListScope.HomeLoadingState() { item { diff --git a/app/src/main/java/com/example/hackathon/presentation/screen/home/HomeTopBar.kt b/feature/home/src/main/java/com/example/hackathon/feature/home/HomeTopBar.kt similarity index 90% rename from app/src/main/java/com/example/hackathon/presentation/screen/home/HomeTopBar.kt rename to feature/home/src/main/java/com/example/hackathon/feature/home/HomeTopBar.kt index 03b5021..7a593be 100644 --- a/app/src/main/java/com/example/hackathon/presentation/screen/home/HomeTopBar.kt +++ b/feature/home/src/main/java/com/example/hackathon/feature/home/HomeTopBar.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.presentation.screen.home +package com.example.hackathon.feature.home import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -12,8 +12,8 @@ import androidx.compose.ui.draw.shadow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.navigation.NavController -import com.example.hackathon.presentation.component.SearchComponent -import com.example.hackathon.ui.theme.HackathonTheme +import com.example.hackathon.feature.home.component.SearchComponent +import com.example.hackathon.core.designsystem.theme.HackathonTheme @Composable fun HomeTopBar( diff --git a/app/src/main/java/com/example/hackathon/presentation/viewmodel/HomeViewModel.kt b/feature/home/src/main/java/com/example/hackathon/feature/home/HomeViewModel.kt similarity index 97% rename from app/src/main/java/com/example/hackathon/presentation/viewmodel/HomeViewModel.kt rename to feature/home/src/main/java/com/example/hackathon/feature/home/HomeViewModel.kt index bfe951a..a74723c 100644 --- a/app/src/main/java/com/example/hackathon/presentation/viewmodel/HomeViewModel.kt +++ b/feature/home/src/main/java/com/example/hackathon/feature/home/HomeViewModel.kt @@ -1,10 +1,10 @@ -package com.example.hackathon.presentation.viewmodel +package com.example.hackathon.feature.home import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.example.hackathon.domain.entity.Category -import com.example.hackathon.domain.entity.Combination -import com.example.hackathon.domain.repository.CombinationRepository +import com.example.hackathon.core.model.Category +import com.example.hackathon.core.model.Combination +import com.example.hackathon.core.domain.repository.CombinationRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow diff --git a/app/src/main/java/com/example/hackathon/presentation/screen/home/SearchScreen.kt b/feature/home/src/main/java/com/example/hackathon/feature/home/SearchScreen.kt similarity index 95% rename from app/src/main/java/com/example/hackathon/presentation/screen/home/SearchScreen.kt rename to feature/home/src/main/java/com/example/hackathon/feature/home/SearchScreen.kt index 28b9561..6cee362 100644 --- a/app/src/main/java/com/example/hackathon/presentation/screen/home/SearchScreen.kt +++ b/feature/home/src/main/java/com/example/hackathon/feature/home/SearchScreen.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.presentation.screen.home +package com.example.hackathon.feature.home import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -37,11 +37,11 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.example.hackathon.R -import com.example.hackathon.presentation.component.CombinationList -import com.example.hackathon.presentation.viewmodel.SearchViewModel -import com.example.hackathon.ui.theme.Gray700 -import com.example.hackathon.ui.theme.HackathonTheme +import com.example.hackathon.core.designsystem.R +import com.example.hackathon.feature.home.component.CombinationList +import com.example.hackathon.feature.home.SearchViewModel +import com.example.hackathon.core.designsystem.theme.Gray700 +import com.example.hackathon.core.designsystem.theme.HackathonTheme @Composable fun SearchScreen( diff --git a/app/src/main/java/com/example/hackathon/presentation/viewmodel/SearchViewModel.kt b/feature/home/src/main/java/com/example/hackathon/feature/home/SearchViewModel.kt similarity index 96% rename from app/src/main/java/com/example/hackathon/presentation/viewmodel/SearchViewModel.kt rename to feature/home/src/main/java/com/example/hackathon/feature/home/SearchViewModel.kt index 0b5847b..37323cf 100644 --- a/app/src/main/java/com/example/hackathon/presentation/viewmodel/SearchViewModel.kt +++ b/feature/home/src/main/java/com/example/hackathon/feature/home/SearchViewModel.kt @@ -1,9 +1,9 @@ -package com.example.hackathon.presentation.viewmodel +package com.example.hackathon.feature.home import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.example.hackathon.domain.entity.Combination -import com.example.hackathon.domain.repository.CombinationRepository +import com.example.hackathon.core.model.Combination +import com.example.hackathon.core.domain.repository.CombinationRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow diff --git a/app/src/main/java/com/example/hackathon/presentation/component/CombinationList.kt b/feature/home/src/main/java/com/example/hackathon/feature/home/component/CombinationList.kt similarity index 85% rename from app/src/main/java/com/example/hackathon/presentation/component/CombinationList.kt rename to feature/home/src/main/java/com/example/hackathon/feature/home/component/CombinationList.kt index 2856b42..3106f55 100644 --- a/app/src/main/java/com/example/hackathon/presentation/component/CombinationList.kt +++ b/feature/home/src/main/java/com/example/hackathon/feature/home/component/CombinationList.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.presentation.component +package com.example.hackathon.feature.home.component import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -14,10 +14,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import com.example.hackathon.core.component.CombinationCard -import com.example.hackathon.domain.entity.Combination -import com.example.hackathon.ui.theme.Gray700 -import com.example.hackathon.ui.theme.HackathonTheme +import com.example.hackathon.core.designsystem.component.CombinationCard +import com.example.hackathon.core.model.Combination +import com.example.hackathon.core.designsystem.theme.Gray700 +import com.example.hackathon.core.designsystem.theme.HackathonTheme @Composable fun CombinationList( diff --git a/app/src/main/java/com/example/hackathon/presentation/component/FilterBar.kt b/feature/home/src/main/java/com/example/hackathon/feature/home/component/FilterBar.kt similarity index 87% rename from app/src/main/java/com/example/hackathon/presentation/component/FilterBar.kt rename to feature/home/src/main/java/com/example/hackathon/feature/home/component/FilterBar.kt index 3a146de..69b9fab 100644 --- a/app/src/main/java/com/example/hackathon/presentation/component/FilterBar.kt +++ b/feature/home/src/main/java/com/example/hackathon/feature/home/component/FilterBar.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.presentation.component +package com.example.hackathon.feature.home.component import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -15,10 +15,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp -import com.example.hackathon.R -import com.example.hackathon.core.component.CategoryChip -import com.example.hackathon.domain.entity.Category -import com.example.hackathon.ui.theme.HackathonTheme +import com.example.hackathon.core.designsystem.R +import com.example.hackathon.core.designsystem.component.CategoryChip +import com.example.hackathon.core.model.Category +import com.example.hackathon.core.designsystem.theme.HackathonTheme @Composable fun FilterBar( diff --git a/app/src/main/java/com/example/hackathon/presentation/component/SearchComponent.kt b/feature/home/src/main/java/com/example/hackathon/feature/home/component/SearchComponent.kt similarity index 91% rename from app/src/main/java/com/example/hackathon/presentation/component/SearchComponent.kt rename to feature/home/src/main/java/com/example/hackathon/feature/home/component/SearchComponent.kt index e0f2717..cf639b7 100644 --- a/app/src/main/java/com/example/hackathon/presentation/component/SearchComponent.kt +++ b/feature/home/src/main/java/com/example/hackathon/feature/home/component/SearchComponent.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.presentation.component +package com.example.hackathon.feature.home.component import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -18,9 +18,9 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp -import com.example.hackathon.R -import com.example.hackathon.core.util.noRippleClickable -import com.example.hackathon.ui.theme.HackathonTheme +import com.example.hackathon.core.designsystem.R +import com.example.hackathon.core.designsystem.util.noRippleClickable +import com.example.hackathon.core.designsystem.theme.HackathonTheme @Composable fun SearchComponent( diff --git a/app/src/main/java/com/example/hackathon/presentation/screen/onboarding/OnboardingScreen.kt b/feature/onboarding/src/main/java/com/example/hackathon/feature/onboarding/OnboardingScreen.kt similarity index 95% rename from app/src/main/java/com/example/hackathon/presentation/screen/onboarding/OnboardingScreen.kt rename to feature/onboarding/src/main/java/com/example/hackathon/feature/onboarding/OnboardingScreen.kt index b328623..cfa82f3 100644 --- a/app/src/main/java/com/example/hackathon/presentation/screen/onboarding/OnboardingScreen.kt +++ b/feature/onboarding/src/main/java/com/example/hackathon/feature/onboarding/OnboardingScreen.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.presentation.screen.onboarding +package com.example.hackathon.feature.onboarding import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -24,8 +24,8 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.example.hackathon.R -import com.example.hackathon.ui.theme.HackathonTheme +import com.example.hackathon.core.designsystem.R +import com.example.hackathon.core.designsystem.theme.HackathonTheme import kotlinx.coroutines.delay @OptIn(ExperimentalMaterial3Api::class) diff --git a/app/src/main/java/com/example/hackathon/presentation/screen/profile/EditProfileScreen.kt b/feature/profile/src/main/java/com/example/hackathon/feature/profile/EditProfileScreen.kt similarity index 97% rename from app/src/main/java/com/example/hackathon/presentation/screen/profile/EditProfileScreen.kt rename to feature/profile/src/main/java/com/example/hackathon/feature/profile/EditProfileScreen.kt index 325b926..2b12bf2 100644 --- a/app/src/main/java/com/example/hackathon/presentation/screen/profile/EditProfileScreen.kt +++ b/feature/profile/src/main/java/com/example/hackathon/feature/profile/EditProfileScreen.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.presentation.screen.profile +package com.example.hackathon.feature.profile import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -41,10 +41,10 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage -import com.example.hackathon.presentation.viewmodel.EditProfileViewModel -import com.example.hackathon.ui.theme.Gray700 -import com.example.hackathon.ui.theme.HackathonTheme -import com.example.hackathon.ui.theme.Primary +import com.example.hackathon.feature.profile.EditProfileViewModel +import com.example.hackathon.core.designsystem.theme.Gray700 +import com.example.hackathon.core.designsystem.theme.HackathonTheme +import com.example.hackathon.core.designsystem.theme.Primary @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/app/src/main/java/com/example/hackathon/presentation/viewmodel/EditProfileViewModel.kt b/feature/profile/src/main/java/com/example/hackathon/feature/profile/EditProfileViewModel.kt similarity index 96% rename from app/src/main/java/com/example/hackathon/presentation/viewmodel/EditProfileViewModel.kt rename to feature/profile/src/main/java/com/example/hackathon/feature/profile/EditProfileViewModel.kt index 24b9e87..fcdc37f 100644 --- a/app/src/main/java/com/example/hackathon/presentation/viewmodel/EditProfileViewModel.kt +++ b/feature/profile/src/main/java/com/example/hackathon/feature/profile/EditProfileViewModel.kt @@ -1,8 +1,8 @@ -package com.example.hackathon.presentation.viewmodel +package com.example.hackathon.feature.profile import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.example.hackathon.domain.repository.UserRepository +import com.example.hackathon.core.domain.repository.UserRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow diff --git a/app/src/main/java/com/example/hackathon/presentation/viewmodel/MyPageViewModel.kt b/feature/profile/src/main/java/com/example/hackathon/feature/profile/MyPageViewModel.kt similarity index 95% rename from app/src/main/java/com/example/hackathon/presentation/viewmodel/MyPageViewModel.kt rename to feature/profile/src/main/java/com/example/hackathon/feature/profile/MyPageViewModel.kt index b0025f5..aeb4ce8 100644 --- a/app/src/main/java/com/example/hackathon/presentation/viewmodel/MyPageViewModel.kt +++ b/feature/profile/src/main/java/com/example/hackathon/feature/profile/MyPageViewModel.kt @@ -1,12 +1,12 @@ -package com.example.hackathon.presentation.viewmodel +package com.example.hackathon.feature.profile import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.example.hackathon.domain.entity.Combination -import com.example.hackathon.domain.entity.User -import com.example.hackathon.domain.repository.AuthRepository -import com.example.hackathon.domain.repository.CombinationRepository -import com.example.hackathon.domain.repository.UserRepository +import com.example.hackathon.core.model.Combination +import com.example.hackathon.core.model.User +import com.example.hackathon.core.domain.repository.AuthRepository +import com.example.hackathon.core.domain.repository.CombinationRepository +import com.example.hackathon.core.domain.repository.UserRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow diff --git a/app/src/main/java/com/example/hackathon/presentation/screen/profile/MyScreen.kt b/feature/profile/src/main/java/com/example/hackathon/feature/profile/MyScreen.kt similarity index 94% rename from app/src/main/java/com/example/hackathon/presentation/screen/profile/MyScreen.kt rename to feature/profile/src/main/java/com/example/hackathon/feature/profile/MyScreen.kt index 25a3cb0..7210304 100644 --- a/app/src/main/java/com/example/hackathon/presentation/screen/profile/MyScreen.kt +++ b/feature/profile/src/main/java/com/example/hackathon/feature/profile/MyScreen.kt @@ -1,4 +1,4 @@ -package com.example.hackathon.presentation.screen.profile +package com.example.hackathon.feature.profile import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -51,15 +51,15 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage -import com.example.hackathon.R -import com.example.hackathon.core.component.CombinationCard -import com.example.hackathon.core.component.TopAppLogoBar -import com.example.hackathon.presentation.viewmodel.MyPageTab -import com.example.hackathon.presentation.viewmodel.MyPageViewModel -import com.example.hackathon.ui.theme.Gray50 -import com.example.hackathon.ui.theme.Gray700 -import com.example.hackathon.ui.theme.HackathonTheme -import com.example.hackathon.ui.theme.Primary +import com.example.hackathon.core.designsystem.R +import com.example.hackathon.core.designsystem.component.CombinationCard +import com.example.hackathon.core.designsystem.component.TopAppLogoBar +import com.example.hackathon.feature.profile.MyPageTab +import com.example.hackathon.feature.profile.MyPageViewModel +import com.example.hackathon.core.designsystem.theme.Gray50 +import com.example.hackathon.core.designsystem.theme.Gray700 +import com.example.hackathon.core.designsystem.theme.HackathonTheme +import com.example.hackathon.core.designsystem.theme.Primary @OptIn(ExperimentalMaterial3Api::class) @Composable From 5fa3cfe0f71575c960169d9689aa98e76b93c15c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=BC=ED=98=81?= Date: Sun, 31 May 2026 08:03:47 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=EB=A9=80=ED=8B=B0=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=EC=95=84=ED=82=A4=ED=85=8D=EC=B2=98=20=EB=8F=84?= =?UTF-8?q?=EC=9E=85=20=EB=B0=8F=20Gradle=20=EC=BB=A8=EB=B2=A4=EC=85=98=20?= =?UTF-8?q?=ED=94=8C=EB=9F=AC=EA=B7=B8=EC=9D=B8=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 프로젝트 구조를 `core` 및 `feature` 계층으로 분리하여 멀티 모듈 아키텍처 도입 - 공통 빌드 로직 관리를 위한 `build-logic` 도입 및 컨벤션 플러그인(Application, Library, Feature, Hilt 등) 구현 - 레이어별 핵심 모듈 생성: `:core:model`, `:core:domain`, `:core:data`, `:core:network`, `:core:designsystem`, `:core:datastore` - 기능별 피처 모듈 생성: `:feature:auth`, `:feature:home`, `:feature:combination`, `:feature:onboarding`, `:feature:profile` - `app` 모듈의 비대해진 `build.gradle.kts` 설정을 모듈별로 분산하고 의존성 구조 최적화 - 모듈화에 따른 패키지 이동 및 관련 소스코드 임포트 경로 일괄 수정 - 프로젝트 전체 SDK 버전을 36(Android 15)으로 상향 조정 및 Java 17 설정 적용 --- app/build.gradle.kts | 109 ++++-------------- .../example/hackathon/HackathonApplication.kt | 13 ++- .../com/example/hackathon/MainActivity.kt | 2 +- .../presentation/navigation/BottomNavBar.kt | 4 +- .../presentation/navigation/NavGraph.kt | 22 ++-- .../presentation/navigation/NavTab.kt | 2 +- .../presentation/viewmodel/MainViewModel.kt | 4 +- build-logic/build.gradle.kts | 48 ++++++++ build-logic/settings.gradle.kts | 14 +++ ...droidApplicationComposeConventionPlugin.kt | 20 ++++ .../AndroidApplicationConventionPlugin.kt | 27 +++++ .../kotlin/AndroidFeatureConventionPlugin.kt | 42 +++++++ .../kotlin/AndroidHiltConventionPlugin.kt | 24 ++++ .../AndroidLibraryComposeConventionPlugin.kt | 23 ++++ .../kotlin/AndroidLibraryConventionPlugin.kt | 27 +++++ .../kotlin/KotlinLibraryConventionPlugin.kt | 31 +++++ .../hackathon/buildlogic/AndroidCompose.kt | 37 ++++++ .../hackathon/buildlogic/KotlinAndroid.kt | 40 +++++++ .../hackathon/buildlogic/ProjectExtensions.kt | 13 +++ build.gradle.kts | 7 +- core/data/build.gradle.kts | 26 +++++ core/data/src/main/AndroidManifest.xml | 2 + core/datastore/build.gradle.kts | 12 ++ core/datastore/src/main/AndroidManifest.xml | 2 + core/designsystem/build.gradle.kts | 19 +++ .../designsystem/src/main/AndroidManifest.xml | 2 + core/domain/build.gradle.kts | 15 +++ core/domain/src/main/AndroidManifest.xml | 2 + core/model/build.gradle.kts | 7 ++ core/model/src/main/AndroidManifest.xml | 2 + core/network/build.gradle.kts | 29 +++++ core/network/src/main/AndroidManifest.xml | 4 + feature/auth/build.gradle.kts | 7 ++ feature/auth/src/main/AndroidManifest.xml | 2 + feature/combination/build.gradle.kts | 7 ++ .../combination/src/main/AndroidManifest.xml | 2 + feature/home/build.gradle.kts | 7 ++ feature/home/src/main/AndroidManifest.xml | 2 + feature/onboarding/build.gradle.kts | 7 ++ .../onboarding/src/main/AndroidManifest.xml | 2 + feature/profile/build.gradle.kts | 7 ++ feature/profile/src/main/AndroidManifest.xml | 2 + gradle/libs.versions.toml | 18 ++- gradlew | 0 settings.gradle.kts | 17 ++- 45 files changed, 597 insertions(+), 114 deletions(-) create mode 100644 build-logic/build.gradle.kts create mode 100644 build-logic/settings.gradle.kts create mode 100644 build-logic/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt create mode 100644 build-logic/src/main/kotlin/AndroidApplicationConventionPlugin.kt create mode 100644 build-logic/src/main/kotlin/AndroidFeatureConventionPlugin.kt create mode 100644 build-logic/src/main/kotlin/AndroidHiltConventionPlugin.kt create mode 100644 build-logic/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt create mode 100644 build-logic/src/main/kotlin/AndroidLibraryConventionPlugin.kt create mode 100644 build-logic/src/main/kotlin/KotlinLibraryConventionPlugin.kt create mode 100644 build-logic/src/main/kotlin/com/example/hackathon/buildlogic/AndroidCompose.kt create mode 100644 build-logic/src/main/kotlin/com/example/hackathon/buildlogic/KotlinAndroid.kt create mode 100644 build-logic/src/main/kotlin/com/example/hackathon/buildlogic/ProjectExtensions.kt create mode 100644 core/data/build.gradle.kts create mode 100644 core/data/src/main/AndroidManifest.xml create mode 100644 core/datastore/build.gradle.kts create mode 100644 core/datastore/src/main/AndroidManifest.xml create mode 100644 core/designsystem/build.gradle.kts create mode 100644 core/designsystem/src/main/AndroidManifest.xml create mode 100644 core/domain/build.gradle.kts create mode 100644 core/domain/src/main/AndroidManifest.xml create mode 100644 core/model/build.gradle.kts create mode 100644 core/model/src/main/AndroidManifest.xml create mode 100644 core/network/build.gradle.kts create mode 100644 core/network/src/main/AndroidManifest.xml create mode 100644 feature/auth/build.gradle.kts create mode 100644 feature/auth/src/main/AndroidManifest.xml create mode 100644 feature/combination/build.gradle.kts create mode 100644 feature/combination/src/main/AndroidManifest.xml create mode 100644 feature/home/build.gradle.kts create mode 100644 feature/home/src/main/AndroidManifest.xml create mode 100644 feature/onboarding/build.gradle.kts create mode 100644 feature/onboarding/src/main/AndroidManifest.xml create mode 100644 feature/profile/build.gradle.kts create mode 100644 feature/profile/src/main/AndroidManifest.xml mode change 100644 => 100755 gradlew diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2f5d6a8..66f0da5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,36 +1,23 @@ plugins { - alias(libs.plugins.android.application) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.compose) - alias(libs.plugins.kotlin.serialization) - alias(libs.plugins.hilt) - alias(libs.plugins.ksp) - alias(libs.plugins.detekt) + id("hackathon.android.application") + id("hackathon.android.application.compose") + id("hackathon.android.hilt") alias(libs.plugins.google.services) } android { namespace = "com.example.hackathon" - compileSdk { - version = release(36) - } defaultConfig { applicationId = "com.example.hackathon" - minSdk = 36 - targetSdk = 36 versionCode = 1 versionName = "1.0" - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - - // 서버 준비 전까지 Mock 모드 사용 (true: Mock, false: 실제 API) + // HackathonApplication / MainViewModel 의 Mock 모드 분기용 buildConfigField("boolean", "USE_MOCK_API", "false") - buildConfigField("String", "BASE_URL", "\"http://13.125.27.133/\"") } buildFeatures { - compose = true buildConfig = true } @@ -39,61 +26,44 @@ android { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" + "proguard-rules.pro", ) } } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 // Detekt 호환성을 위해 17로 설정 - } - kotlinOptions { - jvmTarget = "17" // Java 컴파일러와 일치시킴 (compileOptions와 동일) - } } dependencies { - implementation("androidx.core:core-splashscreen:1.2.0") + // 모듈 의존 + implementation(project(":core:model")) + implementation(project(":core:domain")) + implementation(project(":core:data")) + implementation(project(":core:network")) + implementation(project(":core:datastore")) + implementation(project(":core:designsystem")) + implementation(project(":feature:auth")) + implementation(project(":feature:home")) + implementation(project(":feature:combination")) + implementation(project(":feature:onboarding")) + implementation(project(":feature:profile")) + + // AndroidX / Compose + implementation(libs.androidx.core.splashscreen) implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.activity.compose) - implementation(platform(libs.androidx.compose.bom)) - implementation(libs.androidx.compose.ui) - implementation(libs.androidx.compose.ui.graphics) - implementation(libs.androidx.compose.ui.tooling.preview) - implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.material.icons.extended) + implementation(libs.androidx.compose.foundation.layout) // Navigation implementation(libs.androidx.navigation.compose) - - // Network - implementation(libs.okhttp) - implementation(libs.okhttp.logging.interceptor) - implementation(libs.retrofit) - implementation(libs.retrofit.kotlinx.serialization) - implementation(libs.kotlinx.serialization.json) - - // Image Loading - implementation(libs.coil.compose) - implementation(libs.coil.network.okhttp) - - // Hilt - implementation(libs.hilt.android) - implementation(libs.androidx.compose.foundation.layout) - ksp(libs.hilt.compiler) implementation(libs.hilt.navigation.compose) - // DataStore - implementation(libs.androidx.datastore.preferences) - // Kotlin Metadata (메타데이터 버전 호환성 해결) - // Kotlin 2.0+에서는 kotlin-metadata-jvm 사용 (kotlinx가 아님) - implementation("org.jetbrains.kotlin:kotlin-metadata-jvm:2.0.0") + implementation(libs.kotlin.metadata.jvm) // Firebase - implementation(platform("com.google.firebase:firebase-bom:34.7.0")) - implementation("com.google.firebase:firebase-analytics") + implementation(platform(libs.firebase.bom)) + implementation(libs.firebase.analytics) testImplementation(libs.junit) testImplementation(libs.mockk) @@ -101,35 +71,4 @@ dependencies { testImplementation(libs.kotlinx.coroutines.test) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) - androidTestImplementation(platform(libs.androidx.compose.bom)) - androidTestImplementation(libs.androidx.compose.ui.test.junit4) - debugImplementation(libs.androidx.compose.ui.tooling) - debugImplementation(libs.androidx.compose.ui.test.manifest) } - -// Detekt 설정 -detekt { - buildUponDefaultConfig = true - allRules = false - config.setFrom("$projectDir/../config/detekt.yml") - baseline = file("$projectDir/../config/detekt-baseline.xml") - - // Compose Rules는 나중에 추가 (의존성 문제 해결 후) - // dependencies { - // detektPlugins(libs.compose.rules.detekt) - // } -} - - -// 코드 품질 검사 통합 Task -tasks.register("codeQualityCheck") { - group = "verification" - description = "Runs all code quality checks (Detekt + Tests)" - - dependsOn("detekt", "test") - - doLast { - println("✅ All code quality checks completed!") - } -} - diff --git a/app/src/main/java/com/example/hackathon/HackathonApplication.kt b/app/src/main/java/com/example/hackathon/HackathonApplication.kt index 52179ad..03a3c9f 100644 --- a/app/src/main/java/com/example/hackathon/HackathonApplication.kt +++ b/app/src/main/java/com/example/hackathon/HackathonApplication.kt @@ -2,7 +2,7 @@ package com.example.hackathon import android.app.Application import com.example.hackathon.BuildConfig -import com.example.hackathon.data.local.DummyData +import com.example.hackathon.core.model.DummyData import dagger.hilt.android.HiltAndroidApp @HiltAndroidApp @@ -10,12 +10,15 @@ class HackathonApplication : Application() { override fun onCreate() { super.onCreate() - // 앱 시작 시 currentUser 초기화 - // Mock 모드가 아닐 때는 null로 시작 (로그인 안 됨) + // 앱 시작 시 currentUser 초기화: Mock 모드가 아닐 때만 null(비로그인)로 시작. + // + // USE_MOCK_API 는 app/build.gradle.kts 의 buildConfigField 로 주입되는 컴파일 상수다. + // 지금은 false 로 고정돼 있어 IDE 가 "!USE_MOCK_API 는 항상 true" 경고를 띄우지만, + // 이는 단일 빌드의 상수값만 보고 내린 판단이다. Mock/실 API 를 전환하는 빌드 플래그라 + // 값을 true 로 바꾸면 분기도 갈린다 → 의도된 동작이므로 경고는 무시 가능. if (!BuildConfig.USE_MOCK_API) { DummyData.currentUser = null } - // Mock 모드일 때도 명시적으로 null로 시작 (실제 로그인 시에만 설정) - // DummyData.currentUser = null + // Mock 모드일 때는 DummyData.currentUser 의 기본값을 그대로 둔다(명시 설정 안 함). } } diff --git a/app/src/main/java/com/example/hackathon/MainActivity.kt b/app/src/main/java/com/example/hackathon/MainActivity.kt index 7b2e120..b252b5d 100644 --- a/app/src/main/java/com/example/hackathon/MainActivity.kt +++ b/app/src/main/java/com/example/hackathon/MainActivity.kt @@ -31,7 +31,7 @@ import com.example.hackathon.presentation.navigation.BottomNavBar import com.example.hackathon.presentation.navigation.BottomNavItem import com.example.hackathon.presentation.navigation.NavTab import com.example.hackathon.presentation.route.Route -import com.example.hackathon.ui.theme.HackathonTheme +import com.example.hackathon.core.designsystem.theme.HackathonTheme import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject diff --git a/app/src/main/java/com/example/hackathon/presentation/navigation/BottomNavBar.kt b/app/src/main/java/com/example/hackathon/presentation/navigation/BottomNavBar.kt index f7a8f0d..0e68b48 100644 --- a/app/src/main/java/com/example/hackathon/presentation/navigation/BottomNavBar.kt +++ b/app/src/main/java/com/example/hackathon/presentation/navigation/BottomNavBar.kt @@ -18,8 +18,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.example.hackathon.core.util.noRippleClickable -import com.example.hackathon.ui.theme.HackathonTheme +import com.example.hackathon.core.designsystem.util.noRippleClickable +import com.example.hackathon.core.designsystem.theme.HackathonTheme import kotlin.enums.EnumEntries @Composable diff --git a/app/src/main/java/com/example/hackathon/presentation/navigation/NavGraph.kt b/app/src/main/java/com/example/hackathon/presentation/navigation/NavGraph.kt index 981072c..9fca437 100644 --- a/app/src/main/java/com/example/hackathon/presentation/navigation/NavGraph.kt +++ b/app/src/main/java/com/example/hackathon/presentation/navigation/NavGraph.kt @@ -15,18 +15,18 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.navArgument import com.example.hackathon.presentation.route.Route -import com.example.hackathon.presentation.screen.auth.LoginScreen -import com.example.hackathon.presentation.screen.auth.RegistrationScreen -import com.example.hackathon.presentation.screen.auth.RegistrationSuccessScreen -import com.example.hackathon.presentation.screen.combination.CreateCombinationScreen -import com.example.hackathon.presentation.screen.combination.DetailScreen -import com.example.hackathon.presentation.screen.home.HomeScreen -import com.example.hackathon.presentation.screen.home.SearchScreen -import com.example.hackathon.presentation.screen.onboarding.OnboardingScreen -import com.example.hackathon.presentation.screen.profile.EditProfileScreen -import com.example.hackathon.presentation.screen.profile.MyScreen +import com.example.hackathon.feature.auth.LoginScreen +import com.example.hackathon.feature.auth.RegistrationScreen +import com.example.hackathon.feature.auth.RegistrationSuccessScreen +import com.example.hackathon.feature.combination.CreateCombinationScreen +import com.example.hackathon.feature.combination.DetailScreen +import com.example.hackathon.feature.home.HomeScreen +import com.example.hackathon.feature.home.SearchScreen +import com.example.hackathon.feature.onboarding.OnboardingScreen +import com.example.hackathon.feature.profile.EditProfileScreen +import com.example.hackathon.feature.profile.MyScreen import com.example.hackathon.presentation.viewmodel.MainViewModel -import com.example.hackathon.presentation.viewmodel.MyPageViewModel +import com.example.hackathon.feature.profile.MyPageViewModel @Composable fun AppNavGraph( diff --git a/app/src/main/java/com/example/hackathon/presentation/navigation/NavTab.kt b/app/src/main/java/com/example/hackathon/presentation/navigation/NavTab.kt index 9ed0b64..2b5f907 100644 --- a/app/src/main/java/com/example/hackathon/presentation/navigation/NavTab.kt +++ b/app/src/main/java/com/example/hackathon/presentation/navigation/NavTab.kt @@ -1,7 +1,7 @@ package com.example.hackathon.presentation.navigation import androidx.compose.material.icons.filled.Home -import com.example.hackathon.R +import com.example.hackathon.core.designsystem.R import com.example.hackathon.presentation.route.Route enum class NavTab( diff --git a/app/src/main/java/com/example/hackathon/presentation/viewmodel/MainViewModel.kt b/app/src/main/java/com/example/hackathon/presentation/viewmodel/MainViewModel.kt index 9b26db2..23e149b 100644 --- a/app/src/main/java/com/example/hackathon/presentation/viewmodel/MainViewModel.kt +++ b/app/src/main/java/com/example/hackathon/presentation/viewmodel/MainViewModel.kt @@ -3,8 +3,8 @@ package com.example.hackathon.presentation.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.example.hackathon.BuildConfig -import com.example.hackathon.domain.repository.AuthRepository -import com.example.hackathon.data.local.TokenManager +import com.example.hackathon.core.domain.repository.AuthRepository +import com.example.hackathon.core.datastore.TokenManager import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts new file mode 100644 index 0000000..fe9169f --- /dev/null +++ b/build-logic/build.gradle.kts @@ -0,0 +1,48 @@ +plugins { + `kotlin-dsl` +} + +group = "com.example.hackathon.buildlogic" + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +dependencies { + compileOnly(libs.android.gradlePlugin) + compileOnly(libs.kotlin.gradlePlugin) +} + +gradlePlugin { + plugins { + register("androidApplication") { + id = "hackathon.android.application" + implementationClass = "AndroidApplicationConventionPlugin" + } + register("androidApplicationCompose") { + id = "hackathon.android.application.compose" + implementationClass = "AndroidApplicationComposeConventionPlugin" + } + register("androidLibrary") { + id = "hackathon.android.library" + implementationClass = "AndroidLibraryConventionPlugin" + } + register("androidLibraryCompose") { + id = "hackathon.android.library.compose" + implementationClass = "AndroidLibraryComposeConventionPlugin" + } + register("androidHilt") { + id = "hackathon.android.hilt" + implementationClass = "AndroidHiltConventionPlugin" + } + register("androidFeature") { + id = "hackathon.android.feature" + implementationClass = "AndroidFeatureConventionPlugin" + } + register("kotlinLibrary") { + id = "hackathon.kotlin.library" + implementationClass = "KotlinLibraryConventionPlugin" + } + } +} diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts new file mode 100644 index 0000000..011684d --- /dev/null +++ b/build-logic/settings.gradle.kts @@ -0,0 +1,14 @@ +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } +} + +rootProject.name = "build-logic" diff --git a/build-logic/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt b/build-logic/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt new file mode 100644 index 0000000..173d1f0 --- /dev/null +++ b/build-logic/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt @@ -0,0 +1,20 @@ +import com.android.build.api.dsl.ApplicationExtension +import com.example.hackathon.buildlogic.configureAndroidCompose +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.getByType + +/** + * Compose 를 사용하는 :app 모듈 플러그인. + * hackathon.android.application 적용을 전제로 Compose 설정만 덧붙인다. + */ +class AndroidApplicationComposeConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + pluginManager.apply("org.jetbrains.kotlin.plugin.compose") + + val extension = extensions.getByType() + configureAndroidCompose(extension) + } + } +} diff --git a/build-logic/src/main/kotlin/AndroidApplicationConventionPlugin.kt b/build-logic/src/main/kotlin/AndroidApplicationConventionPlugin.kt new file mode 100644 index 0000000..2140a00 --- /dev/null +++ b/build-logic/src/main/kotlin/AndroidApplicationConventionPlugin.kt @@ -0,0 +1,27 @@ +import com.android.build.api.dsl.ApplicationExtension +import com.example.hackathon.buildlogic.configureKotlinAndroid +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure + +/** + * 안드로이드 애플리케이션(:app) 모듈 공통 플러그인. + * com.android.application + Kotlin 적용 + 공통 SDK/Java/Kotlin 설정 + targetSdk. + */ +class AndroidApplicationConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + with(pluginManager) { + apply("com.android.application") + apply("org.jetbrains.kotlin.android") + } + + extensions.configure { + configureKotlinAndroid(this) + defaultConfig.targetSdk = 36 + defaultConfig.testInstrumentationRunner = + "androidx.test.runner.AndroidJUnitRunner" + } + } + } +} diff --git a/build-logic/src/main/kotlin/AndroidFeatureConventionPlugin.kt b/build-logic/src/main/kotlin/AndroidFeatureConventionPlugin.kt new file mode 100644 index 0000000..9c83b97 --- /dev/null +++ b/build-logic/src/main/kotlin/AndroidFeatureConventionPlugin.kt @@ -0,0 +1,42 @@ +import com.example.hackathon.buildlogic.libs +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.project + +/** + * 피처(:feature:*) 모듈 공통 플러그인. + * library + compose + hilt 를 묶고, 모든 피처가 공유하는 core 모듈(domain/model/designsystem)과 + * Compose/Navigation/Hilt-Navigation/Lifecycle/Coil 의존성을 일괄 주입한다. + */ +class AndroidFeatureConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + with(pluginManager) { + apply("hackathon.android.library") + apply("hackathon.android.library.compose") + apply("hackathon.android.hilt") + } + + dependencies { + add("implementation", project(":core:domain")) + add("implementation", project(":core:model")) + add("implementation", project(":core:designsystem")) + + add("implementation", libs.findLibrary("androidx-core-ktx").get()) + add("implementation", libs.findLibrary("androidx-lifecycle-runtime-ktx").get()) + add("implementation", libs.findLibrary("androidx-activity-compose").get()) + add("implementation", libs.findLibrary("androidx-compose-material-icons-extended").get()) + add("implementation", libs.findLibrary("androidx-navigation-compose").get()) + add("implementation", libs.findLibrary("hilt-navigation-compose").get()) + add("implementation", libs.findLibrary("coil-compose").get()) + add("implementation", libs.findLibrary("coil-network-okhttp").get()) + + add("testImplementation", libs.findLibrary("junit").get()) + add("testImplementation", libs.findLibrary("mockk").get()) + add("testImplementation", libs.findLibrary("turbine").get()) + add("testImplementation", libs.findLibrary("kotlinx-coroutines-test").get()) + } + } + } +} diff --git a/build-logic/src/main/kotlin/AndroidHiltConventionPlugin.kt b/build-logic/src/main/kotlin/AndroidHiltConventionPlugin.kt new file mode 100644 index 0000000..90cd8e4 --- /dev/null +++ b/build-logic/src/main/kotlin/AndroidHiltConventionPlugin.kt @@ -0,0 +1,24 @@ +import com.example.hackathon.buildlogic.libs +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.dependencies + +/** + * Hilt + KSP 공통 플러그인. + * dagger.hilt.android + ksp 적용 + hilt-android / hilt-compiler 의존성 주입. + */ +class AndroidHiltConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + with(pluginManager) { + apply("com.google.devtools.ksp") + apply("com.google.dagger.hilt.android") + } + + dependencies { + add("implementation", libs.findLibrary("hilt-android").get()) + add("ksp", libs.findLibrary("hilt-compiler").get()) + } + } + } +} diff --git a/build-logic/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt b/build-logic/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt new file mode 100644 index 0000000..4d0c7b1 --- /dev/null +++ b/build-logic/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt @@ -0,0 +1,23 @@ +import com.android.build.api.dsl.LibraryExtension +import com.example.hackathon.buildlogic.configureAndroidCompose +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure + +/** + * Compose 를 사용하는 안드로이드 라이브러리 모듈 플러그인. + * hackathon.android.library 적용을 전제로 Compose 설정만 덧붙인다. + */ +class AndroidLibraryComposeConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + with(pluginManager) { + apply("hackathon.android.library") + apply("org.jetbrains.kotlin.plugin.compose") + } + + val extension = extensions.getByType(LibraryExtension::class.java) + configureAndroidCompose(extension) + } + } +} diff --git a/build-logic/src/main/kotlin/AndroidLibraryConventionPlugin.kt b/build-logic/src/main/kotlin/AndroidLibraryConventionPlugin.kt new file mode 100644 index 0000000..589d981 --- /dev/null +++ b/build-logic/src/main/kotlin/AndroidLibraryConventionPlugin.kt @@ -0,0 +1,27 @@ +import com.android.build.api.dsl.LibraryExtension +import com.example.hackathon.buildlogic.configureKotlinAndroid +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.dependencies + +/** + * 안드로이드 라이브러리 모듈 공통 플러그인. + * com.android.library + Kotlin 적용 + 공통 SDK/Java/Kotlin 설정. + */ +class AndroidLibraryConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + with(pluginManager) { + apply("com.android.library") + apply("org.jetbrains.kotlin.android") + } + + extensions.configure { + configureKotlinAndroid(this) + defaultConfig.testInstrumentationRunner = + "androidx.test.runner.AndroidJUnitRunner" + } + } + } +} diff --git a/build-logic/src/main/kotlin/KotlinLibraryConventionPlugin.kt b/build-logic/src/main/kotlin/KotlinLibraryConventionPlugin.kt new file mode 100644 index 0000000..fb268bc --- /dev/null +++ b/build-logic/src/main/kotlin/KotlinLibraryConventionPlugin.kt @@ -0,0 +1,31 @@ +import com.example.hackathon.buildlogic.libs +import org.gradle.api.JavaVersion +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.kotlin.dsl.configure +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension + +/** + * 순수 Kotlin(JVM) 라이브러리 모듈 플러그인 — Android 의존 없는 :core:model 등에 사용. + * org.jetbrains.kotlin.jvm 적용 + Java/Kotlin 17. + */ +class KotlinLibraryConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + pluginManager.apply("org.jetbrains.kotlin.jvm") + + extensions.configure { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + extensions.configure { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } + } + } + } +} diff --git a/build-logic/src/main/kotlin/com/example/hackathon/buildlogic/AndroidCompose.kt b/build-logic/src/main/kotlin/com/example/hackathon/buildlogic/AndroidCompose.kt new file mode 100644 index 0000000..ce7438a --- /dev/null +++ b/build-logic/src/main/kotlin/com/example/hackathon/buildlogic/AndroidCompose.kt @@ -0,0 +1,37 @@ +package com.example.hackathon.buildlogic + +import com.android.build.api.dsl.CommonExtension +import org.gradle.api.Project +import org.gradle.api.artifacts.VersionCatalog +import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.getByType + +/** + * Jetpack Compose 공통 설정. + * buildFeatures.compose 활성화 + Compose BOM 기반 의존성을 모듈에 주입한다. + * org.jetbrains.kotlin.plugin.compose 플러그인은 각 Compose 모듈에서 별도 적용한다. + */ +internal fun Project.configureAndroidCompose( + commonExtension: CommonExtension<*, *, *, *, *, *>, +) { + commonExtension.apply { + buildFeatures { + compose = true + } + } + + dependencies { + val bom = libs.findLibrary("androidx-compose-bom").get() + add("implementation", platform(bom)) + add("androidTestImplementation", platform(bom)) + + add("implementation", libs.findLibrary("androidx-compose-ui").get()) + add("implementation", libs.findLibrary("androidx-compose-ui-graphics").get()) + add("implementation", libs.findLibrary("androidx-compose-ui-tooling-preview").get()) + add("implementation", libs.findLibrary("androidx-compose-material3").get()) + + add("debugImplementation", libs.findLibrary("androidx-compose-ui-tooling").get()) + add("debugImplementation", libs.findLibrary("androidx-compose-ui-test-manifest").get()) + add("androidTestImplementation", libs.findLibrary("androidx-compose-ui-test-junit4").get()) + } +} diff --git a/build-logic/src/main/kotlin/com/example/hackathon/buildlogic/KotlinAndroid.kt b/build-logic/src/main/kotlin/com/example/hackathon/buildlogic/KotlinAndroid.kt new file mode 100644 index 0000000..5a4dcc5 --- /dev/null +++ b/build-logic/src/main/kotlin/com/example/hackathon/buildlogic/KotlinAndroid.kt @@ -0,0 +1,40 @@ +package com.example.hackathon.buildlogic + +import com.android.build.api.dsl.CommonExtension +import org.gradle.api.JavaVersion +import org.gradle.api.Project +import org.gradle.kotlin.dsl.dependencies +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension + +/** + * Android Application/Library 모듈 공통 SDK·Java·Kotlin 설정. + * compileSdk 36 / minSdk 36 / JVM 17 을 단일 지점에서 강제한다. + */ +internal fun Project.configureKotlinAndroid( + commonExtension: CommonExtension<*, *, *, *, *, *>, +) { + commonExtension.apply { + compileSdk = 36 + + defaultConfig { + minSdk = 36 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + } + + configureKotlin() +} + +/** Kotlin JVM 타깃을 17 로 통일. */ +private fun Project.configureKotlin() { + extensions.configure(KotlinAndroidProjectExtension::class.java) { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } + } +} diff --git a/build-logic/src/main/kotlin/com/example/hackathon/buildlogic/ProjectExtensions.kt b/build-logic/src/main/kotlin/com/example/hackathon/buildlogic/ProjectExtensions.kt new file mode 100644 index 0000000..b16c534 --- /dev/null +++ b/build-logic/src/main/kotlin/com/example/hackathon/buildlogic/ProjectExtensions.kt @@ -0,0 +1,13 @@ +package com.example.hackathon.buildlogic + +import org.gradle.api.Project +import org.gradle.api.artifacts.VersionCatalog +import org.gradle.api.artifacts.VersionCatalogsExtension +import org.gradle.kotlin.dsl.getByType + +/** + * 컨벤션 플러그인에서 `libs` 버전 카탈로그에 접근하기 위한 헬퍼. + * 플러그인 코드에서는 일반 accessor(libs.xxx)를 쓸 수 없어 명시적으로 카탈로그를 조회한다. + */ +internal val Project.libs: VersionCatalog + get() = extensions.getByType().named("libs") diff --git a/build.gradle.kts b/build.gradle.kts index 472160b..6d0aeb3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,10 +1,13 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. +// 모든 플러그인을 apply false 로 선언해 버전을 전 서브프로젝트(+build-logic 컨벤션 플러그인)에 공급한다. plugins { alias(libs.plugins.android.application) apply false + alias(libs.plugins.android.library) apply false alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.kotlin.jvm) apply false alias(libs.plugins.kotlin.compose) apply false + alias(libs.plugins.kotlin.serialization) apply false alias(libs.plugins.hilt) apply false alias(libs.plugins.ksp) apply false - alias(libs.plugins.detekt) apply false alias(libs.plugins.google.services) apply false -} \ No newline at end of file +} diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts new file mode 100644 index 0000000..e36b4a3 --- /dev/null +++ b/core/data/build.gradle.kts @@ -0,0 +1,26 @@ +plugins { + id("hackathon.android.library") + id("hackathon.android.hilt") +} + +android { + namespace = "com.example.hackathon.core.data" + + buildFeatures { + buildConfig = true + } + + defaultConfig { + // repositoryimpl 의 Mock/실 API 분기용 (BuildConfig.USE_MOCK_API) + buildConfigField("boolean", "USE_MOCK_API", "false") + } +} + +dependencies { + implementation(project(":core:model")) + implementation(project(":core:domain")) + implementation(project(":core:network")) + implementation(project(":core:datastore")) + + implementation(libs.retrofit) +} diff --git a/core/data/src/main/AndroidManifest.xml b/core/data/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8072ee0 --- /dev/null +++ b/core/data/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/core/datastore/build.gradle.kts b/core/datastore/build.gradle.kts new file mode 100644 index 0000000..7d64dd2 --- /dev/null +++ b/core/datastore/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + id("hackathon.android.library") + id("hackathon.android.hilt") +} + +android { + namespace = "com.example.hackathon.core.datastore" +} + +dependencies { + implementation(libs.androidx.datastore.preferences) +} diff --git a/core/datastore/src/main/AndroidManifest.xml b/core/datastore/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8072ee0 --- /dev/null +++ b/core/datastore/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/core/designsystem/build.gradle.kts b/core/designsystem/build.gradle.kts new file mode 100644 index 0000000..9db114a --- /dev/null +++ b/core/designsystem/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + id("hackathon.android.library") + id("hackathon.android.library.compose") +} + +android { + namespace = "com.example.hackathon.core.designsystem" +} + +dependencies { + implementation(project(":core:model")) + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.material.icons.extended) + implementation(libs.androidx.compose.foundation.layout) + implementation(libs.coil.compose) + implementation(libs.coil.network.okhttp) +} diff --git a/core/designsystem/src/main/AndroidManifest.xml b/core/designsystem/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8072ee0 --- /dev/null +++ b/core/designsystem/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/core/domain/build.gradle.kts b/core/domain/build.gradle.kts new file mode 100644 index 0000000..5627f1d --- /dev/null +++ b/core/domain/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("hackathon.android.library") +} + +android { + namespace = "com.example.hackathon.core.domain" +} + +dependencies { + // repository 인터페이스의 공개 시그니처가 model/network 타입을 노출하므로 api 로 전파. + api(project(":core:model")) + // CombinationRepository 가 파라미터로 android.net.Uri 와 일부 DTO 를 직접 다뤄 network/android 의존. + // TODO: 도메인 순수화(Uri·DTO 를 도메인 모델로 치환) 후 :core:network 의존 제거 — 별도 작업. + api(project(":core:network")) +} diff --git a/core/domain/src/main/AndroidManifest.xml b/core/domain/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8072ee0 --- /dev/null +++ b/core/domain/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/core/model/build.gradle.kts b/core/model/build.gradle.kts new file mode 100644 index 0000000..b750464 --- /dev/null +++ b/core/model/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + id("hackathon.android.library") +} + +android { + namespace = "com.example.hackathon.core.model" +} diff --git a/core/model/src/main/AndroidManifest.xml b/core/model/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8072ee0 --- /dev/null +++ b/core/model/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts new file mode 100644 index 0000000..93818ca --- /dev/null +++ b/core/network/build.gradle.kts @@ -0,0 +1,29 @@ +plugins { + id("hackathon.android.library") + id("hackathon.android.hilt") + alias(libs.plugins.kotlin.serialization) +} + +android { + namespace = "com.example.hackathon.core.network" + + buildFeatures { + buildConfig = true + } + + defaultConfig { + buildConfigField("boolean", "USE_MOCK_API", "false") + buildConfigField("String", "BASE_URL", "\"http://13.125.27.133/\"") + } +} + +dependencies { + implementation(project(":core:model")) + implementation(project(":core:datastore")) + + implementation(libs.okhttp) + implementation(libs.okhttp.logging.interceptor) + implementation(libs.retrofit) + implementation(libs.retrofit.kotlinx.serialization) + implementation(libs.kotlinx.serialization.json) +} diff --git a/core/network/src/main/AndroidManifest.xml b/core/network/src/main/AndroidManifest.xml new file mode 100644 index 0000000..19d2638 --- /dev/null +++ b/core/network/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/feature/auth/build.gradle.kts b/feature/auth/build.gradle.kts new file mode 100644 index 0000000..61ba392 --- /dev/null +++ b/feature/auth/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + id("hackathon.android.feature") +} + +android { + namespace = "com.example.hackathon.feature.auth" +} diff --git a/feature/auth/src/main/AndroidManifest.xml b/feature/auth/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8072ee0 --- /dev/null +++ b/feature/auth/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/feature/combination/build.gradle.kts b/feature/combination/build.gradle.kts new file mode 100644 index 0000000..f5202c7 --- /dev/null +++ b/feature/combination/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + id("hackathon.android.feature") +} + +android { + namespace = "com.example.hackathon.feature.combination" +} diff --git a/feature/combination/src/main/AndroidManifest.xml b/feature/combination/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8072ee0 --- /dev/null +++ b/feature/combination/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/feature/home/build.gradle.kts b/feature/home/build.gradle.kts new file mode 100644 index 0000000..15ebf7c --- /dev/null +++ b/feature/home/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + id("hackathon.android.feature") +} + +android { + namespace = "com.example.hackathon.feature.home" +} diff --git a/feature/home/src/main/AndroidManifest.xml b/feature/home/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8072ee0 --- /dev/null +++ b/feature/home/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/feature/onboarding/build.gradle.kts b/feature/onboarding/build.gradle.kts new file mode 100644 index 0000000..eefe235 --- /dev/null +++ b/feature/onboarding/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + id("hackathon.android.feature") +} + +android { + namespace = "com.example.hackathon.feature.onboarding" +} diff --git a/feature/onboarding/src/main/AndroidManifest.xml b/feature/onboarding/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8072ee0 --- /dev/null +++ b/feature/onboarding/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/feature/profile/build.gradle.kts b/feature/profile/build.gradle.kts new file mode 100644 index 0000000..6ed76df --- /dev/null +++ b/feature/profile/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + id("hackathon.android.feature") +} + +android { + namespace = "com.example.hackathon.feature.profile" +} diff --git a/feature/profile/src/main/AndroidManifest.xml b/feature/profile/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8072ee0 --- /dev/null +++ b/feature/profile/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5c14d6c..ce44fde 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,15 +18,17 @@ hilt = "2.56.1" hiltNavigationCompose = "1.2.0" hiltManager = "1.0.0" ksp = "2.0.21-1.0.28" -detekt = "1.23.8" composeRules = "1.2.2" mockk = "1.13.8" turbine = "1.0.0" kotlinxCoroutinesTest = "1.7.3" googleServices = "4.4.4" +# 34.14.0 은 play-services-measurement 가 Kotlin metadata 2.2.0 으로 빌드돼 본 프로젝트 Kotlin 2.0.21 과 비호환. +# (IDE 의 "newer version available" 경고는 호환성 미고려 → kotlin 2.1+ 업그레이드 전까지 34.7.0 고정) firebaseBom = "34.7.0" datastore = "1.1.1" foundationLayout = "1.10.0" +coreSplashscreen = "1.2.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -55,22 +57,30 @@ coil-network-okhttp = { group = "io.coil-kt.coil3", name = "coil-network-okhttp" hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" } hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigationCompose" } -compose-rules-detekt = { group = "com.twitter.compose.rules", name = "detekt", version.ref = "composeRules" } mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } turbine = { group = "app.cash.turbine", name = "turbine", version.ref = "turbine" } kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutinesTest" } firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" } -firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics-ktx" } +firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics" } androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" } androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "foundationLayout" } +androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "coreSplashscreen" } +# kotlin-metadata-jvm 은 kotlin 컴파일러와 동일 버전 유지 (stdlib transitive 충돌 방지) — version.ref = "kotlin" +kotlin-metadata-jvm = { group = "org.jetbrains.kotlin", name = "kotlin-metadata-jvm", version.ref = "kotlin" } + +# build-logic 컨벤션 플러그인이 Gradle 플러그인 API 에 접근하기 위한 의존성 (compileOnly) +android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "agp" } +kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } +ksp-gradlePlugin = { group = "com.google.devtools.ksp", name = "symbol-processing-gradle-plugin", version.ref = "ksp" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } +android-library = { id = "com.android.library", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } -detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } google-services = { id = "com.google.gms.google-services", version.ref = "googleServices" } diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/settings.gradle.kts b/settings.gradle.kts index b50f2de..df1d907 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,4 +1,5 @@ pluginManagement { + includeBuild("build-logic") repositories { google { content { @@ -21,4 +22,18 @@ dependencyResolutionManagement { rootProject.name = "Hackathon" include(":app") - \ No newline at end of file + +// core 모듈 +include(":core:model") +include(":core:designsystem") +include(":core:datastore") +include(":core:network") +include(":core:domain") +include(":core:data") + +// feature 모듈 +include(":feature:auth") +include(":feature:home") +include(":feature:combination") +include(":feature:onboarding") +include(":feature:profile") From 56f39e34ed22a2b98eea1ad7ebf0dbdf99dd2512 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=BC=ED=98=81?= Date: Sun, 31 May 2026 16:22:07 +0900 Subject: [PATCH 3/4] =?UTF-8?q?refactor:=20=EC=A0=84=EC=B2=B4=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=EC=A0=9D=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=ED=8F=AC?= =?UTF-8?q?=EB=A7=B7=ED=8C=85=20=EB=B0=8F=20=EC=9D=B8=ED=94=84=EB=9D=BC/AP?= =?UTF-8?q?I=20=EC=97=B0=EB=8F=99=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 프로젝트 전반에 ktlint 스타일(android_studio) 적용 및 대규모 코드 포맷팅 수행 - GitHub Actions CI 환경에서 google-services.json stub 생성 로직 추가 (Secret 의존성 제거) - TokenManager에 JWT Payload에서 userId를 추출하는 기능 구현 및 Repository 연동 - AuthInterceptor의 토큰 재발급(Reissue) 및 401 Unauthorized 에러 처리 로직 개선 - 레시피 등록/수정/삭제 시 실제 API 호출 및 이미지 Base64 인코딩 처리 로직 추가 - Navigation 및 Scaffold 하단 바 노출 조건 정교화 (로그인, 등록, 상세 페이지 등) - Repository 및 ViewModel 내 Mock API 처리와 실제 서버 API 호출 로직 정비 - Compose UI 컴포넌트 내 불필요한 import 정리 및 스타일 가이드 준수 --- .editorconfig | 4 +- .github/workflows/lint.yml | 50 +- .github/workflows/unit-test.yml | 51 +- .../example/hackathon/HackathonApplication.kt | 2 +- .../com/example/hackathon/MainActivity.kt | 54 +- .../presentation/navigation/BottomNavBar.kt | 19 +- .../presentation/navigation/BottomNavItem.kt | 6 +- .../presentation/navigation/NavGraph.kt | 37 +- .../presentation/navigation/NavTab.kt | 12 +- .../hackathon/presentation/route/Route.kt | 4 +- .../presentation/viewmodel/MainViewModel.kt | 44 +- .../kotlin/AndroidFeatureConventionPlugin.kt | 5 +- .../hackathon/buildlogic/AndroidCompose.kt | 4 +- .../hackathon/buildlogic/KotlinAndroid.kt | 4 +- .../core/data/di/RepositoryModule.kt | 4 +- .../core/data/mapper/CombinationMapper.kt | 46 +- .../core/data/mapper/RecipeMapper.kt | 178 ++-- .../data/repositoryimpl/AuthRepositoryImpl.kt | 362 ++++--- .../CombinationRepositoryImpl.kt | 911 +++++++++--------- .../repositoryimpl/RecipeRepositoryImpl.kt | 222 +++-- .../data/repositoryimpl/UserRepositoryImpl.kt | 145 +-- .../hackathon/core/datastore/TokenManager.kt | 158 +-- .../designsystem/component/CategoryChip.kt | 16 +- .../designsystem/component/CombinationCard.kt | 102 +- .../designsystem/component/IngredientRow.kt | 11 +- .../core/designsystem/component/SearchBar.kt | 10 +- .../core/designsystem/component/TagChip.kt | 20 +- .../designsystem/component/TopAppLogoBar.kt | 9 +- .../core/designsystem/theme/Color.kt | 4 +- .../core/designsystem/theme/Theme.kt | 6 +- .../hackathon/core/designsystem/theme/Type.kt | 30 +- .../core/designsystem/util/ModifierExt.kt | 40 +- .../core/domain/repository/AuthRepository.kt | 30 +- .../repository/CombinationRepository.kt | 10 +- .../core/domain/repository/UserRepository.kt | 5 +- .../example/hackathon/core/model/Category.kt | 2 +- .../hackathon/core/model/Combination.kt | 2 +- .../example/hackathon/core/model/DummyData.kt | 60 +- .../hackathon/core/model/RecipeDetail.kt | 16 +- .../hackathon/core/model/RecipeRanking.kt | 7 +- .../com/example/hackathon/core/model/User.kt | 6 +- .../hackathon/core/network/AuthInterceptor.kt | 128 +-- .../hackathon/core/network/di/ApiModule.kt | 14 +- .../core/network/di/NetworkModule.kt | 32 +- .../dto/request/CreateCombinationRequest.kt | 2 +- .../core/network/dto/request/LoginRequest.kt | 7 +- .../core/network/dto/request/LogoutRequest.kt | 4 +- .../network/dto/request/RegisterRequest.kt | 6 +- .../network/dto/request/ReissueRequest.kt | 4 +- .../core/network/dto/request/SignupRequest.kt | 7 +- .../dto/request/UpdateCombinationRequest.kt | 2 +- .../dto/request/UpdateProfileRequest.kt | 5 +- .../core/network/dto/response/BaseResponse.kt | 6 +- .../dto/response/CombinationResponse.kt | 2 +- .../network/dto/response/DetailResponse.kt | 2 +- .../network/dto/response/LoginResponse.kt | 2 +- .../dto/response/MyPageRecipeResponse.kt | 2 +- .../network/dto/response/PostPreviewDto.kt | 8 +- .../dto/response/RecipeDetailResponse.kt | 19 +- .../dto/response/RecipeRankingResponse.kt | 4 +- .../network/dto/response/ReissueResponse.kt | 3 +- .../network/dto/response/SignupResponse.kt | 5 +- .../dto/response/UserProfileResponse.kt | 2 +- .../core/network/dto/response/UserResponse.kt | 6 +- .../core/network/service/AuthService.kt | 16 +- .../network/service/CombinationService.kt | 30 +- .../core/network/service/RecipeService.kt | 18 +- .../core/network/service/UserService.kt | 4 +- .../hackathon/feature/auth/LoginScreen.kt | 81 +- .../hackathon/feature/auth/LoginViewModel.kt | 168 ++-- .../feature/auth/RegistrationScreen.kt | 86 +- .../feature/auth/RegistrationSuccessScreen.kt | 21 +- .../feature/auth/RegistrationViewModel.kt | 125 +-- .../combination/CreateCombinationScreen.kt | 102 +- .../combination/CreateCombinationViewModel.kt | 211 ++-- .../feature/combination/DetailScreen.kt | 110 +-- .../feature/combination/DetailViewModel.kt | 160 +-- .../component/HashTagInputSection.kt | 31 +- .../component/ImageUploadSection.kt | 75 +- .../component/IngredientInputSection.kt | 31 +- .../component/VisibilitySelectionSection.kt | 24 +- .../CreateCombinationViewModelTest.kt | 280 +++--- .../feature/combination/MainDispatcherRule.kt | 5 +- .../feature/home/HomeCombinationList.kt | 27 +- .../hackathon/feature/home/HomeContent.kt | 46 +- .../hackathon/feature/home/HomeScreen.kt | 39 +- .../feature/home/HomeStateContent.kt | 12 +- .../hackathon/feature/home/HomeTopBar.kt | 27 +- .../hackathon/feature/home/HomeViewModel.kt | 304 +++--- .../hackathon/feature/home/SearchScreen.kt | 43 +- .../hackathon/feature/home/SearchViewModel.kt | 191 ++-- .../feature/home/component/CombinationList.kt | 12 +- .../feature/home/component/FilterBar.kt | 12 +- .../feature/home/component/SearchComponent.kt | 29 +- .../feature/onboarding/OnboardingScreen.kt | 19 +- .../feature/profile/EditProfileScreen.kt | 66 +- .../feature/profile/EditProfileViewModel.kt | 98 +- .../feature/profile/MyPageViewModel.kt | 251 ++--- .../hackathon/feature/profile/MyScreen.kt | 68 +- 99 files changed, 2897 insertions(+), 2905 deletions(-) diff --git a/.editorconfig b/.editorconfig index a2751be..8591ac0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,6 +10,9 @@ insert_final_newline = true trim_trailing_whitespace = true [*.{kt,kts}] +# CI(ScaCap/action-ktlint, android: true)와 동일한 Android 스타일 강제 +ktlint_code_style = android_studio + # Composable 함수는 대문자 시작 허용 (PascalCase) ktlint_function_naming_ignore_when_annotated_with = Composable @@ -18,4 +21,3 @@ ktlint_standard_no-wildcard-imports = disabled [*.md] trim_trailing_whitespace = false - diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1516be8..ea0fec1 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -40,26 +40,40 @@ jobs: java-version: '17' cache: 'gradle' - # fail-fast — secret 누락 시 Gradle 까지 가지 않고 즉시 실패. - # secret 값은 echo 하지 않음 (env 로만 받아서 길이 검사). 누락 키 이름은 안전. - - name: Verify required secrets are set - env: - GOOGLE_SERVICES_JSON_B64: ${{ secrets.GOOGLE_SERVICES_JSON_B64 }} - run: | - missing="" - [ -z "$GOOGLE_SERVICES_JSON_B64" ] && missing="$missing GOOGLE_SERVICES_JSON_B64" - if [ -n "$missing" ]; then - echo "::error::필수 secret 누락:$missing — Settings → Secrets and variables → Actions 에 등록" - exit 1 - fi - # google-services plugin 이 app/google-services.json 을 요구. 파일은 .gitignore 라 - # CI 환경엔 없음. base64 인코딩된 secret 으로 받아서 decode. - - name: Decode google-services.json - env: - GOOGLE_SERVICES_JSON_B64: ${{ secrets.GOOGLE_SERVICES_JSON_B64 }} + # CI 환경엔 없음. Android Lint 는 실제 Firebase 가 불필요하므로 stub JSON 생성 + # (plugin 통과 목적, secret 비의존). + - name: Create stub google-services.json (CI only) run: | - echo "$GOOGLE_SERVICES_JSON_B64" | base64 -d > app/google-services.json + cat > app/google-services.json <<'EOF' + { + "project_info": { + "project_number": "000000000000", + "project_id": "hackathon-ci-stub", + "storage_bucket": "hackathon-ci-stub.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:000000000000:android:0000000000000000000000", + "android_client_info": { + "package_name": "com.example.hackathon" + } + }, + "oauth_client": [], + "api_key": [ + { "current_key": "AIzaSyDfake_ci_only_no_real_value_xxxxxxxxxxx" } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" + } + EOF - name: Run Android Lint run: ./gradlew lintDebug --continue --parallel diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 4568db2..12767b4 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -25,26 +25,39 @@ jobs: java-version: '17' cache: 'gradle' - # fail-fast — secret 누락 시 Gradle 까지 가지 않고 즉시 실패. - # secret 값은 echo 하지 않음 (env 로만 받아서 길이 검사). 누락 키 이름은 안전. - - name: Verify required secrets are set - env: - GOOGLE_SERVICES_JSON_B64: ${{ secrets.GOOGLE_SERVICES_JSON_B64 }} + # google-services plugin 이 app/google-services.json 을 요구. 파일은 .gitignore 라 CI 엔 없음. + # unit test 는 실제 Firebase 가 불필요하므로 stub JSON 생성 (plugin 통과 목적, secret 비의존). + - name: Create stub google-services.json (CI only) run: | - missing="" - [ -z "$GOOGLE_SERVICES_JSON_B64" ] && missing="$missing GOOGLE_SERVICES_JSON_B64" - if [ -n "$missing" ]; then - echo "::error::필수 secret 누락:$missing — Settings → Secrets and variables → Actions 에 등록" - exit 1 - fi - - # google-services plugin 이 app/google-services.json 을 요구. 파일은 .gitignore 라 - # CI 환경엔 없음. base64 인코딩된 secret 으로 받아서 decode. - - name: Decode google-services.json - env: - GOOGLE_SERVICES_JSON_B64: ${{ secrets.GOOGLE_SERVICES_JSON_B64 }} - run: | - echo "$GOOGLE_SERVICES_JSON_B64" | base64 -d > app/google-services.json + cat > app/google-services.json <<'EOF' + { + "project_info": { + "project_number": "000000000000", + "project_id": "hackathon-ci-stub", + "storage_bucket": "hackathon-ci-stub.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:000000000000:android:0000000000000000000000", + "android_client_info": { + "package_name": "com.example.hackathon" + } + }, + "oauth_client": [], + "api_key": [ + { "current_key": "AIzaSyDfake_ci_only_no_real_value_xxxxxxxxxxx" } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" + } + EOF - name: Run unit tests run: ./gradlew testDebugUnitTest --continue --parallel diff --git a/app/src/main/java/com/example/hackathon/HackathonApplication.kt b/app/src/main/java/com/example/hackathon/HackathonApplication.kt index 03a3c9f..84e6e85 100644 --- a/app/src/main/java/com/example/hackathon/HackathonApplication.kt +++ b/app/src/main/java/com/example/hackathon/HackathonApplication.kt @@ -9,7 +9,7 @@ import dagger.hilt.android.HiltAndroidApp class HackathonApplication : Application() { override fun onCreate() { super.onCreate() - + // 앱 시작 시 currentUser 초기화: Mock 모드가 아닐 때만 null(비로그인)로 시작. // // USE_MOCK_API 는 app/build.gradle.kts 의 buildConfigField 로 주입되는 컴파일 상수다. diff --git a/app/src/main/java/com/example/hackathon/MainActivity.kt b/app/src/main/java/com/example/hackathon/MainActivity.kt index b252b5d..bfe8fed 100644 --- a/app/src/main/java/com/example/hackathon/MainActivity.kt +++ b/app/src/main/java/com/example/hackathon/MainActivity.kt @@ -26,18 +26,17 @@ import androidx.compose.ui.unit.dp import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController +import com.example.hackathon.core.designsystem.theme.HackathonTheme import com.example.hackathon.presentation.navigation.AppNavGraph import com.example.hackathon.presentation.navigation.BottomNavBar import com.example.hackathon.presentation.navigation.BottomNavItem import com.example.hackathon.presentation.navigation.NavTab import com.example.hackathon.presentation.route.Route -import com.example.hackathon.core.designsystem.theme.HackathonTheme import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @AndroidEntryPoint class MainActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { val splashScreen = installSplashScreen() super.onCreate(savedInstanceState) @@ -57,31 +56,35 @@ class MainActivity : ComponentActivity() { // 하단 네비게이션 바를 숨겨야 하는 화면들 val shouldHideBottomBar = currentRoute == Route.Login.route || - currentRoute == Route.Registration.route || - currentRoute == Route.RegistrationSuccess.route || - currentRoute == Route.Onboarding.route || - currentRoute == Route.EditProfile.route || - currentRoute == Route.Detail.route || - currentRoute == Route.Search.route + currentRoute == Route.Registration.route || + currentRoute == Route.RegistrationSuccess.route || + currentRoute == Route.Onboarding.route || + currentRoute == Route.EditProfile.route || + currentRoute == Route.Detail.route || + currentRoute == Route.Search.route Scaffold( bottomBar = { if (!shouldHideBottomBar) { Box( - modifier = Modifier - .fillMaxWidth() - .shadow( - elevation = 12.dp, - shape = RoundedCornerShape( - topStart = 30.dp, - topEnd = 30.dp - ), - clip = false - ) - .background( - Color.White, - shape = RoundedCornerShape(topStart = 30.dp, topEnd = 30.dp) - ) + modifier = + Modifier + .fillMaxWidth() + .shadow( + elevation = 12.dp, + shape = + RoundedCornerShape( + topStart = 30.dp, + topEnd = 30.dp + ), + clip = false + ).background( + Color.White, + shape = RoundedCornerShape( + topStart = 30.dp, + topEnd = 30.dp + ) + ) ) { BottomNavBar( visible = true, @@ -95,19 +98,18 @@ class MainActivity : ComponentActivity() { saveState = true } } - }, + } ) } } - }, + } ) { innerPadding -> AppNavGraph( navController = navController, - modifier = Modifier.padding(innerPadding), + modifier = Modifier.padding(innerPadding) ) } } } } } - diff --git a/app/src/main/java/com/example/hackathon/presentation/navigation/BottomNavBar.kt b/app/src/main/java/com/example/hackathon/presentation/navigation/BottomNavBar.kt index 0e68b48..ebe4249 100644 --- a/app/src/main/java/com/example/hackathon/presentation/navigation/BottomNavBar.kt +++ b/app/src/main/java/com/example/hackathon/presentation/navigation/BottomNavBar.kt @@ -18,8 +18,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.example.hackathon.core.designsystem.util.noRippleClickable import com.example.hackathon.core.designsystem.theme.HackathonTheme +import com.example.hackathon.core.designsystem.util.noRippleClickable import kotlin.enums.EnumEntries @Composable @@ -27,7 +27,7 @@ fun BottomNavBar( visible: Boolean, tabs: EnumEntries, currentTab: NavTab?, - onItemSelected: (NavTab) -> Unit, + onItemSelected: (NavTab) -> Unit ) { if (!visible) return @@ -39,11 +39,10 @@ fun BottomNavBar( elevation = 0.5.dp, clip = true, ambientColor = Color.Black.copy(alpha = 0.6f), - spotColor = Color.Black.copy(alpha = 0.9f), + spotColor = Color.Black.copy(alpha = 0.9f) + ).background( + color = Color.White ) - .background( - color = Color.White, - ), ) { Column { Row( @@ -53,7 +52,7 @@ fun BottomNavBar( .navigationBarsPadding() .fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, + horizontalArrangement = Arrangement.SpaceBetween ) { tabs.forEach { tab -> val isSelected = tab == currentTab @@ -66,7 +65,7 @@ fun BottomNavBar( onItemSelected(tab) }, verticalArrangement = Arrangement.spacedBy(2.dp), - horizontalAlignment = Alignment.CenterHorizontally, + horizontalAlignment = Alignment.CenterHorizontally ) { Icon( painter = painterResource(id = tab.icon), @@ -76,7 +75,7 @@ fun BottomNavBar( HackathonTheme.colors.black } else { Color(0xFF8B91A1) - }, + } ) Text( text = tab.label, @@ -86,7 +85,7 @@ fun BottomNavBar( HackathonTheme.colors.black } else { Color(0xFF939DA9) - }, + } ) } } diff --git a/app/src/main/java/com/example/hackathon/presentation/navigation/BottomNavItem.kt b/app/src/main/java/com/example/hackathon/presentation/navigation/BottomNavItem.kt index 390411c..017c98e 100644 --- a/app/src/main/java/com/example/hackathon/presentation/navigation/BottomNavItem.kt +++ b/app/src/main/java/com/example/hackathon/presentation/navigation/BottomNavItem.kt @@ -2,8 +2,4 @@ package com.example.hackathon.presentation.navigation import androidx.compose.ui.graphics.vector.ImageVector -data class BottomNavItem( - val label: String, - val route: String, - val icon: ImageVector, -) +data class BottomNavItem(val label: String, val route: String, val icon: ImageVector) diff --git a/app/src/main/java/com/example/hackathon/presentation/navigation/NavGraph.kt b/app/src/main/java/com/example/hackathon/presentation/navigation/NavGraph.kt index 9fca437..a6a127c 100644 --- a/app/src/main/java/com/example/hackathon/presentation/navigation/NavGraph.kt +++ b/app/src/main/java/com/example/hackathon/presentation/navigation/NavGraph.kt @@ -14,7 +14,6 @@ import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.navArgument -import com.example.hackathon.presentation.route.Route import com.example.hackathon.feature.auth.LoginScreen import com.example.hackathon.feature.auth.RegistrationScreen import com.example.hackathon.feature.auth.RegistrationSuccessScreen @@ -24,15 +23,13 @@ import com.example.hackathon.feature.home.HomeScreen import com.example.hackathon.feature.home.SearchScreen import com.example.hackathon.feature.onboarding.OnboardingScreen import com.example.hackathon.feature.profile.EditProfileScreen +import com.example.hackathon.feature.profile.MyPageViewModel import com.example.hackathon.feature.profile.MyScreen +import com.example.hackathon.presentation.route.Route import com.example.hackathon.presentation.viewmodel.MainViewModel -import com.example.hackathon.feature.profile.MyPageViewModel @Composable -fun AppNavGraph( - navController: NavHostController, - modifier: Modifier = Modifier, -) { +fun AppNavGraph(navController: NavHostController, modifier: Modifier = Modifier) { // 인증 상태 확인을 위한 ViewModel val mainViewModel: MainViewModel = hiltViewModel() val isLoggedIn by mainViewModel.isLoggedIn.collectAsStateWithLifecycle() @@ -41,7 +38,7 @@ fun AppNavGraph( if (isLoggedIn == null) { Box( modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center, + contentAlignment = Alignment.Center ) { CircularProgressIndicator() } @@ -50,7 +47,7 @@ fun AppNavGraph( // isLoggedIn 상태에 따라 startDestination 결정 NavHost( navController = navController, - startDestination = if (isLoggedIn == true) Route.Home.route else Route.Login.route, + startDestination = if (isLoggedIn == true) Route.Home.route else Route.Login.route ) { composable(route = Route.Home.route) { HomeScreen( @@ -61,7 +58,7 @@ fun AppNavGraph( }, onCreateClick = { navController.navigate(Route.Create.route) - }, + } ) } composable(route = Route.Search.route) { @@ -70,7 +67,7 @@ fun AppNavGraph( onNavigateBack = { navController.popBackStack() }, onCombinationClick = { id -> navController.navigate(Route.Detail.createRoute(id)) - }, + } ) } composable( @@ -79,8 +76,8 @@ fun AppNavGraph( listOf( navArgument("id") { type = NavType.StringType - }, - ), + } + ) ) { backStackEntry -> val idString = backStackEntry.arguments?.getString("id") @@ -88,14 +85,14 @@ fun AppNavGraph( DetailScreen( recipeId = recipeId, - onNavigateBack = { navController.popBackStack() }, + onNavigateBack = { navController.popBackStack() } ) } composable(route = Route.Create.route) { CreateCombinationScreen( modifier = modifier, - onNavigateBack = { navController.popBackStack() }, + onNavigateBack = { navController.popBackStack() } ) } composable(route = Route.Login.route) { @@ -111,7 +108,7 @@ fun AppNavGraph( }, onNavigateToRegistration = { navController.navigate(Route.Registration.route) - }, + } ) } composable(route = Route.Registration.route) { @@ -123,7 +120,7 @@ fun AppNavGraph( navController.navigate(Route.RegistrationSuccess.route) { popUpTo(Route.Registration.route) { inclusive = true } } - }, + } ) } composable(route = Route.RegistrationSuccess.route) { @@ -133,7 +130,7 @@ fun AppNavGraph( navController.navigate(Route.Login.route) { popUpTo(Route.RegistrationSuccess.route) { inclusive = true } } - }, + } ) } composable(route = Route.Onboarding.route) { @@ -143,7 +140,7 @@ fun AppNavGraph( navController.navigate(Route.Login.route) { popUpTo(Route.Onboarding.route) { inclusive = true } } - }, + } ) } composable(route = Route.My.route) { @@ -159,7 +156,7 @@ fun AppNavGraph( }, onChangeNickname = { navController.navigate(Route.EditProfile.route) - }, + } ) } composable(route = Route.EditProfile.route) { @@ -172,7 +169,7 @@ fun AppNavGraph( onProfileUpdated = { // 프로필 저장 후 MyScreen의 프로필 새로고침 myScreenViewModel.loadProfile() - }, + } ) } } diff --git a/app/src/main/java/com/example/hackathon/presentation/navigation/NavTab.kt b/app/src/main/java/com/example/hackathon/presentation/navigation/NavTab.kt index 2b5f907..600a6ca 100644 --- a/app/src/main/java/com/example/hackathon/presentation/navigation/NavTab.kt +++ b/app/src/main/java/com/example/hackathon/presentation/navigation/NavTab.kt @@ -4,20 +4,16 @@ import androidx.compose.material.icons.filled.Home import com.example.hackathon.core.designsystem.R import com.example.hackathon.presentation.route.Route -enum class NavTab( - val route: String, - val label: String, - val icon: Int, -) { +enum class NavTab(val route: String, val label: String, val icon: Int) { Home(Route.Home.route, label = "홈", icon = R.drawable.ic_home), Create( route = Route.Create.route, label = "작성", - icon = R.drawable.ic_add, + icon = R.drawable.ic_add ), My( route = Route.My.route, label = "마이", - icon = R.drawable.ic_my, - ), + icon = R.drawable.ic_my + ) } diff --git a/app/src/main/java/com/example/hackathon/presentation/route/Route.kt b/app/src/main/java/com/example/hackathon/presentation/route/Route.kt index 06b18ac..bcab8ff 100644 --- a/app/src/main/java/com/example/hackathon/presentation/route/Route.kt +++ b/app/src/main/java/com/example/hackathon/presentation/route/Route.kt @@ -1,8 +1,6 @@ package com.example.hackathon.presentation.route -sealed class Route( - val route: String, -) { +sealed class Route(val route: String) { data object Home : Route(route = "home") data object Detail : Route("detail") { diff --git a/app/src/main/java/com/example/hackathon/presentation/viewmodel/MainViewModel.kt b/app/src/main/java/com/example/hackathon/presentation/viewmodel/MainViewModel.kt index 23e149b..9248eb2 100644 --- a/app/src/main/java/com/example/hackathon/presentation/viewmodel/MainViewModel.kt +++ b/app/src/main/java/com/example/hackathon/presentation/viewmodel/MainViewModel.kt @@ -3,38 +3,38 @@ package com.example.hackathon.presentation.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.example.hackathon.BuildConfig -import com.example.hackathon.core.domain.repository.AuthRepository import com.example.hackathon.core.datastore.TokenManager +import com.example.hackathon.core.domain.repository.AuthRepository import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import javax.inject.Inject @HiltViewModel class MainViewModel - @Inject - constructor( - private val authRepository: AuthRepository, - private val tokenManager: TokenManager, - ) : ViewModel() { - // 초기 상태: null = 로딩 중, true = 로그인됨, false = 로그인 안됨 - private val _isLoggedIn = MutableStateFlow(null) - val isLoggedIn: StateFlow = _isLoggedIn.asStateFlow() +@Inject +constructor( + private val authRepository: AuthRepository, + private val tokenManager: TokenManager +) : ViewModel() { + // 초기 상태: null = 로딩 중, true = 로그인됨, false = 로그인 안됨 + private val _isLoggedIn = MutableStateFlow(null) + val isLoggedIn: StateFlow = _isLoggedIn.asStateFlow() - init { - checkAuthStatus() - } + init { + checkAuthStatus() + } + + private fun checkAuthStatus() { + viewModelScope.launch { + // Mock 모드가 해제된 경우, Mock 토큰이 있으면 클리어 + tokenManager.clearMockTokensIfNeeded(BuildConfig.USE_MOCK_API) - private fun checkAuthStatus() { - viewModelScope.launch { - // Mock 모드가 해제된 경우, Mock 토큰이 있으면 클리어 - tokenManager.clearMockTokensIfNeeded(BuildConfig.USE_MOCK_API) - - // 토큰 확인 - val hasTokens = authRepository.hasValidTokens() - _isLoggedIn.value = hasTokens - } + // 토큰 확인 + val hasTokens = authRepository.hasValidTokens() + _isLoggedIn.value = hasTokens } } +} diff --git a/build-logic/src/main/kotlin/AndroidFeatureConventionPlugin.kt b/build-logic/src/main/kotlin/AndroidFeatureConventionPlugin.kt index 9c83b97..94c1513 100644 --- a/build-logic/src/main/kotlin/AndroidFeatureConventionPlugin.kt +++ b/build-logic/src/main/kotlin/AndroidFeatureConventionPlugin.kt @@ -26,7 +26,10 @@ class AndroidFeatureConventionPlugin : Plugin { add("implementation", libs.findLibrary("androidx-core-ktx").get()) add("implementation", libs.findLibrary("androidx-lifecycle-runtime-ktx").get()) add("implementation", libs.findLibrary("androidx-activity-compose").get()) - add("implementation", libs.findLibrary("androidx-compose-material-icons-extended").get()) + add( + "implementation", + libs.findLibrary("androidx-compose-material-icons-extended").get() + ) add("implementation", libs.findLibrary("androidx-navigation-compose").get()) add("implementation", libs.findLibrary("hilt-navigation-compose").get()) add("implementation", libs.findLibrary("coil-compose").get()) diff --git a/build-logic/src/main/kotlin/com/example/hackathon/buildlogic/AndroidCompose.kt b/build-logic/src/main/kotlin/com/example/hackathon/buildlogic/AndroidCompose.kt index ce7438a..c4ed766 100644 --- a/build-logic/src/main/kotlin/com/example/hackathon/buildlogic/AndroidCompose.kt +++ b/build-logic/src/main/kotlin/com/example/hackathon/buildlogic/AndroidCompose.kt @@ -11,9 +11,7 @@ import org.gradle.kotlin.dsl.getByType * buildFeatures.compose 활성화 + Compose BOM 기반 의존성을 모듈에 주입한다. * org.jetbrains.kotlin.plugin.compose 플러그인은 각 Compose 모듈에서 별도 적용한다. */ -internal fun Project.configureAndroidCompose( - commonExtension: CommonExtension<*, *, *, *, *, *>, -) { +internal fun Project.configureAndroidCompose(commonExtension: CommonExtension<*, *, *, *, *, *>) { commonExtension.apply { buildFeatures { compose = true diff --git a/build-logic/src/main/kotlin/com/example/hackathon/buildlogic/KotlinAndroid.kt b/build-logic/src/main/kotlin/com/example/hackathon/buildlogic/KotlinAndroid.kt index 5a4dcc5..88d338e 100644 --- a/build-logic/src/main/kotlin/com/example/hackathon/buildlogic/KotlinAndroid.kt +++ b/build-logic/src/main/kotlin/com/example/hackathon/buildlogic/KotlinAndroid.kt @@ -11,9 +11,7 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension * Android Application/Library 모듈 공통 SDK·Java·Kotlin 설정. * compileSdk 36 / minSdk 36 / JVM 17 을 단일 지점에서 강제한다. */ -internal fun Project.configureKotlinAndroid( - commonExtension: CommonExtension<*, *, *, *, *, *>, -) { +internal fun Project.configureKotlinAndroid(commonExtension: CommonExtension<*, *, *, *, *, *>) { commonExtension.apply { compileSdk = 36 diff --git a/core/data/src/main/java/com/example/hackathon/core/data/di/RepositoryModule.kt b/core/data/src/main/java/com/example/hackathon/core/data/di/RepositoryModule.kt index a79879c..0734cf9 100644 --- a/core/data/src/main/java/com/example/hackathon/core/data/di/RepositoryModule.kt +++ b/core/data/src/main/java/com/example/hackathon/core/data/di/RepositoryModule.kt @@ -19,7 +19,9 @@ import javax.inject.Singleton abstract class RepositoryModule { @Binds @Singleton - abstract fun bindCombinationRepository(combinationRepositoryImpl: CombinationRepositoryImpl): CombinationRepository + abstract fun bindCombinationRepository( + combinationRepositoryImpl: CombinationRepositoryImpl + ): CombinationRepository @Binds @Singleton diff --git a/core/data/src/main/java/com/example/hackathon/core/data/mapper/CombinationMapper.kt b/core/data/src/main/java/com/example/hackathon/core/data/mapper/CombinationMapper.kt index f4f7094..6a2fe68 100644 --- a/core/data/src/main/java/com/example/hackathon/core/data/mapper/CombinationMapper.kt +++ b/core/data/src/main/java/com/example/hackathon/core/data/mapper/CombinationMapper.kt @@ -1,34 +1,28 @@ package com.example.hackathon.core.data.mapper -import com.example.hackathon.core.network.dto.request.CreateCombinationRequest -import com.example.hackathon.core.network.dto.response.CombinationResponse -import com.example.hackathon.core.network.dto.response.UserResponse import com.example.hackathon.core.model.Category import com.example.hackathon.core.model.Combination import com.example.hackathon.core.model.User +import com.example.hackathon.core.network.dto.request.CreateCombinationRequest +import com.example.hackathon.core.network.dto.response.CombinationResponse +import com.example.hackathon.core.network.dto.response.UserResponse -fun CombinationResponse.toEntity(): Combination { - return Combination( - id = id, - title = title, - description = description, - imageUrl = imageUrl, - category = Category.valueOf(category.uppercase()), - ingredients = ingredients, - author = author.toEntity(), - likeCount = likeCount, - createdAt = createdAt, - ) -} +fun CombinationResponse.toEntity(): Combination = Combination( + id = id, + title = title, + description = description, + imageUrl = imageUrl, + category = Category.valueOf(category.uppercase()), + ingredients = ingredients, + author = author.toEntity(), + likeCount = likeCount, + createdAt = createdAt +) -fun UserResponse.toEntity(): User { - return User( - id = id, - nickname = nickname, - profileImageUrl = profileImageUrl, - ) -} +fun UserResponse.toEntity(): User = User( + id = id, + nickname = nickname, + profileImageUrl = profileImageUrl +) -fun CreateCombinationRequest.toCategoryString(): String { - return category.uppercase() -} +fun CreateCombinationRequest.toCategoryString(): String = category.uppercase() diff --git a/core/data/src/main/java/com/example/hackathon/core/data/mapper/RecipeMapper.kt b/core/data/src/main/java/com/example/hackathon/core/data/mapper/RecipeMapper.kt index ec94ef0..bb91f23 100644 --- a/core/data/src/main/java/com/example/hackathon/core/data/mapper/RecipeMapper.kt +++ b/core/data/src/main/java/com/example/hackathon/core/data/mapper/RecipeMapper.kt @@ -1,5 +1,12 @@ package com.example.hackathon.core.data.mapper +import com.example.hackathon.core.data.mapper.toEntity +import com.example.hackathon.core.model.Ingredient +import com.example.hackathon.core.model.RecipeDetail +import com.example.hackathon.core.model.RecipeRanking +import com.example.hackathon.core.model.RecipeRankingItem +import com.example.hackathon.core.model.Stats +import com.example.hackathon.core.model.UserInteraction import com.example.hackathon.core.network.dto.response.AuthorDto import com.example.hackathon.core.network.dto.response.DetailResponse import com.example.hackathon.core.network.dto.response.IngredientDto @@ -8,117 +15,104 @@ import com.example.hackathon.core.network.dto.response.RecipeDetailResponse import com.example.hackathon.core.network.dto.response.RecipeRankingItemResponse import com.example.hackathon.core.network.dto.response.RecipeRankingResponse import com.example.hackathon.core.network.dto.response.UserResponse -import com.example.hackathon.core.data.mapper.toEntity -import com.example.hackathon.core.model.Ingredient -import com.example.hackathon.core.model.RecipeDetail -import com.example.hackathon.core.model.RecipeRanking -import com.example.hackathon.core.model.RecipeRankingItem -import com.example.hackathon.core.model.Stats -import com.example.hackathon.core.model.UserInteraction // ========== 기존 DTO Mapper (하위 호환성) ========== -fun RecipeDetailResponse.toDomain(): RecipeDetail = - RecipeDetail( - id = id, - title = title, - category = category, - description = description, - author = author.toEntity(), - images = images, - ingredients = - ingredients.map { - Ingredient( - name = it.name, - amount = it.amount, - ) - }, - stats = - Stats( - likesCount = stats.likesCount, - ), - userInteraction = - UserInteraction( - isLiked = userInteraction.isLiked, - isMine = userInteraction.isMine, - ), - tags = tags, - createdAt = createdAt, - updatedAt = updatedAt, - ) +fun RecipeDetailResponse.toDomain(): RecipeDetail = RecipeDetail( + id = id, + title = title, + category = category, + description = description, + author = author.toEntity(), + images = images, + ingredients = + ingredients.map { + Ingredient( + name = it.name, + amount = it.amount + ) + }, + stats = + Stats( + likesCount = stats.likesCount + ), + userInteraction = + UserInteraction( + isLiked = userInteraction.isLiked, + isMine = userInteraction.isMine + ), + tags = tags, + createdAt = createdAt, + updatedAt = updatedAt +) -fun RecipeRankingResponse.toDomain(): RecipeRanking = - RecipeRanking( - currentCategory = currentCategory, - list = list.map { it.toDomain() }, - ) +fun RecipeRankingResponse.toDomain(): RecipeRanking = RecipeRanking( + currentCategory = currentCategory, + list = list.map { it.toDomain() } +) -fun RecipeRankingItemResponse.toDomain(): RecipeRankingItem = - RecipeRankingItem( - title = title, - description = description, - viewCount = viewCount, - likes = likes, - username = username, - thumbnailUrl = thumbnailUrl, - ) +fun RecipeRankingItemResponse.toDomain(): RecipeRankingItem = RecipeRankingItem( + title = title, + description = description, + viewCount = viewCount, + likes = likes, + username = username, + thumbnailUrl = thumbnailUrl +) // ========== 새로운 DTO Mapper (Swagger 스펙) ========== /** * PostPreviewDto를 RecipeRankingItem으로 변환 */ -fun PostPreviewDto.toDomain(): RecipeRankingItem = - RecipeRankingItem( - title = title, - description = description ?: "", - viewCount = viewCount, - likes = likesCount, - username = author.nickname, - thumbnailUrl = thumbnailUrl, - ) +fun PostPreviewDto.toDomain(): RecipeRankingItem = RecipeRankingItem( + title = title, + description = description ?: "", + viewCount = viewCount, + likes = likesCount, + username = author.nickname, + thumbnailUrl = thumbnailUrl +) /** * List를 RecipeRanking으로 변환 * @param currentCategory 현재 카테고리 (기본값: "ALL") */ -fun List.toDomain(currentCategory: String = "ALL"): RecipeRanking = - RecipeRanking( - currentCategory = currentCategory, - list = map { it.toDomain() }, - ) +fun List.toDomain(currentCategory: String = "ALL"): RecipeRanking = RecipeRanking( + currentCategory = currentCategory, + list = map { it.toDomain() } +) /** * DetailResponse를 RecipeDetail로 변환 */ -fun DetailResponse.toDomain(): RecipeDetail = - RecipeDetail( - id = postId, - title = title, - category = category ?: "", - description = description, - author = author.toEntity(), - images = images, - ingredients = - ingredients.map { - Ingredient( - name = it.name, - amount = it.amount, - ) - }, - stats = - Stats( - likesCount = likesCount, - ), - userInteraction = - UserInteraction( - isLiked = isLiked, - isMine = isMine, - ), - tags = tags, - createdAt = createdAt, - updatedAt = updatedAt ?: createdAt, - ) +fun DetailResponse.toDomain(): RecipeDetail = RecipeDetail( + id = postId, + title = title, + category = category ?: "", + description = description, + author = author.toEntity(), + images = images, + ingredients = + ingredients.map { + Ingredient( + name = it.name, + amount = it.amount + ) + }, + stats = + Stats( + likesCount = likesCount + ), + userInteraction = + UserInteraction( + isLiked = isLiked, + isMine = isMine + ), + tags = tags, + createdAt = createdAt, + updatedAt = updatedAt ?: createdAt +) /** * AuthorDto를 User로 변환 @@ -127,5 +121,5 @@ fun AuthorDto.toEntity(): com.example.hackathon.core.model.User = com.example.hackathon.core.model.User( id = userId, nickname = nickname, - profileImageUrl = profileImageUrl, + profileImageUrl = profileImageUrl ) diff --git a/core/data/src/main/java/com/example/hackathon/core/data/repositoryimpl/AuthRepositoryImpl.kt b/core/data/src/main/java/com/example/hackathon/core/data/repositoryimpl/AuthRepositoryImpl.kt index e7a0e75..5691793 100644 --- a/core/data/src/main/java/com/example/hackathon/core/data/repositoryimpl/AuthRepositoryImpl.kt +++ b/core/data/src/main/java/com/example/hackathon/core/data/repositoryimpl/AuthRepositoryImpl.kt @@ -1,119 +1,115 @@ package com.example.hackathon.core.data.repositoryimpl import com.example.hackathon.core.data.BuildConfig +import com.example.hackathon.core.data.mapper.toEntity +import com.example.hackathon.core.datastore.TokenManager +import com.example.hackathon.core.domain.repository.AuthRepository +import com.example.hackathon.core.domain.repository.LoginResult +import com.example.hackathon.core.domain.repository.ReissueResult +import com.example.hackathon.core.domain.repository.SignupResult +import com.example.hackathon.core.model.DummyData +import com.example.hackathon.core.model.User import com.example.hackathon.core.network.dto.request.LoginRequest import com.example.hackathon.core.network.dto.request.LogoutRequest import com.example.hackathon.core.network.dto.request.ReissueRequest import com.example.hackathon.core.network.dto.request.SignupRequest -import com.example.hackathon.core.model.DummyData -import com.example.hackathon.core.datastore.TokenManager -import com.example.hackathon.core.data.mapper.toEntity import com.example.hackathon.core.network.service.AuthService import com.example.hackathon.core.network.service.UserService -import com.example.hackathon.core.model.User -import com.example.hackathon.core.domain.repository.AuthRepository -import com.example.hackathon.core.domain.repository.LoginResult -import com.example.hackathon.core.domain.repository.ReissueResult -import com.example.hackathon.core.domain.repository.SignupResult -import kotlinx.coroutines.delay import javax.inject.Inject +import kotlinx.coroutines.delay class AuthRepositoryImpl - @Inject - constructor( - private val authService: AuthService, - private val userService: UserService, - private val tokenManager: TokenManager, - ) : AuthRepository { - // 1. 회원가입 - override suspend fun signup( - password: String, - nickname: String, - ): Result = - runCatching { - if (BuildConfig.USE_MOCK_API) { - // Mock 모드: 더미 데이터 반환 - delay(800) // 네트워크 시뮬레이션 - - // 닉네임 중복 체크 (간단한 Mock) - if (nickname == "test") { - throw Exception("이미 존재하는 닉네임입니다") - } - - // 성공 응답 +@Inject +constructor( + private val authService: AuthService, + private val userService: UserService, + private val tokenManager: TokenManager +) : AuthRepository { + // 1. 회원가입 + override suspend fun signup(password: String, nickname: String): Result = + runCatching { + if (BuildConfig.USE_MOCK_API) { + // Mock 모드: 더미 데이터 반환 + delay(800) // 네트워크 시뮬레이션 + + // 닉네임 중복 체크 (간단한 Mock) + if (nickname == "test") { + throw Exception("이미 존재하는 닉네임입니다") + } + + // 성공 응답 + SignupResult( + id = System.currentTimeMillis(), // 임시 ID + nickname = nickname + ) + } else { + // 실제 API 호출 (Swagger 스펙: nickname, password만 필요) + // 서버 응답: text/plain "가입이 완료되었습니다" (200 OK) + val request = SignupRequest(nickname = nickname, password = password) + val response = authService.signup(request) + + if (response.isSuccessful) { + // 성공: 서버가 text/plain으로 "가입이 완료되었습니다" 반환 + // ID는 서버에서 반환하지 않으므로 임시 ID 사용 (실제로는 로그인 후 사용자 정보에서 가져옴) SignupResult( - id = System.currentTimeMillis(), // 임시 ID - nickname = nickname, + id = System.currentTimeMillis(), // 임시 ID (실제로는 사용하지 않을 수 있음) + nickname = nickname ) } else { - // 실제 API 호출 (Swagger 스펙: nickname, password만 필요) - // 서버 응답: text/plain "가입이 완료되었습니다" (200 OK) - val request = SignupRequest(nickname = nickname, password = password) - val response = authService.signup(request) - - if (response.isSuccessful) { - // 성공: 서버가 text/plain으로 "가입이 완료되었습니다" 반환 - // ID는 서버에서 반환하지 않으므로 임시 ID 사용 (실제로는 로그인 후 사용자 정보에서 가져옴) - SignupResult( - id = System.currentTimeMillis(), // 임시 ID (실제로는 사용하지 않을 수 있음) - nickname = nickname, - ) - } else { - throw Exception("회원가입 실패: ${response.code()}") - } + throw Exception("회원가입 실패: ${response.code()}") } } + } - // 2. 로그인 - override suspend fun login( - nickname: String, - password: String, - ): Result = - runCatching { - if (BuildConfig.USE_MOCK_API) { - // Mock 모드: 더미 데이터 반환 - delay(800) // 네트워크 시뮬레이션 - - // 간단한 인증 체크 (Mock) - if (password.isEmpty()) { - throw Exception("비밀번호를 입력해주세요") - } - - // Mock 토큰 생성 - val mockAccessToken = "mock_access_token_${System.currentTimeMillis()}" - val mockRefreshToken = "mock_refresh_token_${System.currentTimeMillis()}" - - // 사용자 정보 생성 - val mockUser = User( + // 2. 로그인 + override suspend fun login(nickname: String, password: String): Result = + runCatching { + if (BuildConfig.USE_MOCK_API) { + // Mock 모드: 더미 데이터 반환 + delay(800) // 네트워크 시뮬레이션 + + // 간단한 인증 체크 (Mock) + if (password.isEmpty()) { + throw Exception("비밀번호를 입력해주세요") + } + + // Mock 토큰 생성 + val mockAccessToken = "mock_access_token_${System.currentTimeMillis()}" + val mockRefreshToken = "mock_refresh_token_${System.currentTimeMillis()}" + + // 사용자 정보 생성 + val mockUser = + User( id = "user_${System.currentTimeMillis()}", nickname = nickname, - profileImageUrl = null, - ) - - // 토큰 저장 - tokenManager.saveTokens(mockAccessToken, mockRefreshToken) - - // DummyData 업데이트 - DummyData.currentUser = mockUser - - LoginResult( - accessToken = mockAccessToken, - refreshToken = mockRefreshToken, - user = mockUser, + profileImageUrl = null ) - } else { - // 실제 API 호출 (Swagger 스펙: nickname, password 사용) - val request = LoginRequest(nickname = nickname, password = password) - val loginResponse = authService.login(request) - - // 토큰 저장 - tokenManager.saveTokens(loginResponse.accessToken, loginResponse.refreshToken) - - // 사용자 정보 가져오기 (로그인 응답에 user가 없으면 /users/mypage로 조회) - // loginResponse.user 는 다른 모듈(core:network)의 nullable public property 라 - // smart cast 불가 → 지역변수로 스냅샷 후 분기. - val responseUser = loginResponse.user - val user = if (responseUser != null) { + + // 토큰 저장 + tokenManager.saveTokens(mockAccessToken, mockRefreshToken) + + // DummyData 업데이트 + DummyData.currentUser = mockUser + + LoginResult( + accessToken = mockAccessToken, + refreshToken = mockRefreshToken, + user = mockUser + ) + } else { + // 실제 API 호출 (Swagger 스펙: nickname, password 사용) + val request = LoginRequest(nickname = nickname, password = password) + val loginResponse = authService.login(request) + + // 토큰 저장 + tokenManager.saveTokens(loginResponse.accessToken, loginResponse.refreshToken) + + // 사용자 정보 가져오기 (로그인 응답에 user가 없으면 /users/mypage로 조회) + // loginResponse.user 는 다른 모듈(core:network)의 nullable public property 라 + // smart cast 불가 → 지역변수로 스냅샷 후 분기. + val responseUser = loginResponse.user + val user = + if (responseUser != null) { responseUser.toEntity() } else { // 사용자 정보를 별도로 조회 @@ -122,102 +118,100 @@ class AuthRepositoryImpl User( id = profile.id ?: "unknown", nickname = profile.nickname, - profileImageUrl = profile.profileImageUrl, + profileImageUrl = profile.profileImageUrl ) } - LoginResult( - accessToken = loginResponse.accessToken, - refreshToken = loginResponse.refreshToken, - user = user, - ) - } + LoginResult( + accessToken = loginResponse.accessToken, + refreshToken = loginResponse.refreshToken, + user = user + ) } + } - // 3. 토큰 재발급 - override suspend fun reissue(): Result = - runCatching { - val refreshToken = - tokenManager.getRefreshToken() - ?: throw IllegalStateException("Refresh token is null") - - if (BuildConfig.USE_MOCK_API) { - // Mock 모드: 새 토큰 생성 - delay(500) - val newAccessToken = "mock_access_token_${System.currentTimeMillis()}" - val newRefreshToken = "mock_refresh_token_${System.currentTimeMillis()}" - - tokenManager.saveTokens(newAccessToken, newRefreshToken) - - ReissueResult( - accessToken = newAccessToken, - refreshToken = newRefreshToken, - ) - } else { - // 실제 API 호출 - // 서버 응답: {"accessToken":"...","refreshToken":"..."} (BaseResponse 래퍼 없음) - val request = ReissueRequest(refreshToken = refreshToken) - val reissueResponse = authService.reissue(request) - - // 새 토큰 저장 - val newRefreshToken = reissueResponse.refreshToken ?: refreshToken - tokenManager.saveTokens(reissueResponse.accessToken, newRefreshToken) - - ReissueResult( - accessToken = reissueResponse.accessToken, - refreshToken = reissueResponse.refreshToken, - ) - } - } + // 3. 토큰 재발급 + override suspend fun reissue(): Result = runCatching { + val refreshToken = + tokenManager.getRefreshToken() + ?: throw IllegalStateException("Refresh token is null") - // 4. 로그아웃 - override suspend fun logout(): Result = - runCatching { - val refreshToken = - tokenManager.getRefreshToken() - ?: throw IllegalStateException("Refresh token is null") - - if (BuildConfig.USE_MOCK_API) { - // Mock 모드: 토큰만 삭제 - delay(300) - tokenManager.clearTokens() - // 사용자 정보도 초기화 - DummyData.currentUser = null - Result.success(Unit) - } else { - // 실제 API 호출 - val request = LogoutRequest(refreshToken = refreshToken) - val response = authService.logout(request) - - if (response.code == 200) { - // 토큰 삭제 - tokenManager.clearTokens() - // 사용자 정보도 초기화 - DummyData.currentUser = null - Result.success(Unit) - } else { - // 로그아웃 실패해도 로컬 데이터는 초기화 - tokenManager.clearTokens() - DummyData.currentUser = null - Result.failure(Exception(response.message)) - } - } - } + if (BuildConfig.USE_MOCK_API) { + // Mock 모드: 새 토큰 생성 + delay(500) + val newAccessToken = "mock_access_token_${System.currentTimeMillis()}" + val newRefreshToken = "mock_refresh_token_${System.currentTimeMillis()}" - // 토큰 확인 - override suspend fun hasValidTokens(): Boolean { - // Mock 모드가 아닐 때만 실제 토큰 확인 - // Mock 모드에서는 자동 로그인 비활성화 (더미 데이터로 자동 로그인 방지) - if (BuildConfig.USE_MOCK_API) { - return false - } - - val accessToken = tokenManager.getAccessToken() - // Mock 토큰이면 무시 - if (accessToken != null && accessToken.startsWith("mock_")) { - return false + tokenManager.saveTokens(newAccessToken, newRefreshToken) + + ReissueResult( + accessToken = newAccessToken, + refreshToken = newRefreshToken + ) + } else { + // 실제 API 호출 + // 서버 응답: {"accessToken":"...","refreshToken":"..."} (BaseResponse 래퍼 없음) + val request = ReissueRequest(refreshToken = refreshToken) + val reissueResponse = authService.reissue(request) + + // 새 토큰 저장 + val newRefreshToken = reissueResponse.refreshToken ?: refreshToken + tokenManager.saveTokens(reissueResponse.accessToken, newRefreshToken) + + ReissueResult( + accessToken = reissueResponse.accessToken, + refreshToken = reissueResponse.refreshToken + ) + } + } + + // 4. 로그아웃 + override suspend fun logout(): Result = runCatching { + val refreshToken = + tokenManager.getRefreshToken() + ?: throw IllegalStateException("Refresh token is null") + + if (BuildConfig.USE_MOCK_API) { + // Mock 모드: 토큰만 삭제 + delay(300) + tokenManager.clearTokens() + // 사용자 정보도 초기화 + DummyData.currentUser = null + Result.success(Unit) + } else { + // 실제 API 호출 + val request = LogoutRequest(refreshToken = refreshToken) + val response = authService.logout(request) + + if (response.code == 200) { + // 토큰 삭제 + tokenManager.clearTokens() + // 사용자 정보도 초기화 + DummyData.currentUser = null + Result.success(Unit) + } else { + // 로그아웃 실패해도 로컬 데이터는 초기화 + tokenManager.clearTokens() + DummyData.currentUser = null + Result.failure(Exception(response.message)) } - - return tokenManager.hasTokens() } } + + // 토큰 확인 + override suspend fun hasValidTokens(): Boolean { + // Mock 모드가 아닐 때만 실제 토큰 확인 + // Mock 모드에서는 자동 로그인 비활성화 (더미 데이터로 자동 로그인 방지) + if (BuildConfig.USE_MOCK_API) { + return false + } + + val accessToken = tokenManager.getAccessToken() + // Mock 토큰이면 무시 + if (accessToken != null && accessToken.startsWith("mock_")) { + return false + } + + return tokenManager.hasTokens() + } +} diff --git a/core/data/src/main/java/com/example/hackathon/core/data/repositoryimpl/CombinationRepositoryImpl.kt b/core/data/src/main/java/com/example/hackathon/core/data/repositoryimpl/CombinationRepositoryImpl.kt index 891960d..d02cdb6 100644 --- a/core/data/src/main/java/com/example/hackathon/core/data/repositoryimpl/CombinationRepositoryImpl.kt +++ b/core/data/src/main/java/com/example/hackathon/core/data/repositoryimpl/CombinationRepositoryImpl.kt @@ -3,304 +3,331 @@ package com.example.hackathon.core.data.repositoryimpl import android.content.Context import android.net.Uri import com.example.hackathon.core.data.BuildConfig -import com.example.hackathon.core.network.dto.request.CreateCombinationRequest -import com.example.hackathon.core.network.dto.request.RegisterRequest -import com.example.hackathon.core.network.dto.request.UpdateCombinationRequest -import com.example.hackathon.core.network.dto.request.UpdateRegisterRequest -import com.example.hackathon.core.network.dto.response.IngredientDto -import com.example.hackathon.core.model.DummyData import com.example.hackathon.core.data.mapper.toEntity import com.example.hackathon.core.datastore.TokenManager -import com.example.hackathon.core.network.service.CombinationService -import com.example.hackathon.core.network.service.RecipeService -import com.example.hackathon.core.network.service.UserService +import com.example.hackathon.core.domain.repository.CombinationRepository import com.example.hackathon.core.model.Category import com.example.hackathon.core.model.Combination +import com.example.hackathon.core.model.DummyData import com.example.hackathon.core.model.Ingredient import com.example.hackathon.core.model.RecipeDetail import com.example.hackathon.core.model.Stats import com.example.hackathon.core.model.UserInteraction -import com.example.hackathon.core.domain.repository.CombinationRepository +import com.example.hackathon.core.network.dto.request.CreateCombinationRequest +import com.example.hackathon.core.network.dto.request.RegisterRequest +import com.example.hackathon.core.network.dto.request.UpdateCombinationRequest +import com.example.hackathon.core.network.dto.request.UpdateRegisterRequest +import com.example.hackathon.core.network.dto.response.IngredientDto +import com.example.hackathon.core.network.service.CombinationService +import com.example.hackathon.core.network.service.RecipeService +import com.example.hackathon.core.network.service.UserService import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.delay import java.util.Base64 import javax.inject.Inject +import kotlinx.coroutines.delay class CombinationRepositoryImpl - @Inject - constructor( - private val combinationService: CombinationService, - private val recipeService: RecipeService, - private val userService: UserService, - private val tokenManager: TokenManager, - @ApplicationContext private val context: Context, - ) : CombinationRepository { - - /** - * Uri를 Base64 인코딩된 문자열로 변환 - * Swagger 스펙: images는 문자열 배열 (Base64 인코딩된 이미지 데이터) - */ - private suspend fun encodeImageToBase64(uri: Uri?): String? { - if (uri == null) return null - return try { - context.contentResolver.openInputStream(uri)?.use { inputStream -> - val bytes = inputStream.readBytes() - Base64.getEncoder().encodeToString(bytes) - } - } catch (e: Exception) { - null +@Inject +constructor( + private val combinationService: CombinationService, + private val recipeService: RecipeService, + private val userService: UserService, + private val tokenManager: TokenManager, + @ApplicationContext private val context: Context +) : CombinationRepository { + /** + * Uri를 Base64 인코딩된 문자열로 변환 + * Swagger 스펙: images는 문자열 배열 (Base64 인코딩된 이미지 데이터) + */ + private suspend fun encodeImageToBase64(uri: Uri?): String? { + if (uri == null) return null + return try { + context.contentResolver.openInputStream(uri)?.use { inputStream -> + val bytes = inputStream.readBytes() + Base64.getEncoder().encodeToString(bytes) } + } catch (e: Exception) { + null } - // 임시로 새로 등록한 조합을 저장하는 리스트 (서버 API 연동 전까지 사용) - private val createdCombinations = mutableListOf() + } - // 좋아요한 조합 ID를 저장하는 Set (서버 API 연동 전까지 사용) - private val likedCombinationIds = mutableSetOf() + // 임시로 새로 등록한 조합을 저장하는 리스트 (서버 API 연동 전까지 사용) + private val createdCombinations = mutableListOf() - // 조합별 좋아요 수를 추적하는 Map (서버 API 연동 전까지 사용) - private val combinationLikeCounts = mutableMapOf() + // 좋아요한 조합 ID를 저장하는 Set (서버 API 연동 전까지 사용) + private val likedCombinationIds = mutableSetOf() - // TODO: 서버 API 연동 시 실제 API 호출로 변경 - override suspend fun getCombinations(category: Category?, page: Int, pageSize: Int): Result> { - return try { - // 네트워크 시뮬레이션 - delay(500) - // 더미 데이터 + 새로 등록한 조합 합치기 - val allCombinations = DummyData.dummyCombinations + createdCombinations - val filtered = - if (category == null || category == Category.ALL) { - allCombinations - } else { - allCombinations.filter { it.category == category } - } - // 좋아요 상태 및 좋아요 수 반영 - val combinationsWithLikeStatus = - filtered.map { combination -> - val likeCount = - combinationLikeCounts[combination.id] ?: combination.likeCount - combination.copy( - isLiked = likedCombinationIds.contains(combination.id), - likeCount = likeCount, - ) - } - - // 페이지네이션 적용 - val startIndex = (page - 1) * pageSize - val endIndex = startIndex + pageSize - val paginatedCombinations = combinationsWithLikeStatus.subList( - startIndex.coerceAtMost(combinationsWithLikeStatus.size), - endIndex.coerceAtMost(combinationsWithLikeStatus.size) + // 조합별 좋아요 수를 추적하는 Map (서버 API 연동 전까지 사용) + private val combinationLikeCounts = mutableMapOf() + + // TODO: 서버 API 연동 시 실제 API 호출로 변경 + override suspend fun getCombinations( + category: Category?, + page: Int, + pageSize: Int + ): Result> = try { + // 네트워크 시뮬레이션 + delay(500) + // 더미 데이터 + 새로 등록한 조합 합치기 + val allCombinations = DummyData.dummyCombinations + createdCombinations + val filtered = + if (category == null || category == Category.ALL) { + allCombinations + } else { + allCombinations.filter { it.category == category } + } + // 좋아요 상태 및 좋아요 수 반영 + val combinationsWithLikeStatus = + filtered.map { combination -> + val likeCount = + combinationLikeCounts[combination.id] ?: combination.likeCount + combination.copy( + isLiked = likedCombinationIds.contains(combination.id), + likeCount = likeCount ) - - Result.success(paginatedCombinations) - } catch (e: Exception) { - Result.failure(e) } - } - override suspend fun getCombinationById(id: String): Result { - return try { - delay(300) - // 더미 데이터 + 새로 등록한 조합에서 찾기 - val allCombinations = DummyData.dummyCombinations + createdCombinations - val combination = allCombinations.find { it.id == id } - if (combination != null) { - // 좋아요 상태 및 좋아요 수 반영 - val likeCount = combinationLikeCounts[id] ?: combination.likeCount - val combinationWithLikeStatus = - combination.copy( - isLiked = likedCombinationIds.contains(id), - likeCount = likeCount, - ) - Result.success(combinationWithLikeStatus) - } else { - Result.failure(Exception("조합을 찾을 수 없습니다")) - } - } catch (e: Exception) { - Result.failure(e) - } + // 페이지네이션 적용 + val startIndex = (page - 1) * pageSize + val endIndex = startIndex + pageSize + val paginatedCombinations = + combinationsWithLikeStatus.subList( + startIndex.coerceAtMost(combinationsWithLikeStatus.size), + endIndex.coerceAtMost(combinationsWithLikeStatus.size) + ) + + Result.success(paginatedCombinations) + } catch (e: Exception) { + Result.failure(e) + } + + override suspend fun getCombinationById(id: String): Result = try { + delay(300) + // 더미 데이터 + 새로 등록한 조합에서 찾기 + val allCombinations = DummyData.dummyCombinations + createdCombinations + val combination = allCombinations.find { it.id == id } + if (combination != null) { + // 좋아요 상태 및 좋아요 수 반영 + val likeCount = combinationLikeCounts[id] ?: combination.likeCount + val combinationWithLikeStatus = + combination.copy( + isLiked = likedCombinationIds.contains(id), + likeCount = likeCount + ) + Result.success(combinationWithLikeStatus) + } else { + Result.failure(Exception("조합을 찾을 수 없습니다")) } + } catch (e: Exception) { + Result.failure(e) + } - override suspend fun createCombination( - title: String, - description: String, - category: Category, - ingredients: List, - tags: List, - imageUri: android.net.Uri?, - isPublic: Boolean, - ): Result { - return try { - if (BuildConfig.USE_MOCK_API) { - // Mock 모드: 더미 데이터 반환 - delay(500) - // imageUri가 있으면 임시로 문자열로 변환 (실제로는 서버에 업로드 후 URL 받아야 함) - val imageUrl = imageUri?.toString() - val newCombination = - Combination( - id = System.currentTimeMillis().toString(), - title = title, - description = description, - imageUrl = imageUrl, - category = category, - ingredients = ingredients, - tags = tags, - // 현재 로그인한 사용자 사용 - author = DummyData.currentUser ?: DummyData.dummyUser, // 로그인 안 됨 시 더미 사용자 사용 - likeCount = 0, - isLiked = false, - createdAt = - java.text.SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault()) - .format(java.util.Date()), - ) - // 임시로 로컬 리스트에 추가 (서버 API 연동 전까지) - createdCombinations.add(newCombination) - Result.success(newCombination) - } else { - // 실제 API 호출 (Swagger 스펙: POST /register 사용) - // 이미지를 Base64 인코딩된 문자열로 변환 - // 디버깅: 이미지가 문제일 수 있으므로 빈 배열로 테스트 가능 - val imageStrings = if (imageUri != null) { - val base64String = encodeImageToBase64(imageUri) - if (base64String != null) { - // Base64 문자열 크기 체크 (너무 크면 서버에서 처리 실패 가능) - // 일반적으로 1MB 이상이면 문제가 될 수 있음 - if (base64String.length > 1_000_000) { - android.util.Log.w("CombinationRepository", "Image Base64 string is too large: ${base64String.length} bytes. Consider compressing the image.") - } - listOf(base64String) - } else { - emptyList() + override suspend fun createCombination( + title: String, + description: String, + category: Category, + ingredients: List, + tags: List, + imageUri: android.net.Uri?, + isPublic: Boolean + ): Result = try { + if (BuildConfig.USE_MOCK_API) { + // Mock 모드: 더미 데이터 반환 + delay(500) + // imageUri가 있으면 임시로 문자열로 변환 (실제로는 서버에 업로드 후 URL 받아야 함) + val imageUrl = imageUri?.toString() + val newCombination = + Combination( + id = System.currentTimeMillis().toString(), + title = title, + description = description, + imageUrl = imageUrl, + category = category, + ingredients = ingredients, + tags = tags, + // 현재 로그인한 사용자 사용 + author = DummyData.currentUser ?: DummyData.dummyUser, // 로그인 안 됨 시 더미 사용자 사용 + likeCount = 0, + isLiked = false, + createdAt = + java.text + .SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault()) + .format(java.util.Date()) + ) + // 임시로 로컬 리스트에 추가 (서버 API 연동 전까지) + createdCombinations.add(newCombination) + Result.success(newCombination) + } else { + // 실제 API 호출 (Swagger 스펙: POST /register 사용) + // 이미지를 Base64 인코딩된 문자열로 변환 + // 디버깅: 이미지가 문제일 수 있으므로 빈 배열로 테스트 가능 + val imageStrings = + if (imageUri != null) { + val base64String = encodeImageToBase64(imageUri) + if (base64String != null) { + // Base64 문자열 크기 체크 (너무 크면 서버에서 처리 실패 가능) + // 일반적으로 1MB 이상이면 문제가 될 수 있음 + if (base64String.length > 1_000_000) { + android.util.Log.w( + "CombinationRepository", + "Image Base64 string is too large: ${base64String.length} bytes. Consider compressing the image." + ) } + listOf(base64String) } else { emptyList() } - - // 디버깅: 요청 데이터 로깅 (개발 중에만) - if (BuildConfig.DEBUG) { - android.util.Log.d("CombinationRepository", "RegisterRequest: title=$title, categories=${listOf(category.name)}, ingredients count=${ingredients.size}, images count=${imageStrings.size}, description length=${description.length}") - } - - // 현재 사용자 ID 가져오기 - // 1. JWT 토큰에서 추출 시도 (가장 안정적) - // 2. /users/mypage API 호출 시도 - // 3. DummyData에서 fallback - val userId = tokenManager.getUserIdFromToken() - ?: try { - val userProfile = userService.getMyPage() - userProfile.id?.toLongOrNull() - } catch (e: Exception) { - // /users/mypage가 실패하면 null - null - } - ?: DummyData.currentUser?.id?.toLongOrNull() - ?: throw IllegalStateException("User ID is not available. Please login again.") - - // Swagger 스펙에 맞게 RegisterRequest 생성 - // ingredients는 "재료명 용량" 형태의 문자열 리스트이므로 파싱 필요 - val ingredientDtos = ingredients.map { ingredientString -> - // "재료명 용량" 형태를 파싱 - // 마지막 공백을 기준으로 name과 amount 분리 - val lastSpaceIndex = ingredientString.lastIndexOf(' ') - if (lastSpaceIndex > 0 && lastSpaceIndex < ingredientString.length - 1) { - IngredientDto( - name = ingredientString.substring(0, lastSpaceIndex).trim(), - amount = ingredientString.substring(lastSpaceIndex + 1).trim(), - ) - } else { - // 공백이 없거나 형식이 잘못된 경우 전체를 name으로 사용 - IngredientDto( - name = ingredientString.trim(), - amount = "", - ) - } + } else { + emptyList() + } + + // 디버깅: 요청 데이터 로깅 (개발 중에만) + if (BuildConfig.DEBUG) { + android.util.Log.d( + "CombinationRepository", + "RegisterRequest: title=$title, categories=${listOf( + category.name + )}, ingredients count=${ingredients.size}, images count=${imageStrings.size}, description length=${description.length}" + ) + } + + // 현재 사용자 ID 가져오기 + // 1. JWT 토큰에서 추출 시도 (가장 안정적) + // 2. /users/mypage API 호출 시도 + // 3. DummyData에서 fallback + val userId = + tokenManager.getUserIdFromToken() + ?: try { + val userProfile = userService.getMyPage() + userProfile.id?.toLongOrNull() + } catch (e: Exception) { + // /users/mypage가 실패하면 null + null } - - val registerRequest = RegisterRequest( - title = title, - categories = listOf(category.name), // 단일 카테고리를 배열로 변환 - ingredients = ingredientDtos, - images = imageStrings, - description = description, - isPrivate = !isPublic, // isPublic = true면 isPrivate = false (전체 공개), isPublic = false면 isPrivate = true (나만 보기) + ?: DummyData.currentUser?.id?.toLongOrNull() + ?: throw IllegalStateException( + "User ID is not available. Please login again." ) - - val response = combinationService.register(userId, registerRequest) - - // Response을 사용하면 빈 응답도 안전하게 처리됨 - if (response.isSuccessful) { - // 200 OK면 성공으로 간주하고 임시 Combination 생성 - val newCombination = Combination( - id = System.currentTimeMillis().toString(), - title = title, - description = description, - imageUrl = imageUri?.toString(), - category = category, - ingredients = ingredients, - tags = tags, - author = DummyData.currentUser ?: DummyData.dummyUser, - likeCount = 0, - isLiked = false, - createdAt = java.text.SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault()) - .format(java.util.Date()), + + // Swagger 스펙에 맞게 RegisterRequest 생성 + // ingredients는 "재료명 용량" 형태의 문자열 리스트이므로 파싱 필요 + val ingredientDtos = + ingredients.map { ingredientString -> + // "재료명 용량" 형태를 파싱 + // 마지막 공백을 기준으로 name과 amount 분리 + val lastSpaceIndex = ingredientString.lastIndexOf(' ') + if (lastSpaceIndex > 0 && + lastSpaceIndex < ingredientString.length - 1 + ) { + IngredientDto( + name = ingredientString.substring(0, lastSpaceIndex).trim(), + amount = ingredientString.substring(lastSpaceIndex + 1).trim() ) - createdCombinations.add(newCombination) - Result.success(newCombination) } else { - throw IllegalStateException("Register failed: ${response.code()}") + // 공백이 없거나 형식이 잘못된 경우 전체를 name으로 사용 + IngredientDto( + name = ingredientString.trim(), + amount = "" + ) } } - } catch (e: Exception) { - Result.failure(e) + + val registerRequest = + RegisterRequest( + title = title, + categories = listOf(category.name), // 단일 카테고리를 배열로 변환 + ingredients = ingredientDtos, + images = imageStrings, + description = description, + isPrivate = !isPublic // isPublic = true면 isPrivate = false (전체 공개), isPublic = false면 isPrivate = true (나만 보기) + ) + + val response = combinationService.register(userId, registerRequest) + + // Response을 사용하면 빈 응답도 안전하게 처리됨 + if (response.isSuccessful) { + // 200 OK면 성공으로 간주하고 임시 Combination 생성 + val newCombination = + Combination( + id = System.currentTimeMillis().toString(), + title = title, + description = description, + imageUrl = imageUri?.toString(), + category = category, + ingredients = ingredients, + tags = tags, + author = DummyData.currentUser ?: DummyData.dummyUser, + likeCount = 0, + isLiked = false, + createdAt = + java.text + .SimpleDateFormat( + "yyyy-MM-dd", + java.util.Locale.getDefault() + ) + .format(java.util.Date()) + ) + createdCombinations.add(newCombination) + Result.success(newCombination) + } else { + throw IllegalStateException("Register failed: ${response.code()}") } } + } catch (e: Exception) { + Result.failure(e) + } - override suspend fun updateCombination( - id: String, - title: String, - description: String, - category: Category, - ingredients: List, - tags: List, - imageUri: android.net.Uri?, - isPublic: Boolean, - ): Result { - return try { - if (BuildConfig.USE_MOCK_API) { - // Mock 모드: 로컬 데이터 업데이트 - delay(500) - val allCombinations = DummyData.dummyCombinations + createdCombinations - val existingCombination = allCombinations.find { it.id == id } + override suspend fun updateCombination( + id: String, + title: String, + description: String, + category: Category, + ingredients: List, + tags: List, + imageUri: android.net.Uri?, + isPublic: Boolean + ): Result { + return try { + if (BuildConfig.USE_MOCK_API) { + // Mock 모드: 로컬 데이터 업데이트 + delay(500) + val allCombinations = DummyData.dummyCombinations + createdCombinations + val existingCombination = + allCombinations.find { it.id == id } ?: return Result.failure(Exception("조합을 찾을 수 없습니다")) - val imageUrl = imageUri?.toString() ?: existingCombination.imageUrl - val updatedCombination = - existingCombination.copy( - title = title, - description = description, - category = category, - ingredients = ingredients, - tags = tags, - imageUrl = imageUrl, - ) + val imageUrl = imageUri?.toString() ?: existingCombination.imageUrl + val updatedCombination = + existingCombination.copy( + title = title, + description = description, + category = category, + ingredients = ingredients, + tags = tags, + imageUrl = imageUrl + ) - // 로컬 리스트 업데이트 - val index = createdCombinations.indexOfFirst { it.id == id } - if (index >= 0) { - createdCombinations[index] = updatedCombination - } + // 로컬 리스트 업데이트 + val index = createdCombinations.indexOfFirst { it.id == id } + if (index >= 0) { + createdCombinations[index] = updatedCombination + } - Result.success(updatedCombination) - } else { - // 실제 API 호출 (Swagger 스펙: PUT /register/{postId} 사용) - // 이미지를 Base64 인코딩된 문자열로 변환 - // 디버깅: 이미지가 문제일 수 있으므로 빈 배열로 테스트 가능 - val imageStrings = if (imageUri != null) { + Result.success(updatedCombination) + } else { + // 실제 API 호출 (Swagger 스펙: PUT /register/{postId} 사용) + // 이미지를 Base64 인코딩된 문자열로 변환 + // 디버깅: 이미지가 문제일 수 있으므로 빈 배열로 테스트 가능 + val imageStrings = + if (imageUri != null) { val base64String = encodeImageToBase64(imageUri) if (base64String != null) { // Base64 문자열 크기 체크 if (base64String.length > 1_000_000) { - android.util.Log.w("CombinationRepository", "Image Base64 string is too large: ${base64String.length} bytes. Consider compressing the image.") + android.util.Log.w( + "CombinationRepository", + "Image Base64 string is too large: ${base64String.length} bytes. Consider compressing the image." + ) } listOf(base64String) } else { @@ -309,12 +336,13 @@ class CombinationRepositoryImpl } else { emptyList() } - - // 현재 사용자 ID 가져오기 - // 1. JWT 토큰에서 추출 시도 (가장 안정적) - // 2. /users/mypage API 호출 시도 - // 3. DummyData에서 fallback - val userId = tokenManager.getUserIdFromToken() + + // 현재 사용자 ID 가져오기 + // 1. JWT 토큰에서 추출 시도 (가장 안정적) + // 2. /users/mypage API 호출 시도 + // 3. DummyData에서 fallback + val userId = + tokenManager.getUserIdFromToken() ?: try { val userProfile = userService.getMyPage() userProfile.id?.toLongOrNull() @@ -323,51 +351,66 @@ class CombinationRepositoryImpl null } ?: DummyData.currentUser?.id?.toLongOrNull() - ?: throw IllegalStateException("User ID is not available. Please login again.") - - val postId = id.toLongOrNull() + ?: throw IllegalStateException( + "User ID is not available. Please login again." + ) + + val postId = + id.toLongOrNull() ?: throw IllegalStateException("Post ID is not a valid number") - - // 디버깅: 요청 데이터 로깅 (개발 중에만) - if (BuildConfig.DEBUG) { - android.util.Log.d("CombinationRepository", "UpdateRegisterRequest: postId=$postId, title=$title, categories=${listOf(category.name)}, ingredients count=${ingredients.size}, images count=${imageStrings.size}") - } - - // Swagger 스펙에 맞게 UpdateRegisterRequest 생성 - // ingredients는 "재료명 용량" 형태의 문자열 리스트이므로 파싱 필요 - val ingredientDtos = ingredients.map { ingredientString -> + + // 디버깅: 요청 데이터 로깅 (개발 중에만) + if (BuildConfig.DEBUG) { + android.util.Log.d( + "CombinationRepository", + "UpdateRegisterRequest: postId=$postId, title=$title, categories=${listOf( + category.name + )}, ingredients count=${ingredients.size}, images count=${imageStrings.size}" + ) + } + + // Swagger 스펙에 맞게 UpdateRegisterRequest 생성 + // ingredients는 "재료명 용량" 형태의 문자열 리스트이므로 파싱 필요 + val ingredientDtos = + ingredients.map { ingredientString -> // "재료명 용량" 형태를 파싱 // 마지막 공백을 기준으로 name과 amount 분리 val lastSpaceIndex = ingredientString.lastIndexOf(' ') - if (lastSpaceIndex > 0 && lastSpaceIndex < ingredientString.length - 1) { + if (lastSpaceIndex > 0 && + lastSpaceIndex < ingredientString.length - 1 + ) { IngredientDto( name = ingredientString.substring(0, lastSpaceIndex).trim(), - amount = ingredientString.substring(lastSpaceIndex + 1).trim(), + amount = ingredientString.substring(lastSpaceIndex + 1).trim() ) } else { // 공백이 없거나 형식이 잘못된 경우 전체를 name으로 사용 IngredientDto( name = ingredientString.trim(), - amount = "", + amount = "" ) } } - - val updateRequest = UpdateRegisterRequest( + + val updateRequest = + UpdateRegisterRequest( title = title, - categories = listOf(category.name), // 단일 카테고리를 배열로 변환 + // 단일 카테고리를 배열로 변환 + categories = listOf(category.name), ingredients = ingredientDtos, images = imageStrings, description = description, - isPrivate = !isPublic, // isPublic = true면 isPrivate = false (전체 공개), isPublic = false면 isPrivate = true (나만 보기) + // isPublic=true → isPrivate=false(전체 공개), false → true(나만 보기) + isPrivate = !isPublic ) - - val response = combinationService.updateRegister(postId, userId, updateRequest) - - // Response을 사용하면 빈 응답도 안전하게 처리됨 - if (response.isSuccessful) { - // 200 OK면 성공으로 간주: 로컬 데이터 업데이트 - val updatedCombination = Combination( + + val response = combinationService.updateRegister(postId, userId, updateRequest) + + // Response을 사용하면 빈 응답도 안전하게 처리됨 + if (response.isSuccessful) { + // 200 OK면 성공으로 간주: 로컬 데이터 업데이트 + val updatedCombination = + Combination( id = id, title = title, description = description, @@ -378,172 +421,170 @@ class CombinationRepositoryImpl author = DummyData.currentUser ?: DummyData.dummyUser, likeCount = 0, isLiked = false, - createdAt = java.text.SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault()) - .format(java.util.Date()), + createdAt = + java.text + .SimpleDateFormat( + "yyyy-MM-dd", + java.util.Locale.getDefault() + ) + .format(java.util.Date()) ) - val index = createdCombinations.indexOfFirst { it.id == id } - if (index >= 0) { - createdCombinations[index] = updatedCombination - } - Result.success(updatedCombination) - } else { - throw IllegalStateException("Update register failed: ${response.code()}") + val index = createdCombinations.indexOfFirst { it.id == id } + if (index >= 0) { + createdCombinations[index] = updatedCombination } + Result.success(updatedCombination) + } else { + throw IllegalStateException("Update register failed: ${response.code()}") } - } catch (e: Exception) { - Result.failure(e) } + } catch (e: Exception) { + Result.failure(e) } + } - override suspend fun deleteCombination(id: String): Result { - return try { - if (BuildConfig.USE_MOCK_API) { - // Mock 모드: 로컬 데이터 삭제 - delay(300) - createdCombinations.removeAll { it.id == id } - Result.success(Unit) - } else { - // 실제 API 호출 (Swagger 스펙: DELETE /register/{postId} 사용) - // 현재 사용자 ID 가져오기 - // 1. JWT 토큰에서 추출 시도 (가장 안정적) - // 2. /users/mypage API 호출 시도 - // 3. DummyData에서 fallback - val userId = tokenManager.getUserIdFromToken() - ?: try { - val userProfile = userService.getMyPage() - userProfile.id?.toLongOrNull() - } catch (e: Exception) { - // /users/mypage가 실패하면 null - null - } - ?: DummyData.currentUser?.id?.toLongOrNull() - ?: throw IllegalStateException("User ID is not available. Please login again.") - - val postId = id.toLongOrNull() - ?: throw IllegalStateException("Post ID is not a valid number") - - val response = combinationService.deleteRegister(postId, userId) - - // Response을 사용하면 빈 응답도 안전하게 처리됨 - if (response.isSuccessful) { - Result.success(Unit) - } else { - throw IllegalStateException("Delete register failed: ${response.code()}") + override suspend fun deleteCombination(id: String): Result = try { + if (BuildConfig.USE_MOCK_API) { + // Mock 모드: 로컬 데이터 삭제 + delay(300) + createdCombinations.removeAll { it.id == id } + Result.success(Unit) + } else { + // 실제 API 호출 (Swagger 스펙: DELETE /register/{postId} 사용) + // 현재 사용자 ID 가져오기 + // 1. JWT 토큰에서 추출 시도 (가장 안정적) + // 2. /users/mypage API 호출 시도 + // 3. DummyData에서 fallback + val userId = + tokenManager.getUserIdFromToken() + ?: try { + val userProfile = userService.getMyPage() + userProfile.id?.toLongOrNull() + } catch (e: Exception) { + // /users/mypage가 실패하면 null + null } - } - } catch (e: Exception) { - Result.failure(e) - } - } + ?: DummyData.currentUser?.id?.toLongOrNull() + ?: throw IllegalStateException( + "User ID is not available. Please login again." + ) - override suspend fun getMyCombinations(): Result> { - return try { - delay(300) - // TODO: 실제 API 호출 - // 더미 데이터 + 새로 등록한 조합에서 내 조합만 필터링 - val allCombinations = DummyData.dummyCombinations + createdCombinations - // 현재 로그인한 사용자의 조합만 - val myCombinations = - allCombinations - .filter { - it.author.id == DummyData.currentUser?.id - } - .map { combination -> - val likeCount = - combinationLikeCounts[combination.id] ?: combination.likeCount - combination.copy( - isLiked = likedCombinationIds.contains(combination.id), - likeCount = likeCount, - ) - } - Result.success(myCombinations) - } catch (e: Exception) { - Result.failure(e) - } - } + val postId = + id.toLongOrNull() + ?: throw IllegalStateException("Post ID is not a valid number") - override suspend fun getLikedCombinations(): Result> { - return try { - delay(300) - val allCombinations = DummyData.dummyCombinations + createdCombinations - val likedCombinations = - allCombinations - .filter { likedCombinationIds.contains(it.id) } - .map { combination -> - val likeCount = - combinationLikeCounts[combination.id] ?: combination.likeCount - combination.copy( - isLiked = true, - likeCount = likeCount, - ) - } - Result.success(likedCombinations) - } catch (e: Exception) { - Result.failure(e) + val response = combinationService.deleteRegister(postId, userId) + + // Response을 사용하면 빈 응답도 안전하게 처리됨 + if (response.isSuccessful) { + Result.success(Unit) + } else { + throw IllegalStateException("Delete register failed: ${response.code()}") } } + } catch (e: Exception) { + Result.failure(e) + } - override suspend fun likeCombination(id: String): Result { - return try { - delay(100) - // 좋아요 토글 로직 - val wasLiked = likedCombinationIds.contains(id) - if (wasLiked) { - likedCombinationIds.remove(id) - // 좋아요 수 감소 - val allCombinations = DummyData.dummyCombinations + createdCombinations - val combination = allCombinations.find { it.id == id } - if (combination != null) { - val currentCount = combinationLikeCounts[id] ?: combination.likeCount - combinationLikeCounts[id] = (currentCount - 1).coerceAtLeast(0) - } - } else { - likedCombinationIds.add(id) - // 좋아요 수 증가 - val allCombinations = DummyData.dummyCombinations + createdCombinations - val combination = allCombinations.find { it.id == id } - if (combination != null) { - val currentCount = combinationLikeCounts[id] ?: combination.likeCount - combinationLikeCounts[id] = currentCount + 1 - } + override suspend fun getMyCombinations(): Result> = try { + delay(300) + // TODO: 실제 API 호출 + // 더미 데이터 + 새로 등록한 조합에서 내 조합만 필터링 + val allCombinations = DummyData.dummyCombinations + createdCombinations + // 현재 로그인한 사용자의 조합만 + val myCombinations = + allCombinations + .filter { + it.author.id == DummyData.currentUser?.id + }.map { combination -> + val likeCount = + combinationLikeCounts[combination.id] ?: combination.likeCount + combination.copy( + isLiked = likedCombinationIds.contains(combination.id), + likeCount = likeCount + ) } - Result.success(Unit) - } catch (e: Exception) { - Result.failure(e) + Result.success(myCombinations) + } catch (e: Exception) { + Result.failure(e) + } + + override suspend fun getLikedCombinations(): Result> = try { + delay(300) + val allCombinations = DummyData.dummyCombinations + createdCombinations + val likedCombinations = + allCombinations + .filter { likedCombinationIds.contains(it.id) } + .map { combination -> + val likeCount = + combinationLikeCounts[combination.id] ?: combination.likeCount + combination.copy( + isLiked = true, + likeCount = likeCount + ) + } + Result.success(likedCombinations) + } catch (e: Exception) { + Result.failure(e) + } + + override suspend fun likeCombination(id: String): Result = try { + delay(100) + // 좋아요 토글 로직 + val wasLiked = likedCombinationIds.contains(id) + if (wasLiked) { + likedCombinationIds.remove(id) + // 좋아요 수 감소 + val allCombinations = DummyData.dummyCombinations + createdCombinations + val combination = allCombinations.find { it.id == id } + if (combination != null) { + val currentCount = combinationLikeCounts[id] ?: combination.likeCount + combinationLikeCounts[id] = (currentCount - 1).coerceAtLeast(0) + } + } else { + likedCombinationIds.add(id) + // 좋아요 수 증가 + val allCombinations = DummyData.dummyCombinations + createdCombinations + val combination = allCombinations.find { it.id == id } + if (combination != null) { + val currentCount = combinationLikeCounts[id] ?: combination.likeCount + combinationLikeCounts[id] = currentCount + 1 } } + Result.success(Unit) + } catch (e: Exception) { + Result.failure(e) + } - override suspend fun searchCombinations(query: String): Result> { - return try { - delay(300) - val allCombinations = DummyData.dummyCombinations + createdCombinations - val searchResults = - allCombinations - .filter { - it.title.contains(query, ignoreCase = true) || - it.description.contains(query, ignoreCase = true) || - it.tags.any { tag -> tag.contains(query, ignoreCase = true) } || - it.ingredients.any { ingredient -> ingredient.contains(query, ignoreCase = true) } - } - .map { combination -> - val likeCount = - combinationLikeCounts[combination.id] ?: combination.likeCount - combination.copy( - isLiked = likedCombinationIds.contains(combination.id), - likeCount = likeCount, - ) + override suspend fun searchCombinations(query: String): Result> = try { + delay(300) + val allCombinations = DummyData.dummyCombinations + createdCombinations + val searchResults = + allCombinations + .filter { + it.title.contains(query, ignoreCase = true) || + it.description.contains(query, ignoreCase = true) || + it.tags.any { tag -> tag.contains(query, ignoreCase = true) } || + it.ingredients.any { ingredient -> + ingredient.contains(query, ignoreCase = true) } - Result.success(searchResults) - } catch (e: Exception) { - Result.failure(e) - } - } + }.map { combination -> + val likeCount = + combinationLikeCounts[combination.id] ?: combination.likeCount + combination.copy( + isLiked = likedCombinationIds.contains(combination.id), + likeCount = likeCount + ) + } + Result.success(searchResults) + } catch (e: Exception) { + Result.failure(e) + } - override suspend fun getRecipeDetail(id: Long): Result = - runCatching { - DummyData.getRecipeDetailById(id) - ?: throw IllegalStateException("Recipe detail data is null") - } + override suspend fun getRecipeDetail(id: Long): Result = runCatching { + DummyData.getRecipeDetailById(id) + ?: throw IllegalStateException("Recipe detail data is null") + } /* * TODO: 나중에 API 연동 후 아래 코드 사용 @@ -558,4 +599,4 @@ class CombinationRepositoryImpl * data.toDomain() * } */ - } +} diff --git a/core/data/src/main/java/com/example/hackathon/core/data/repositoryimpl/RecipeRepositoryImpl.kt b/core/data/src/main/java/com/example/hackathon/core/data/repositoryimpl/RecipeRepositoryImpl.kt index f322e57..56af5ae 100644 --- a/core/data/src/main/java/com/example/hackathon/core/data/repositoryimpl/RecipeRepositoryImpl.kt +++ b/core/data/src/main/java/com/example/hackathon/core/data/repositoryimpl/RecipeRepositoryImpl.kt @@ -2,137 +2,135 @@ package com.example.hackathon.core.data.repositoryimpl import com.example.hackathon.core.data.BuildConfig import com.example.hackathon.core.data.mapper.toDomain -import com.example.hackathon.core.network.service.RecipeService +import com.example.hackathon.core.domain.repository.RecipeRepository import com.example.hackathon.core.model.RecipeDetail import com.example.hackathon.core.model.RecipeRanking -import com.example.hackathon.core.domain.repository.RecipeRepository -import kotlinx.coroutines.delay -import retrofit2.HttpException +import com.example.hackathon.core.network.service.RecipeService import java.io.IOException import javax.inject.Inject +import kotlinx.coroutines.delay +import retrofit2.HttpException class RecipeRepositoryImpl - @Inject - constructor( - private val recipeService: RecipeService, - ) : RecipeRepository { - // 1. 전체 랭킹 조회(홈화면) - override suspend fun getRecipeRanking(page: Int?): Result = - runCatching { - val response = recipeService.getRecipes(page = page, category = null) - val data = - response.data - ?: throw IllegalStateException("Recipe ranking data is null") - // List를 RecipeRanking으로 변환 - data.toDomain(currentCategory = "ALL") - } +@Inject +constructor(private val recipeService: RecipeService) : + RecipeRepository { + // 1. 전체 랭킹 조회(홈화면) + override suspend fun getRecipeRanking(page: Int?): Result = runCatching { + val response = recipeService.getRecipes(page = page, category = null) + val data = + response.data + ?: throw IllegalStateException("Recipe ranking data is null") + // List를 RecipeRanking으로 변환 + data.toDomain(currentCategory = "ALL") + } - // 2. 특정 카테고리 랭킹조회 - override suspend fun getRecipeRankingByCategory(categoryId: String): Result = - runCatching { - val response = recipeService.getRecipes(page = null, category = categoryId) - val data = - response.data - ?: throw IllegalStateException("Recipe ranking data is null") - // List를 RecipeRanking으로 변환 - data.toDomain(currentCategory = categoryId) - } + // 2. 특정 카테고리 랭킹조회 + override suspend fun getRecipeRankingByCategory(categoryId: String): Result = + runCatching { + val response = recipeService.getRecipes(page = null, category = categoryId) + val data = + response.data + ?: throw IllegalStateException("Recipe ranking data is null") + // List를 RecipeRanking으로 변환 + data.toDomain(currentCategory = categoryId) + } - // 3. 검색/해시태그검색 - override suspend fun searchRecipes(keyword: String): Result = - runCatching { - val response = recipeService.searchRecipes(keyword) - val data = - response.data - ?: throw IllegalStateException("Recipe search data is null") - // List를 RecipeRanking으로 변환 - data.toDomain(currentCategory = "SEARCH") - } + // 3. 검색/해시태그검색 + override suspend fun searchRecipes(keyword: String): Result = runCatching { + val response = recipeService.searchRecipes(keyword) + val data = + response.data + ?: throw IllegalStateException("Recipe search data is null") + // List를 RecipeRanking으로 변환 + data.toDomain(currentCategory = "SEARCH") + } - // 4. 레시피 상세조회 - override suspend fun getRecipeDetail(postId: Long): Result = - runCatching { - val response = recipeService.getRecipeDetail(postId) - val data = - response.data - ?: throw IllegalStateException("Recipe detail data is null") - // DetailResponse를 RecipeDetail로 변환 - data.toDomain() - } + // 4. 레시피 상세조회 + override suspend fun getRecipeDetail(postId: Long): Result = runCatching { + val response = recipeService.getRecipeDetail(postId) + val data = + response.data + ?: throw IllegalStateException("Recipe detail data is null") + // DetailResponse를 RecipeDetail로 변환 + data.toDomain() + } - // 5. 좋아요 - override suspend fun likeRecipe(id: Long): Result { - return try { - if (BuildConfig.USE_MOCK_API) { - // Mock 모드: 네트워크 시뮬레이션 - delay(300) + // 5. 좋아요 + override suspend fun likeRecipe(id: Long): Result = try { + if (BuildConfig.USE_MOCK_API) { + // Mock 모드: 네트워크 시뮬레이션 + delay(300) + Result.success(Unit) + } else { + // 실제 API 호출 + try { + val response = recipeService.likeRecipe(id) + if (response.isSuccessful) { + // 서버는 200 OK with empty body를 반환 Result.success(Unit) } else { - // 실제 API 호출 - try { - val response = recipeService.likeRecipe(id) - if (response.isSuccessful) { - // 서버는 200 OK with empty body를 반환 - Result.success(Unit) - } else { - Result.failure(Exception("서버 오류: ${response.code()} ${response.message()}")) - } - } catch (e: HttpException) { - // HTTP 에러 (4xx, 5xx) 처리 - val errorMessage = when (e.code()) { - 500 -> "서버 내부 오류가 발생했습니다. 잠시 후 다시 시도해주세요." - 404 -> "레시피를 찾을 수 없습니다." - 401 -> "인증이 필요합니다." - 403 -> "권한이 없습니다." - else -> "좋아요 처리에 실패했습니다. (${e.code()})" - } - Result.failure(Exception(errorMessage)) - } catch (e: IOException) { - // 네트워크 에러 - Result.failure(Exception("네트워크 연결을 확인해주세요.")) - } + Result.failure( + Exception("서버 오류: ${response.code()} ${response.message()}") + ) } - } catch (e: Exception) { - // 기타 예외 처리 - Result.failure(e) + } catch (e: HttpException) { + // HTTP 에러 (4xx, 5xx) 처리 + val errorMessage = + when (e.code()) { + 500 -> "서버 내부 오류가 발생했습니다. 잠시 후 다시 시도해주세요." + 404 -> "레시피를 찾을 수 없습니다." + 401 -> "인증이 필요합니다." + 403 -> "권한이 없습니다." + else -> "좋아요 처리에 실패했습니다. (${e.code()})" + } + Result.failure(Exception(errorMessage)) + } catch (e: IOException) { + // 네트워크 에러 + Result.failure(Exception("네트워크 연결을 확인해주세요.")) } } + } catch (e: Exception) { + // 기타 예외 처리 + Result.failure(e) + } - // 6. 좋아요 취소 - override suspend fun unlikeRecipe(postId: Long): Result { - return try { - if (BuildConfig.USE_MOCK_API) { - // Mock 모드: 네트워크 시뮬레이션 - delay(300) + // 6. 좋아요 취소 + override suspend fun unlikeRecipe(postId: Long): Result = try { + if (BuildConfig.USE_MOCK_API) { + // Mock 모드: 네트워크 시뮬레이션 + delay(300) + Result.success(Unit) + } else { + // 실제 API 호출 + try { + val response = recipeService.unlikeRecipe(postId) + if (response.isSuccessful) { + // 서버는 200 OK with empty body를 반환 Result.success(Unit) } else { - // 실제 API 호출 - try { - val response = recipeService.unlikeRecipe(postId) - if (response.isSuccessful) { - // 서버는 200 OK with empty body를 반환 - Result.success(Unit) - } else { - Result.failure(Exception("서버 오류: ${response.code()} ${response.message()}")) - } - } catch (e: HttpException) { - // HTTP 에러 (4xx, 5xx) 처리 - val errorMessage = when (e.code()) { - 500 -> "서버 내부 오류가 발생했습니다. 잠시 후 다시 시도해주세요." - 404 -> "레시피를 찾을 수 없습니다." - 401 -> "인증이 필요합니다." - 403 -> "권한이 없습니다." - else -> "좋아요 취소에 실패했습니다. (${e.code()})" - } - Result.failure(Exception(errorMessage)) - } catch (e: IOException) { - // 네트워크 에러 - Result.failure(Exception("네트워크 연결을 확인해주세요.")) - } + Result.failure( + Exception("서버 오류: ${response.code()} ${response.message()}") + ) } - } catch (e: Exception) { - // 기타 예외 처리 - Result.failure(e) + } catch (e: HttpException) { + // HTTP 에러 (4xx, 5xx) 처리 + val errorMessage = + when (e.code()) { + 500 -> "서버 내부 오류가 발생했습니다. 잠시 후 다시 시도해주세요." + 404 -> "레시피를 찾을 수 없습니다." + 401 -> "인증이 필요합니다." + 403 -> "권한이 없습니다." + else -> "좋아요 취소에 실패했습니다. (${e.code()})" + } + Result.failure(Exception(errorMessage)) + } catch (e: IOException) { + // 네트워크 에러 + Result.failure(Exception("네트워크 연결을 확인해주세요.")) } } + } catch (e: Exception) { + // 기타 예외 처리 + Result.failure(e) } +} diff --git a/core/data/src/main/java/com/example/hackathon/core/data/repositoryimpl/UserRepositoryImpl.kt b/core/data/src/main/java/com/example/hackathon/core/data/repositoryimpl/UserRepositoryImpl.kt index 952a396..5ae2c70 100644 --- a/core/data/src/main/java/com/example/hackathon/core/data/repositoryimpl/UserRepositoryImpl.kt +++ b/core/data/src/main/java/com/example/hackathon/core/data/repositoryimpl/UserRepositoryImpl.kt @@ -1,99 +1,104 @@ package com.example.hackathon.core.data.repositoryimpl import com.example.hackathon.core.data.BuildConfig -import com.example.hackathon.core.model.DummyData -import com.example.hackathon.core.datastore.TokenManager import com.example.hackathon.core.data.mapper.toEntity -import com.example.hackathon.core.network.service.UserService -import com.example.hackathon.core.model.User +import com.example.hackathon.core.datastore.TokenManager import com.example.hackathon.core.domain.repository.UserRepository +import com.example.hackathon.core.model.DummyData +import com.example.hackathon.core.model.User +import com.example.hackathon.core.network.service.UserService import javax.inject.Inject class UserRepositoryImpl - @Inject - constructor( - private val userService: UserService, - private val tokenManager: TokenManager, - ) : UserRepository { - override suspend fun getProfile(): Result { - return try { - if (BuildConfig.USE_MOCK_API) { - // Mock 모드: 더미 데이터 반환 - val currentUser = DummyData.currentUser +@Inject +constructor( + private val userService: UserService, + private val tokenManager: TokenManager +) : UserRepository { + override suspend fun getProfile(): Result { + return try { + if (BuildConfig.USE_MOCK_API) { + // Mock 모드: 더미 데이터 반환 + val currentUser = + DummyData.currentUser ?: return Result.failure(Exception("User not logged in")) - Result.success(currentUser) - } else { - // 실제 API 호출 - // 서버 응답: {"nickname":"...","myRecipeCount":0} (BaseResponse 래퍼 없음) - val profile = userService.getMyPage() - - // userId는 JWT에서 가져오거나 기본값 사용 - val userId = tokenManager.getUserIdFromToken()?.toString() + Result.success(currentUser) + } else { + // 실제 API 호출 + // 서버 응답: {"nickname":"...","myRecipeCount":0} (BaseResponse 래퍼 없음) + val profile = userService.getMyPage() + + // userId는 JWT에서 가져오거나 기본값 사용 + val userId = + tokenManager.getUserIdFromToken()?.toString() ?: profile.id ?: "unknown" - - val user = User( + + val user = + User( id = userId, nickname = profile.nickname, - profileImageUrl = profile.profileImageUrl, + profileImageUrl = profile.profileImageUrl ) - - // 로컬 더미 데이터도 업데이트 (하위 호환성) - DummyData.currentUser = user - - Result.success(user) - } - } catch (e: Exception) { - Result.failure(e) + + // 로컬 더미 데이터도 업데이트 (하위 호환성) + DummyData.currentUser = user + + Result.success(user) } + } catch (e: Exception) { + Result.failure(e) } + } - override suspend fun updateProfile( - nickname: String, - profileImageUrl: String?, - ): Result { - return try { - if (BuildConfig.USE_MOCK_API) { - // Mock 모드: 로컬 사용자 정보 업데이트 - val currentUser = DummyData.currentUser + override suspend fun updateProfile(nickname: String, profileImageUrl: String?): Result { + return try { + if (BuildConfig.USE_MOCK_API) { + // Mock 모드: 로컬 사용자 정보 업데이트 + val currentUser = + DummyData.currentUser ?: return Result.failure(Exception("User not logged in")) - val updatedUser = - currentUser.copy( - nickname = nickname, - profileImageUrl = profileImageUrl, - ) - DummyData.currentUser = updatedUser - Result.success(updatedUser) - } else { - // 실제 API 호출 - val request = com.example.hackathon.core.network.dto.request.UpdateProfileRequest( + val updatedUser = + currentUser.copy( + nickname = nickname, + profileImageUrl = profileImageUrl + ) + DummyData.currentUser = updatedUser + Result.success(updatedUser) + } else { + // 실제 API 호출 + val request = + com.example.hackathon.core.network.dto.request.UpdateProfileRequest( nickname = nickname, - profileImageUrl = profileImageUrl, + profileImageUrl = profileImageUrl ) - val response = userService.updateMyPage(request) - - // BaseResponse에서 data 추출 - val profile = response.data + val response = userService.updateMyPage(request) + + // BaseResponse에서 data 추출 + val profile = + response.data ?: return Result.failure(Exception(response.message ?: "프로필 업데이트 실패")) - - // userId는 JWT에서 가져오거나 기본값 사용 - val userId = tokenManager.getUserIdFromToken()?.toString() + + // userId는 JWT에서 가져오거나 기본값 사용 + val userId = + tokenManager.getUserIdFromToken()?.toString() ?: profile.id ?: "unknown" - - val updatedUser = User( + + val updatedUser = + User( id = userId, nickname = profile.nickname, - profileImageUrl = profile.profileImageUrl, + profileImageUrl = profile.profileImageUrl ) - - // 로컬 더미 데이터도 업데이트 (하위 호환성) - DummyData.currentUser = updatedUser - - Result.success(updatedUser) - } - } catch (e: Exception) { - Result.failure(e) + + // 로컬 더미 데이터도 업데이트 (하위 호환성) + DummyData.currentUser = updatedUser + + Result.success(updatedUser) } + } catch (e: Exception) { + Result.failure(e) } } +} diff --git a/core/datastore/src/main/java/com/example/hackathon/core/datastore/TokenManager.kt b/core/datastore/src/main/java/com/example/hackathon/core/datastore/TokenManager.kt index 21fbbc0..6017e69 100644 --- a/core/datastore/src/main/java/com/example/hackathon/core/datastore/TokenManager.kt +++ b/core/datastore/src/main/java/com/example/hackathon/core/datastore/TokenManager.kt @@ -7,111 +7,113 @@ import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import javax.inject.Singleton import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map -import javax.inject.Inject -import javax.inject.Singleton -private val Context.dataStore: DataStore by preferencesDataStore(name = "token_preferences") +private val Context.dataStore: DataStore by preferencesDataStore( + name = "token_preferences" +) @Singleton class TokenManager - @Inject - constructor( - @ApplicationContext private val context: Context, - ) { - companion object { - private val ACCESS_TOKEN_KEY = stringPreferencesKey("access_token") - private val REFRESH_TOKEN_KEY = stringPreferencesKey("refresh_token") - } - - val accessToken: Flow = - context.dataStore.data.map { preferences -> - preferences[ACCESS_TOKEN_KEY] - } - - val refreshToken: Flow = - context.dataStore.data.map { preferences -> - preferences[REFRESH_TOKEN_KEY] - } +@Inject +constructor(@ApplicationContext private val context: Context) { + companion object { + private val ACCESS_TOKEN_KEY = stringPreferencesKey("access_token") + private val REFRESH_TOKEN_KEY = stringPreferencesKey("refresh_token") + } - suspend fun saveTokens(accessToken: String, refreshToken: String) { - context.dataStore.edit { preferences -> - preferences[ACCESS_TOKEN_KEY] = accessToken - preferences[REFRESH_TOKEN_KEY] = refreshToken - } + val accessToken: Flow = + context.dataStore.data.map { preferences -> + preferences[ACCESS_TOKEN_KEY] } - suspend fun getAccessToken(): String? { - return context.dataStore.data.map { it[ACCESS_TOKEN_KEY] }.first() + val refreshToken: Flow = + context.dataStore.data.map { preferences -> + preferences[REFRESH_TOKEN_KEY] } - suspend fun getRefreshToken(): String? { - return context.dataStore.data.map { it[REFRESH_TOKEN_KEY] }.first() + suspend fun saveTokens(accessToken: String, refreshToken: String) { + context.dataStore.edit { preferences -> + preferences[ACCESS_TOKEN_KEY] = accessToken + preferences[REFRESH_TOKEN_KEY] = refreshToken } + } - suspend fun clearTokens() { - context.dataStore.edit { preferences -> - preferences.remove(ACCESS_TOKEN_KEY) - preferences.remove(REFRESH_TOKEN_KEY) - } - } + suspend fun getAccessToken(): String? = context.dataStore.data + .map { it[ACCESS_TOKEN_KEY] } + .first() - suspend fun hasTokens(): Boolean { - val accessToken = getAccessToken() - val refreshToken = getRefreshToken() - return !accessToken.isNullOrBlank() && !refreshToken.isNullOrBlank() + suspend fun getRefreshToken(): String? = context.dataStore.data + .map { it[REFRESH_TOKEN_KEY] } + .first() + + suspend fun clearTokens() { + context.dataStore.edit { preferences -> + preferences.remove(ACCESS_TOKEN_KEY) + preferences.remove(REFRESH_TOKEN_KEY) } + } - /** - * Mock 모드가 해제된 경우, Mock 토큰이 있으면 클리어 - * 앱 시작 시 호출하여 이전 Mock 토큰을 제거 - */ - suspend fun clearMockTokensIfNeeded(useMockApi: Boolean) { - if (!useMockApi) { - val accessToken = getAccessToken() - // Mock 토큰인지 확인 (mock_access_token으로 시작) - if (accessToken != null && accessToken.startsWith("mock_")) { - clearTokens() - } + suspend fun hasTokens(): Boolean { + val accessToken = getAccessToken() + val refreshToken = getRefreshToken() + return !accessToken.isNullOrBlank() && !refreshToken.isNullOrBlank() + } + + /** + * Mock 모드가 해제된 경우, Mock 토큰이 있으면 클리어 + * 앱 시작 시 호출하여 이전 Mock 토큰을 제거 + */ + suspend fun clearMockTokensIfNeeded(useMockApi: Boolean) { + if (!useMockApi) { + val accessToken = getAccessToken() + // Mock 토큰인지 확인 (mock_access_token으로 시작) + if (accessToken != null && accessToken.startsWith("mock_")) { + clearTokens() } } + } + + /** + * JWT 토큰에서 userId 추출 + * JWT payload의 "sub" 필드에서 userId를 가져옴 + */ + suspend fun getUserIdFromToken(): Long? { + val accessToken = getAccessToken() ?: return null + return try { + // JWT는 header.payload.signature 형식 + val parts = accessToken.split(".") + if (parts.size != 3) return null - /** - * JWT 토큰에서 userId 추출 - * JWT payload의 "sub" 필드에서 userId를 가져옴 - */ - suspend fun getUserIdFromToken(): Long? { - val accessToken = getAccessToken() ?: return null - return try { - // JWT는 header.payload.signature 형식 - val parts = accessToken.split(".") - if (parts.size != 3) return null - - // payload 부분 디코딩 - val payload = parts[1] - // Base64 URL-safe 디코딩 - // 패딩이 없을 수 있으므로 추가 - val paddedPayload = when (payload.length % 4) { + // payload 부분 디코딩 + val payload = parts[1] + // Base64 URL-safe 디코딩 + // 패딩이 없을 수 있으므로 추가 + val paddedPayload = + when (payload.length % 4) { 0 -> payload 2 -> "$payload==" 3 -> "$payload=" else -> payload } - - val decodedBytes = android.util.Base64.decode( + + val decodedBytes = + android.util.Base64.decode( paddedPayload, android.util.Base64.URL_SAFE ) - val payloadJson = String(decodedBytes, Charsets.UTF_8) - - // JSON 파싱 - val jsonObject = org.json.JSONObject(payloadJson) - val sub = jsonObject.optString("sub", null) - sub?.toLongOrNull() - } catch (e: Exception) { - null - } + val payloadJson = String(decodedBytes, Charsets.UTF_8) + + // JSON 파싱 + val jsonObject = org.json.JSONObject(payloadJson) + val sub = jsonObject.optString("sub", null) + sub?.toLongOrNull() + } catch (e: Exception) { + null } } +} diff --git a/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/CategoryChip.kt b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/CategoryChip.kt index 2f9d145..074a313 100644 --- a/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/CategoryChip.kt +++ b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/CategoryChip.kt @@ -14,7 +14,7 @@ fun CategoryChip( category: Category, isSelected: Boolean, onClick: () -> Unit, - modifier: Modifier = Modifier, + modifier: Modifier = Modifier ) { FilterChip( selected = isSelected, @@ -25,8 +25,8 @@ fun CategoryChip( colors = FilterChipDefaults.filterChipColors( selectedContainerColor = MaterialTheme.colorScheme.primary, - selectedLabelColor = MaterialTheme.colorScheme.onPrimary, - ), + selectedLabelColor = MaterialTheme.colorScheme.onPrimary + ) ) } @@ -35,17 +35,17 @@ fun CategoryChipRow( categories: List, selectedCategory: Category, onCategorySelected: (Category) -> Unit, - modifier: Modifier = Modifier, + modifier: Modifier = Modifier ) { Row( modifier = modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) ) { categories.forEach { category -> CategoryChip( category = category, isSelected = category == selectedCategory, - onClick = { onCategorySelected(category) }, + onClick = { onCategorySelected(category) } ) } } @@ -57,7 +57,7 @@ private fun CategoryChipPreview() { CategoryChip( category = Category.SUBWAY, isSelected = true, - onClick = {}, + onClick = {} ) } @@ -67,6 +67,6 @@ private fun CategoryChipRowPreview() { CategoryChipRow( categories = listOf(Category.ALL, Category.SUBWAY, Category.HAIDILAO, Category.CONVENIENCE), selectedCategory = Category.SUBWAY, - onCategorySelected = {}, + onCategorySelected = {} ) } diff --git a/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/CombinationCard.kt b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/CombinationCard.kt index 32565db..ad7c51a 100644 --- a/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/CombinationCard.kt +++ b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/CombinationCard.kt @@ -30,13 +30,13 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage -import com.example.hackathon.core.model.Category -import com.example.hackathon.core.model.Combination -import com.example.hackathon.core.model.User import com.example.hackathon.core.designsystem.theme.Gray50 import com.example.hackathon.core.designsystem.theme.Gray700 import com.example.hackathon.core.designsystem.theme.HackathonTheme import com.example.hackathon.core.designsystem.theme.Primary +import com.example.hackathon.core.model.Category +import com.example.hackathon.core.model.Combination +import com.example.hackathon.core.model.User private const val SHADOW_SIZE = 8 @@ -45,7 +45,7 @@ fun CombinationCard( combination: Combination, onClick: () -> Unit, onLikeClick: (() -> Unit)? = null, - modifier: Modifier = Modifier, + modifier: Modifier = Modifier ) { // 🔹 shadow 파라미터 val corner = 15.dp @@ -61,27 +61,27 @@ fun CombinationCard( .fillMaxWidth() .padding( end = shadowBlur + shadowOffsetX, - bottom = shadowBlur + shadowOffsetY, - ) - .drawBehind { + bottom = shadowBlur + shadowOffsetY + ).drawBehind { val left = shadowPad.toPx() val top = shadowPad.toPx() val right = size.width - shadowPad.toPx() val bottom = size.height - shadowPad.toPx() - val paint = Paint().apply { - asFrameworkPaint().apply { - isAntiAlias = true - color = android.graphics.Color.TRANSPARENT + val paint = + Paint().apply { + asFrameworkPaint().apply { + isAntiAlias = true + color = android.graphics.Color.TRANSPARENT - setShadowLayer( - shadowBlur.toPx(), - shadowOffsetX.toPx(), - shadowOffsetY.toPx(), - Color.Black.copy(alpha = shadowAlpha).toArgb(), - ) + setShadowLayer( + shadowBlur.toPx(), + shadowOffsetX.toPx(), + shadowOffsetY.toPx(), + Color.Black.copy(alpha = shadowAlpha).toArgb() + ) + } } - } drawIntoCanvas { canvas -> canvas.drawRoundRect( @@ -91,20 +91,20 @@ fun CombinationCard( bottom, corner.toPx(), corner.toPx(), - paint, + paint ) } - }, + } ) { Card( onClick = onClick, modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(corner), colors = CardDefaults.cardColors(containerColor = Color.White), - elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) ) { Column( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth() ) { // 이미지 Box( @@ -115,16 +115,16 @@ fun CombinationCard( .clip( RoundedCornerShape( topStart = corner, - topEnd = corner, - ), - ), + topEnd = corner + ) + ) ) { if (combination.imageUrl != null) { AsyncImage( model = combination.imageUrl, contentDescription = combination.title, modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop, + contentScale = ContentScale.Crop ) } else { Box( @@ -132,12 +132,12 @@ fun CombinationCard( Modifier .fillMaxSize() .background(Gray50), - contentAlignment = Alignment.Center, + contentAlignment = Alignment.Center ) { Text( text = "이미지 없음", style = HackathonTheme.typography.Body_medium, - color = Gray700.copy(alpha = 0.5f), + color = Gray700.copy(alpha = 0.5f) ) } } @@ -148,7 +148,7 @@ fun CombinationCard( modifier = Modifier .fillMaxWidth() - .padding(16.dp), + .padding(16.dp) ) { Spacer(modifier = Modifier.height(12.dp)) @@ -156,7 +156,7 @@ fun CombinationCard( FlowRow( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(6.dp), - verticalArrangement = Arrangement.spacedBy(6.dp), + verticalArrangement = Arrangement.spacedBy(6.dp) ) { combination.tags.forEachIndexed { index, tag -> val isFirstTag = index == 0 @@ -167,27 +167,25 @@ fun CombinationCard( if (isFirstTag) { Modifier.background( Primary, - RoundedCornerShape(15.dp), + RoundedCornerShape(15.dp) ) } else { Modifier .background( Color.White, - RoundedCornerShape(15.dp), - ) - .border( + RoundedCornerShape(15.dp) + ).border( width = 1.dp, color = Primary, - shape = RoundedCornerShape(15.dp), + shape = RoundedCornerShape(15.dp) ) - }, - ) - .padding(horizontal = 10.dp, vertical = 4.dp), + } + ).padding(horizontal = 10.dp, vertical = 4.dp) ) { Text( text = tag, style = HackathonTheme.typography.Caption_medium, - color = if (isFirstTag) Color.White else Primary, + color = if (isFirstTag) Color.White else Primary ) } } @@ -200,7 +198,7 @@ fun CombinationCard( style = HackathonTheme.typography.Head2_bold, maxLines = 2, overflow = TextOverflow.Ellipsis, - color = Color.Black, + color = Color.Black ) Spacer(modifier = Modifier.height(8.dp)) @@ -210,13 +208,13 @@ fun CombinationCard( style = HackathonTheme.typography.Body_medium, color = Color(0xFFC0C0C0), maxLines = 2, - overflow = TextOverflow.Ellipsis, + overflow = TextOverflow.Ellipsis ) Spacer(modifier = Modifier.height(12.dp)) HorizontalDivider( thickness = 0.5.dp, - color = Color(0xFFC0C0C0), + color = Color(0xFFC0C0C0) ) Spacer(modifier = Modifier.height(12.dp)) @@ -224,7 +222,7 @@ fun CombinationCard( Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, + verticalAlignment = Alignment.CenterVertically ) { Row( modifier = @@ -234,7 +232,7 @@ fun CombinationCard( Modifier }, verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(6.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp) ) { Icon( imageVector = @@ -245,19 +243,19 @@ fun CombinationCard( }, contentDescription = "좋아요", tint = Primary, - modifier = Modifier.size(18.dp), + modifier = Modifier.size(18.dp) ) Text( text = String.format("%,d", combination.likeCount), style = HackathonTheme.typography.Body_medium, - color = Color.Black, + color = Color.Black ) } Text( text = combination.author.nickname, style = HackathonTheme.typography.Body_medium, - color = Color.Black, + color = Color.Black ) } } @@ -266,8 +264,6 @@ fun CombinationCard( } } - - @Preview(showBackground = true, name = "Liked State") @Composable private fun CombinationCardLikedPreview() { @@ -285,9 +281,9 @@ private fun CombinationCardLikedPreview() { author = User(id = "1", nickname = "윤상00", profileImageUrl = null), likeCount = 1120, isLiked = true, - createdAt = System.currentTimeMillis().toString(), + createdAt = System.currentTimeMillis().toString() ), - onClick = {}, + onClick = {} ) } } @@ -309,9 +305,9 @@ private fun CombinationCardNotLikedPreview() { author = User(id = "1", nickname = "윤상00", profileImageUrl = null), likeCount = 1120, isLiked = false, - createdAt = System.currentTimeMillis().toString(), + createdAt = System.currentTimeMillis().toString() ), - onClick = {}, + onClick = {} ) } } diff --git a/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/IngredientRow.kt b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/IngredientRow.kt index 0540cca..8f8835b 100644 --- a/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/IngredientRow.kt +++ b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/IngredientRow.kt @@ -12,21 +12,18 @@ import androidx.compose.ui.unit.dp import com.example.hackathon.core.designsystem.theme.HackathonTheme @Composable -fun IngredientRow( - name: String, - amount: String, -) { +fun IngredientRow(name: String, amount: String) { Row( modifier = Modifier .fillMaxWidth() .padding(vertical = 6.dp), - verticalAlignment = Alignment.CenterVertically, + verticalAlignment = Alignment.CenterVertically ) { Text( text = name, style = HackathonTheme.typography.Body_medium, - color = HackathonTheme.colors.black, + color = HackathonTheme.colors.black ) Spacer(modifier = Modifier.weight(1f)) @@ -34,7 +31,7 @@ fun IngredientRow( Text( text = amount, style = HackathonTheme.typography.Body_medium, - color = HackathonTheme.colors.gray700, + color = HackathonTheme.colors.gray700 ) } } diff --git a/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/SearchBar.kt b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/SearchBar.kt index fe29fe9..2ba7103 100644 --- a/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/SearchBar.kt +++ b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/SearchBar.kt @@ -15,7 +15,7 @@ fun SearchBar( query: String, onQueryChange: (String) -> Unit, modifier: Modifier = Modifier, - placeholder: String = "검색어를 입력하세요", + placeholder: String = "검색어를 입력하세요" ) { OutlinedTextField( value = query, @@ -26,15 +26,15 @@ fun SearchBar( leadingIcon = { Icon( imageVector = Icons.Default.Search, - contentDescription = "검색", + contentDescription = "검색" ) }, singleLine = true, colors = OutlinedTextFieldDefaults.colors( focusedContainerColor = MaterialTheme.colorScheme.surface, - unfocusedContainerColor = MaterialTheme.colorScheme.surface, - ), + unfocusedContainerColor = MaterialTheme.colorScheme.surface + ) ) } @@ -43,6 +43,6 @@ fun SearchBar( private fun SearchBarPreview() { SearchBar( query = "", - onQueryChange = {}, + onQueryChange = {} ) } diff --git a/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/TagChip.kt b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/TagChip.kt index 6006f33..15d857c 100644 --- a/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/TagChip.kt +++ b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/TagChip.kt @@ -21,23 +21,19 @@ import androidx.compose.ui.unit.dp import com.example.hackathon.core.designsystem.theme.Primary @Composable -fun TagChip( - text: String, - onRemove: () -> Unit, - modifier: Modifier = Modifier, -) { +fun TagChip(text: String, onRemove: () -> Unit, modifier: Modifier = Modifier) { Row( modifier = modifier .background(Primary, RoundedCornerShape(15.dp)) .padding(horizontal = 10.dp, vertical = 6.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(6.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp) ) { Text( text = text, style = MaterialTheme.typography.bodyMedium, - color = Color.White, + color = Color.White ) FilledTonalIconButton( onClick = onRemove, @@ -45,14 +41,16 @@ fun TagChip( Modifier .background(Color.White.copy(alpha = 0.2f), shape = CircleShape) .height(24.dp), - colors = IconButtonDefaults.filledTonalIconButtonColors(containerColor = Color.Transparent), + colors = IconButtonDefaults.filledTonalIconButtonColors( + containerColor = Color.Transparent + ), content = { Text( text = "X", color = Color.White, - textAlign = TextAlign.Center, + textAlign = TextAlign.Center ) - }, + } ) } } @@ -62,6 +60,6 @@ fun TagChip( private fun TagChipPreview() { TagChip( text = "#서브웨이", - onRemove = {}, + onRemove = {} ) } diff --git a/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/TopAppLogoBar.kt b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/TopAppLogoBar.kt index f4a522d..d730b56 100644 --- a/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/TopAppLogoBar.kt +++ b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/component/TopAppLogoBar.kt @@ -31,10 +31,11 @@ fun TopAppLogoBar(modifier: Modifier = Modifier) { Image( painter = painterResource(R.drawable.ic_logo_rec), contentDescription = "logo", - modifier = modifier - .width(237.71429.dp) - .height(65.dp), - contentScale = ContentScale.Fit, + modifier = + modifier + .width(237.71429.dp) + .height(65.dp), + contentScale = ContentScale.Fit ) } } diff --git a/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/theme/Color.kt b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/theme/Color.kt index 12fe2ec..674de18 100644 --- a/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/theme/Color.kt +++ b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/theme/Color.kt @@ -47,7 +47,7 @@ data class HackathonColors( val background: Color, val surface: Color, val success: Color, - val error: Color, + val error: Color ) val defaultHackathonColors = @@ -61,7 +61,7 @@ val defaultHackathonColors = background = Background, surface = Surface, success = Success, - error = Error, + error = Error ) val LocalHackathonColors = staticCompositionLocalOf { defaultHackathonColors } diff --git a/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/theme/Theme.kt b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/theme/Theme.kt index 56afeea..76156f7 100644 --- a/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/theme/Theme.kt +++ b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/theme/Theme.kt @@ -20,18 +20,18 @@ private val LightColorScheme = onBackground = Black, onSurface = Black, error = Error, - onError = White, + onError = White ) @Composable fun HackathonTheme( // 다크 모드 미지원 - 항상 라이트 모드만 사용 - content: @Composable () -> Unit, + content: @Composable () -> Unit ) { MaterialTheme( colorScheme = LightColorScheme, typography = Typography, - content = content, + content = content ) } diff --git a/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/theme/Type.kt b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/theme/Type.kt index 75d1ea2..80f40dc 100644 --- a/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/theme/Type.kt +++ b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/theme/Type.kt @@ -29,7 +29,7 @@ data class HackathonTypography( val Sub2_medium: TextStyle, val Body_semibold: TextStyle, val Body_medium: TextStyle, - val Caption_medium: TextStyle, + val Caption_medium: TextStyle ) val defaultHackathonTypography = @@ -38,68 +38,68 @@ val defaultHackathonTypography = TextStyle( fontFamily = FontBold, fontSize = 24.sp, - lineHeight = 28.sp, + lineHeight = 28.sp ), Head1_semibold = TextStyle( fontFamily = FontSemiBold, fontSize = 24.sp, - lineHeight = 28.sp, + lineHeight = 28.sp ), Head2_bold = TextStyle( fontFamily = FontBold, fontSize = 22.sp, - lineHeight = 24.sp, + lineHeight = 24.sp ), Head2_semibold = TextStyle( fontFamily = FontSemiBold, fontSize = 22.sp, - lineHeight = 24.sp, + lineHeight = 24.sp ), Sub1_semibold = TextStyle( fontFamily = FontSemiBold, fontSize = 18.sp, - lineHeight = 20.sp, + lineHeight = 20.sp ), Sub1_medium = TextStyle( fontFamily = FontMedium, fontSize = 18.sp, - lineHeight = 20.sp, + lineHeight = 20.sp ), Sub2_semibold = TextStyle( fontFamily = FontSemiBold, fontSize = 16.sp, - lineHeight = 28.sp, + lineHeight = 28.sp ), Sub2_medium = TextStyle( fontFamily = FontBold, fontSize = 16.sp, - lineHeight = 18.sp, + lineHeight = 18.sp ), Body_semibold = TextStyle( fontFamily = FontSemiBold, fontSize = 14.sp, - lineHeight = 16.sp, + lineHeight = 16.sp ), Body_medium = TextStyle( fontFamily = FontMedium, fontSize = 14.sp, - lineHeight = 16.sp, + lineHeight = 16.sp ), Caption_medium = TextStyle( fontFamily = FontMedium, fontSize = 12.sp, - lineHeight = 14.sp, - ), + lineHeight = 14.sp + ) ) val LocalHackathonTypography = staticCompositionLocalOf { defaultHackathonTypography } @@ -112,8 +112,8 @@ val Typography = fontWeight = FontWeight.Normal, fontSize = 16.sp, lineHeight = 24.sp, - letterSpacing = 0.5.sp, - ), + letterSpacing = 0.5.sp + ) /* Other default text styles to override titleLarge = TextStyle( fontFamily = FontFamily.Default, diff --git a/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/util/ModifierExt.kt b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/util/ModifierExt.kt index 30a27df..95f56fa 100644 --- a/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/util/ModifierExt.kt +++ b/core/designsystem/src/main/java/com/example/hackathon/core/designsystem/util/ModifierExt.kt @@ -23,27 +23,22 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -inline fun Modifier.noRippleClickable(crossinline onClick: () -> Unit): Modifier = - composed { - clickable( - indication = null, - interactionSource = remember { MutableInteractionSource() }, - ) { - onClick() - } +inline fun Modifier.noRippleClickable(crossinline onClick: () -> Unit): Modifier = composed { + clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + onClick() } +} -fun Modifier.addFocusCleaner( - focusManager: FocusManager, - doOnClear: () -> Unit = {}, -): Modifier { - return this.pointerInput(Unit) { +fun Modifier.addFocusCleaner(focusManager: FocusManager, doOnClear: () -> Unit = {}): Modifier = + this.pointerInput(Unit) { detectTapGestures(onTap = { doOnClear() focusManager.clearFocus() }) } -} @Composable fun Modifier.dropShadow( @@ -56,16 +51,17 @@ fun Modifier.dropShadow( ) = composed { val density = LocalDensity.current - val paint = remember(color, blur) { - Paint().apply { - this.color = color - val blurPx = with(density) { blur.toPx() } - if (blurPx > 0f) { - this.asFrameworkPaint().maskFilter = - BlurMaskFilter(blurPx, BlurMaskFilter.Blur.NORMAL) + val paint = + remember(color, blur) { + Paint().apply { + this.color = color + val blurPx = with(density) { blur.toPx() } + if (blurPx > 0f) { + this.asFrameworkPaint().maskFilter = + BlurMaskFilter(blurPx, BlurMaskFilter.Blur.NORMAL) + } } } - } drawBehind { val spreadPx = spread.toPx() diff --git a/core/domain/src/main/java/com/example/hackathon/core/domain/repository/AuthRepository.kt b/core/domain/src/main/java/com/example/hackathon/core/domain/repository/AuthRepository.kt index 22bae01..ec5409b 100644 --- a/core/domain/src/main/java/com/example/hackathon/core/domain/repository/AuthRepository.kt +++ b/core/domain/src/main/java/com/example/hackathon/core/domain/repository/AuthRepository.kt @@ -4,16 +4,10 @@ import com.example.hackathon.core.model.User interface AuthRepository { // 1. 회원가입 - suspend fun signup( - password: String, - nickname: String, - ): Result + suspend fun signup(password: String, nickname: String): Result // 2. 로그인 - suspend fun login( - nickname: String, - password: String, - ): Result + suspend fun login(nickname: String, password: String): Result // 3. 토큰 재발급 suspend fun reissue(): Result @@ -25,18 +19,8 @@ interface AuthRepository { suspend fun hasValidTokens(): Boolean } -data class SignupResult( - val id: Long, - val nickname: String, -) - -data class LoginResult( - val accessToken: String, - val refreshToken: String, - val user: User, -) - -data class ReissueResult( - val accessToken: String, - val refreshToken: String?, -) +data class SignupResult(val id: Long, val nickname: String) + +data class LoginResult(val accessToken: String, val refreshToken: String, val user: User) + +data class ReissueResult(val accessToken: String, val refreshToken: String?) diff --git a/core/domain/src/main/java/com/example/hackathon/core/domain/repository/CombinationRepository.kt b/core/domain/src/main/java/com/example/hackathon/core/domain/repository/CombinationRepository.kt index d8be919..b3e5949 100644 --- a/core/domain/src/main/java/com/example/hackathon/core/domain/repository/CombinationRepository.kt +++ b/core/domain/src/main/java/com/example/hackathon/core/domain/repository/CombinationRepository.kt @@ -5,7 +5,11 @@ import com.example.hackathon.core.model.Combination import com.example.hackathon.core.model.RecipeDetail interface CombinationRepository { - suspend fun getCombinations(category: Category? = null, page: Int = 1, pageSize: Int = 10): Result> + suspend fun getCombinations( + category: Category? = null, + page: Int = 1, + pageSize: Int = 10 + ): Result> suspend fun getCombinationById(id: String): Result @@ -16,7 +20,7 @@ interface CombinationRepository { ingredients: List, tags: List = emptyList(), imageUri: android.net.Uri? = null, - isPublic: Boolean = true, + isPublic: Boolean = true ): Result suspend fun updateCombination( @@ -27,7 +31,7 @@ interface CombinationRepository { ingredients: List, tags: List = emptyList(), imageUri: android.net.Uri? = null, - isPublic: Boolean = true, + isPublic: Boolean = true ): Result suspend fun deleteCombination(id: String): Result diff --git a/core/domain/src/main/java/com/example/hackathon/core/domain/repository/UserRepository.kt b/core/domain/src/main/java/com/example/hackathon/core/domain/repository/UserRepository.kt index c5c15ca..5a7459d 100644 --- a/core/domain/src/main/java/com/example/hackathon/core/domain/repository/UserRepository.kt +++ b/core/domain/src/main/java/com/example/hackathon/core/domain/repository/UserRepository.kt @@ -5,8 +5,5 @@ import com.example.hackathon.core.model.User interface UserRepository { suspend fun getProfile(): Result - suspend fun updateProfile( - nickname: String, - profileImageUrl: String? = null, - ): Result + suspend fun updateProfile(nickname: String, profileImageUrl: String? = null): Result } diff --git a/core/model/src/main/java/com/example/hackathon/core/model/Category.kt b/core/model/src/main/java/com/example/hackathon/core/model/Category.kt index 28e9850..ba177a9 100644 --- a/core/model/src/main/java/com/example/hackathon/core/model/Category.kt +++ b/core/model/src/main/java/com/example/hackathon/core/model/Category.kt @@ -4,5 +4,5 @@ enum class Category(val displayName: String) { ALL("#전체"), HAIDILAO("#하이디라오"), SUBWAY("#서브웨이"), - CONVENIENCE("#편의점"), + CONVENIENCE("#편의점") } diff --git a/core/model/src/main/java/com/example/hackathon/core/model/Combination.kt b/core/model/src/main/java/com/example/hackathon/core/model/Combination.kt index ae99968..48bdcd4 100644 --- a/core/model/src/main/java/com/example/hackathon/core/model/Combination.kt +++ b/core/model/src/main/java/com/example/hackathon/core/model/Combination.kt @@ -12,5 +12,5 @@ data class Combination( val likeCount: Int, // 현재 사용자가 좋아요를 눌렀는지 여부 val isLiked: Boolean = false, - val createdAt: String, + val createdAt: String ) diff --git a/core/model/src/main/java/com/example/hackathon/core/model/DummyData.kt b/core/model/src/main/java/com/example/hackathon/core/model/DummyData.kt index d5f3508..c963839 100644 --- a/core/model/src/main/java/com/example/hackathon/core/model/DummyData.kt +++ b/core/model/src/main/java/com/example/hackathon/core/model/DummyData.kt @@ -13,7 +13,7 @@ object DummyData { User( id = "user1", nickname = "쩝쩝박사", - profileImageUrl = null, + profileImageUrl = null ) // 현재 로그인한 사용자 정보 (임시 저장) @@ -33,13 +33,13 @@ object DummyData { "에그마요 15cm", "아보카도 추가", "올리브 오일", - "후추", + "후추" ), tags = listOf("#태그원", "#태그투", "#태그쓰리"), author = dummyUser, likeCount = 42, isLiked = false, - createdAt = "2024-01-05", + createdAt = "2024-01-05" ), Combination( id = "2", @@ -52,13 +52,13 @@ object DummyData { "마늘 다진 것", "고추기름", "참기름", - "간장", + "간장" ), tags = listOf("#태그포", "#태그파이브", "#태그식스"), author = dummyUser, likeCount = 38, isLiked = false, - createdAt = "2024-01-04", + createdAt = "2024-01-04" ), Combination( id = "3", @@ -70,13 +70,13 @@ object DummyData { listOf( "불닭볶음면", "슬라이스 치즈 2장", - "계란", + "계란" ), tags = listOf("#태그세븐", "#태그에이트", "#태그나인"), author = dummyUser, likeCount = 56, isLiked = false, - createdAt = "2024-01-03", + createdAt = "2024-01-03" ), Combination( id = "4", @@ -89,7 +89,7 @@ object DummyData { author = dummyUser, likeCount = 28, isLiked = false, - createdAt = "2024-01-02", + createdAt = "2024-01-02" ), Combination( id = "5", @@ -102,7 +102,7 @@ object DummyData { author = dummyUser, likeCount = 45, isLiked = false, - createdAt = "2024-01-01", + createdAt = "2024-01-01" ), Combination( id = "6", @@ -115,7 +115,7 @@ object DummyData { author = dummyUser, likeCount = 33, isLiked = false, - createdAt = "2023-12-30", + createdAt = "2023-12-30" ), Combination( id = "7", @@ -128,7 +128,7 @@ object DummyData { author = dummyUser, likeCount = 51, isLiked = false, - createdAt = "2023-12-29", + createdAt = "2023-12-29" ), Combination( id = "8", @@ -141,7 +141,7 @@ object DummyData { author = dummyUser, likeCount = 67, isLiked = false, - createdAt = "2023-12-28", + createdAt = "2023-12-28" ), Combination( id = "9", @@ -154,7 +154,7 @@ object DummyData { author = dummyUser, likeCount = 89, isLiked = false, - createdAt = "2023-12-27", + createdAt = "2023-12-27" ), Combination( id = "10", @@ -167,7 +167,7 @@ object DummyData { author = dummyUser, likeCount = 34, isLiked = false, - createdAt = "2023-12-26", + createdAt = "2023-12-26" ), Combination( id = "11", @@ -180,7 +180,7 @@ object DummyData { author = dummyUser, likeCount = 72, isLiked = false, - createdAt = "2023-12-25", + createdAt = "2023-12-25" ), Combination( id = "12", @@ -193,7 +193,7 @@ object DummyData { author = dummyUser, likeCount = 64, isLiked = false, - createdAt = "2023-12-24", + createdAt = "2023-12-24" ), Combination( id = "13", @@ -206,7 +206,7 @@ object DummyData { author = dummyUser, likeCount = 58, isLiked = false, - createdAt = "2023-12-23", + createdAt = "2023-12-23" ), Combination( id = "14", @@ -219,7 +219,7 @@ object DummyData { author = dummyUser, likeCount = 41, isLiked = false, - createdAt = "2023-12-22", + createdAt = "2023-12-22" ), Combination( id = "15", @@ -232,7 +232,7 @@ object DummyData { author = dummyUser, likeCount = 47, isLiked = false, - createdAt = "2023-12-21", + createdAt = "2023-12-21" ), Combination( id = "16", @@ -245,7 +245,7 @@ object DummyData { author = dummyUser, likeCount = 63, isLiked = false, - createdAt = "2023-12-20", + createdAt = "2023-12-20" ), Combination( id = "17", @@ -258,7 +258,7 @@ object DummyData { author = dummyUser, likeCount = 55, isLiked = false, - createdAt = "2023-12-19", + createdAt = "2023-12-19" ), Combination( id = "18", @@ -271,7 +271,7 @@ object DummyData { author = dummyUser, likeCount = 76, isLiked = false, - createdAt = "2023-12-18", + createdAt = "2023-12-18" ), Combination( id = "19", @@ -284,7 +284,7 @@ object DummyData { author = dummyUser, likeCount = 49, isLiked = false, - createdAt = "2023-12-17", + createdAt = "2023-12-17" ), Combination( id = "20", @@ -297,7 +297,7 @@ object DummyData { author = dummyUser, likeCount = 82, isLiked = false, - createdAt = "2023-12-16", + createdAt = "2023-12-16" ), Combination( id = "21", @@ -310,7 +310,7 @@ object DummyData { author = dummyUser, likeCount = 39, isLiked = false, - createdAt = "2023-12-15", + createdAt = "2023-12-15" ), Combination( id = "22", @@ -323,8 +323,8 @@ object DummyData { author = dummyUser, likeCount = 54, isLiked = false, - createdAt = "2023-12-14", - ), + createdAt = "2023-12-14" + ) ) fun getRecipeDetailById(id: Long): RecipeDetail? { @@ -343,18 +343,18 @@ object DummyData { combination.ingredients.map { Ingredient( name = it, - amount = "", + amount = "" ) }, stats = Stats(likesCount = combination.likeCount), userInteraction = UserInteraction( isLiked = combination.isLiked, - isMine = combination.author.id == currentUser?.id, + isMine = combination.author.id == currentUser?.id ), tags = combination.tags, createdAt = combination.createdAt, - updatedAt = combination.createdAt, + updatedAt = combination.createdAt ) } } diff --git a/core/model/src/main/java/com/example/hackathon/core/model/RecipeDetail.kt b/core/model/src/main/java/com/example/hackathon/core/model/RecipeDetail.kt index 6cf5361..b40e96a 100644 --- a/core/model/src/main/java/com/example/hackathon/core/model/RecipeDetail.kt +++ b/core/model/src/main/java/com/example/hackathon/core/model/RecipeDetail.kt @@ -12,19 +12,11 @@ data class RecipeDetail( val userInteraction: UserInteraction, val tags: List, val createdAt: String, - val updatedAt: String, + val updatedAt: String ) -data class Ingredient( - val name: String, - val amount: String, -) +data class Ingredient(val name: String, val amount: String) -data class Stats( - val likesCount: Int, -) +data class Stats(val likesCount: Int) -data class UserInteraction( - val isLiked: Boolean, - val isMine: Boolean, -) +data class UserInteraction(val isLiked: Boolean, val isMine: Boolean) diff --git a/core/model/src/main/java/com/example/hackathon/core/model/RecipeRanking.kt b/core/model/src/main/java/com/example/hackathon/core/model/RecipeRanking.kt index 52782a3..b77472c 100644 --- a/core/model/src/main/java/com/example/hackathon/core/model/RecipeRanking.kt +++ b/core/model/src/main/java/com/example/hackathon/core/model/RecipeRanking.kt @@ -1,9 +1,6 @@ package com.example.hackathon.core.model -data class RecipeRanking( - val currentCategory: String, - val list: List, -) +data class RecipeRanking(val currentCategory: String, val list: List) data class RecipeRankingItem( val title: String, @@ -11,5 +8,5 @@ data class RecipeRankingItem( val viewCount: Int, val likes: Int, val username: String, - val thumbnailUrl: String? = null, + val thumbnailUrl: String? = null ) diff --git a/core/model/src/main/java/com/example/hackathon/core/model/User.kt b/core/model/src/main/java/com/example/hackathon/core/model/User.kt index 796c902..a38293c 100644 --- a/core/model/src/main/java/com/example/hackathon/core/model/User.kt +++ b/core/model/src/main/java/com/example/hackathon/core/model/User.kt @@ -1,7 +1,3 @@ package com.example.hackathon.core.model -data class User( - val id: String, - val nickname: String, - val profileImageUrl: String?, -) +data class User(val id: String, val nickname: String, val profileImageUrl: String?) diff --git a/core/network/src/main/java/com/example/hackathon/core/network/AuthInterceptor.kt b/core/network/src/main/java/com/example/hackathon/core/network/AuthInterceptor.kt index f91abb7..5d406d3 100644 --- a/core/network/src/main/java/com/example/hackathon/core/network/AuthInterceptor.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/AuthInterceptor.kt @@ -1,14 +1,14 @@ package com.example.hackathon.core.network -import com.example.hackathon.core.network.dto.request.ReissueRequest import com.example.hackathon.core.datastore.TokenManager +import com.example.hackathon.core.network.dto.request.ReissueRequest import com.example.hackathon.core.network.service.AuthService +import javax.inject.Inject +import javax.inject.Singleton import kotlinx.coroutines.runBlocking import okhttp3.Interceptor import okhttp3.Request import okhttp3.Response -import javax.inject.Inject -import javax.inject.Singleton /** * JWT 토큰을 자동으로 추가하고, 401 응답 시 토큰 재발급을 시도하는 Interceptor @@ -22,86 +22,92 @@ import javax.inject.Singleton */ @Singleton class AuthInterceptor - @Inject - constructor( - private val tokenManager: TokenManager, - private val authService: AuthService, - ) : Interceptor { - override fun intercept(chain: Interceptor.Chain): Response { - val originalRequest = chain.request() +@Inject +constructor( + private val tokenManager: TokenManager, + private val authService: AuthService +) : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val originalRequest = chain.request() - // 인증이 필요 없는 엔드포인트는 그대로 진행 - if (!isAuthRequired(originalRequest)) { - return chain.proceed(originalRequest) - } + // 인증이 필요 없는 엔드포인트는 그대로 진행 + if (!isAuthRequired(originalRequest)) { + return chain.proceed(originalRequest) + } - // AccessToken 추가 - val accessToken = runBlocking { tokenManager.getAccessToken() } - val authenticatedRequest = if (accessToken != null) { - originalRequest.newBuilder() + // AccessToken 추가 + val accessToken = runBlocking { tokenManager.getAccessToken() } + val authenticatedRequest = + if (accessToken != null) { + originalRequest + .newBuilder() .header("Authorization", "Bearer $accessToken") .build() } else { originalRequest } - var response = chain.proceed(authenticatedRequest) + var response = chain.proceed(authenticatedRequest) - // 401 Unauthorized 응답 시 토큰 재발급 시도 - if (response.code == 401) { - response.close() // 리소스 정리 + // 401 Unauthorized 응답 시 토큰 재발급 시도 + if (response.code == 401) { + response.close() // 리소스 정리 - val newAccessToken = runBlocking { + val newAccessToken = + runBlocking { reissueToken() } - // 재발급 성공 시 원래 요청을 새 토큰으로 재시도 - if (newAccessToken != null) { - val retryRequest = originalRequest.newBuilder() + // 재발급 성공 시 원래 요청을 새 토큰으로 재시도 + if (newAccessToken != null) { + val retryRequest = + originalRequest + .newBuilder() .header("Authorization", "Bearer $newAccessToken") .build() - response = chain.proceed(retryRequest) - } - // 재발급 실패 시 원래 401 응답을 그대로 반환 (이미 close됨) + response = chain.proceed(retryRequest) } - - return response + // 재발급 실패 시 원래 401 응답을 그대로 반환 (이미 close됨) } - /** - * RefreshToken으로 새 AccessToken을 발급받음 - * @return 새 AccessToken, 실패 시 null - */ - private suspend fun reissueToken(): String? { - return try { - val refreshToken = tokenManager.getRefreshToken() + return response + } + + /** + * RefreshToken으로 새 AccessToken을 발급받음 + * @return 새 AccessToken, 실패 시 null + */ + private suspend fun reissueToken(): String? { + return try { + val refreshToken = + tokenManager.getRefreshToken() ?: return null - val request = ReissueRequest(refreshToken = refreshToken) - val reissueResponse = authService.reissue(request) + val request = ReissueRequest(refreshToken = refreshToken) + val reissueResponse = authService.reissue(request) - // 서버 응답: {"accessToken":"...","refreshToken":"..."} (BaseResponse 래퍼 없음) - val newAccessToken = reissueResponse.accessToken - val newRefreshToken = reissueResponse.refreshToken ?: refreshToken + // 서버 응답: {"accessToken":"...","refreshToken":"..."} (BaseResponse 래퍼 없음) + val newAccessToken = reissueResponse.accessToken + val newRefreshToken = reissueResponse.refreshToken ?: refreshToken - // 새 토큰 저장 - tokenManager.saveTokens(newAccessToken, newRefreshToken) - newAccessToken - } catch (e: Exception) { - // 재발급 실패 (네트워크 에러, 파싱 에러 등) - // android.util.Log.e("AuthInterceptor", "Reissue exception", e) - null - } + // 새 토큰 저장 + tokenManager.saveTokens(newAccessToken, newRefreshToken) + newAccessToken + } catch (e: Exception) { + // 재발급 실패 (네트워크 에러, 파싱 에러 등) + // android.util.Log.e("AuthInterceptor", "Reissue exception", e) + null } + } - /** - * 인증이 필요한 엔드포인트인지 확인 - */ - private fun isAuthRequired(request: Request): Boolean { - val path = request.url.encodedPath - // 인증이 필요 없는 엔드포인트 - return !path.contains("/auth/login") && - !path.contains("/auth/signup") && - !path.contains("/auth/reissue") - } + /** + * 인증이 필요한 엔드포인트인지 확인 + */ + private fun isAuthRequired(request: Request): Boolean { + val path = request.url.encodedPath + // 인증이 필요 없는 엔드포인트 + return !path.contains("/auth/login") && + !path.contains("/auth/signup") && + !path.contains("/auth/reissue") } +} diff --git a/core/network/src/main/java/com/example/hackathon/core/network/di/ApiModule.kt b/core/network/src/main/java/com/example/hackathon/core/network/di/ApiModule.kt index 6f10713..aa64c20 100644 --- a/core/network/src/main/java/com/example/hackathon/core/network/di/ApiModule.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/di/ApiModule.kt @@ -8,25 +8,29 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import retrofit2.Retrofit import javax.inject.Singleton +import retrofit2.Retrofit @Module @InstallIn(SingletonComponent::class) object ApiModule { @Provides @Singleton - fun providesHomeService(@MainRetrofit retrofit: Retrofit): HomeService = retrofit.create(HomeService::class.java) + fun providesHomeService(@MainRetrofit retrofit: Retrofit): HomeService = + retrofit.create(HomeService::class.java) @Provides @Singleton - fun providesCombinationService(@MainRetrofit retrofit: Retrofit): CombinationService = retrofit.create(CombinationService::class.java) + fun providesCombinationService(@MainRetrofit retrofit: Retrofit): CombinationService = + retrofit.create(CombinationService::class.java) @Provides @Singleton - fun providesUserService(@MainRetrofit retrofit: Retrofit): UserService = retrofit.create(UserService::class.java) + fun providesUserService(@MainRetrofit retrofit: Retrofit): UserService = + retrofit.create(UserService::class.java) @Provides @Singleton - fun providesAuthService(@AuthRetrofit retrofit: Retrofit): AuthService = retrofit.create(AuthService::class.java) + fun providesAuthService(@AuthRetrofit retrofit: Retrofit): AuthService = + retrofit.create(AuthService::class.java) } diff --git a/core/network/src/main/java/com/example/hackathon/core/network/di/NetworkModule.kt b/core/network/src/main/java/com/example/hackathon/core/network/di/NetworkModule.kt index 6af9cec..d247b93 100644 --- a/core/network/src/main/java/com/example/hackathon/core/network/di/NetworkModule.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/di/NetworkModule.kt @@ -1,12 +1,13 @@ package com.example.hackathon.core.network.di -import com.example.hackathon.core.network.BuildConfig import com.example.hackathon.core.network.AuthInterceptor +import com.example.hackathon.core.network.BuildConfig import com.example.hackathon.core.network.service.RecipeService import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton import kotlinx.serialization.json.Json import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient @@ -14,22 +15,20 @@ import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Converter import retrofit2.Retrofit import retrofit2.converter.kotlinx.serialization.asConverterFactory -import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) object NetworkModule { @Provides @Singleton - fun providesOkHttpClient( - authInterceptor: AuthInterceptor, - ): OkHttpClient { + fun providesOkHttpClient(authInterceptor: AuthInterceptor): OkHttpClient { val loggingInterceptor = HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY } - return OkHttpClient.Builder() + return OkHttpClient + .Builder() .addInterceptor(authInterceptor) .addInterceptor(loggingInterceptor) .build() @@ -50,11 +49,9 @@ object NetworkModule { @MainRetrofit @Provides @Singleton - fun providesRetrofit( - client: OkHttpClient, - converterFactory: Converter.Factory, - ): Retrofit = - Retrofit.Builder() + fun providesRetrofit(client: OkHttpClient, converterFactory: Converter.Factory): Retrofit = + Retrofit + .Builder() .baseUrl(BuildConfig.BASE_URL) .client(client) .addConverterFactory(converterFactory) @@ -64,20 +61,20 @@ object NetworkModule { @AuthRetrofit @Provides @Singleton - fun providesAuthRetrofit( - converterFactory: Converter.Factory, - ): Retrofit { + fun providesAuthRetrofit(converterFactory: Converter.Factory): Retrofit { val loggingInterceptor = HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY } val authClient = - OkHttpClient.Builder() + OkHttpClient + .Builder() .addInterceptor(loggingInterceptor) .build() - return Retrofit.Builder() + return Retrofit + .Builder() .baseUrl(BuildConfig.BASE_URL) .client(authClient) .addConverterFactory(converterFactory) @@ -85,5 +82,6 @@ object NetworkModule { } @Provides - fun provideRecipeService(@MainRetrofit retrofit: Retrofit): RecipeService = retrofit.create(RecipeService::class.java) + fun provideRecipeService(@MainRetrofit retrofit: Retrofit): RecipeService = + retrofit.create(RecipeService::class.java) } diff --git a/core/network/src/main/java/com/example/hackathon/core/network/dto/request/CreateCombinationRequest.kt b/core/network/src/main/java/com/example/hackathon/core/network/dto/request/CreateCombinationRequest.kt index 24c6a57..c328147 100644 --- a/core/network/src/main/java/com/example/hackathon/core/network/dto/request/CreateCombinationRequest.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/dto/request/CreateCombinationRequest.kt @@ -7,5 +7,5 @@ data class CreateCombinationRequest( val title: String, val description: String, val category: String, - val ingredients: List, + val ingredients: List ) diff --git a/core/network/src/main/java/com/example/hackathon/core/network/dto/request/LoginRequest.kt b/core/network/src/main/java/com/example/hackathon/core/network/dto/request/LoginRequest.kt index f473abe..0ff7c8b 100644 --- a/core/network/src/main/java/com/example/hackathon/core/network/dto/request/LoginRequest.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/dto/request/LoginRequest.kt @@ -5,11 +5,8 @@ import kotlinx.serialization.Serializable /** * 로그인 요청 DTO * POST /auth/login - * + * * Swagger 스펙에 따라 nickname과 password를 사용 */ @Serializable -data class LoginRequest( - val nickname: String, - val password: String, -) +data class LoginRequest(val nickname: String, val password: String) diff --git a/core/network/src/main/java/com/example/hackathon/core/network/dto/request/LogoutRequest.kt b/core/network/src/main/java/com/example/hackathon/core/network/dto/request/LogoutRequest.kt index b28e5ea..1e3f18b 100644 --- a/core/network/src/main/java/com/example/hackathon/core/network/dto/request/LogoutRequest.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/dto/request/LogoutRequest.kt @@ -3,6 +3,4 @@ package com.example.hackathon.core.network.dto.request import kotlinx.serialization.Serializable @Serializable -data class LogoutRequest( - val refreshToken: String, -) +data class LogoutRequest(val refreshToken: String) diff --git a/core/network/src/main/java/com/example/hackathon/core/network/dto/request/RegisterRequest.kt b/core/network/src/main/java/com/example/hackathon/core/network/dto/request/RegisterRequest.kt index b65410a..336d084 100644 --- a/core/network/src/main/java/com/example/hackathon/core/network/dto/request/RegisterRequest.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/dto/request/RegisterRequest.kt @@ -18,7 +18,8 @@ data class RegisterRequest( val ingredients: List, val images: List, val description: String, - val isPrivate: Boolean = false, // false = 전체 공개, true = 나만 보기 + // false = 전체 공개, true = 나만 보기 + val isPrivate: Boolean = false ) /** @@ -32,5 +33,6 @@ data class UpdateRegisterRequest( val ingredients: List, val images: List, val description: String, - val isPrivate: Boolean = false, // false = 전체 공개, true = 나만 보기 + // false = 전체 공개, true = 나만 보기 + val isPrivate: Boolean = false ) diff --git a/core/network/src/main/java/com/example/hackathon/core/network/dto/request/ReissueRequest.kt b/core/network/src/main/java/com/example/hackathon/core/network/dto/request/ReissueRequest.kt index 905b78b..8bf0508 100644 --- a/core/network/src/main/java/com/example/hackathon/core/network/dto/request/ReissueRequest.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/dto/request/ReissueRequest.kt @@ -11,6 +11,4 @@ import kotlinx.serialization.Serializable * 실제 서버와 다를 경우 필드명 수정 필요 */ @Serializable -data class ReissueRequest( - val refreshToken: String, -) +data class ReissueRequest(val refreshToken: String) diff --git a/core/network/src/main/java/com/example/hackathon/core/network/dto/request/SignupRequest.kt b/core/network/src/main/java/com/example/hackathon/core/network/dto/request/SignupRequest.kt index 8860a98..f340131 100644 --- a/core/network/src/main/java/com/example/hackathon/core/network/dto/request/SignupRequest.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/dto/request/SignupRequest.kt @@ -5,11 +5,8 @@ import kotlinx.serialization.Serializable /** * 회원가입 요청 DTO * POST /auth/signup - * + * * Swagger 스펙에 따라 nickname과 password만 사용 (email 불필요) */ @Serializable -data class SignupRequest( - val nickname: String, - val password: String, -) +data class SignupRequest(val nickname: String, val password: String) diff --git a/core/network/src/main/java/com/example/hackathon/core/network/dto/request/UpdateCombinationRequest.kt b/core/network/src/main/java/com/example/hackathon/core/network/dto/request/UpdateCombinationRequest.kt index ccb65a4..6a820ce 100644 --- a/core/network/src/main/java/com/example/hackathon/core/network/dto/request/UpdateCombinationRequest.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/dto/request/UpdateCombinationRequest.kt @@ -7,5 +7,5 @@ data class UpdateCombinationRequest( val title: String, val description: String, val category: String, - val ingredients: List, + val ingredients: List ) diff --git a/core/network/src/main/java/com/example/hackathon/core/network/dto/request/UpdateProfileRequest.kt b/core/network/src/main/java/com/example/hackathon/core/network/dto/request/UpdateProfileRequest.kt index bd6690c..b049f7b 100644 --- a/core/network/src/main/java/com/example/hackathon/core/network/dto/request/UpdateProfileRequest.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/dto/request/UpdateProfileRequest.kt @@ -3,7 +3,4 @@ package com.example.hackathon.core.network.dto.request import kotlinx.serialization.Serializable @Serializable -data class UpdateProfileRequest( - val nickname: String, - val profileImageUrl: String? = null, -) +data class UpdateProfileRequest(val nickname: String, val profileImageUrl: String? = null) diff --git a/core/network/src/main/java/com/example/hackathon/core/network/dto/response/BaseResponse.kt b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/BaseResponse.kt index 6117ea6..6080c8c 100644 --- a/core/network/src/main/java/com/example/hackathon/core/network/dto/response/BaseResponse.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/BaseResponse.kt @@ -3,8 +3,4 @@ package com.example.hackathon.core.network.dto.response import kotlinx.serialization.Serializable @Serializable -data class BaseResponse( - val code: Int, - val message: String, - val data: T?, -) +data class BaseResponse(val code: Int, val message: String, val data: T?) diff --git a/core/network/src/main/java/com/example/hackathon/core/network/dto/response/CombinationResponse.kt b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/CombinationResponse.kt index 459d039..311a8da 100644 --- a/core/network/src/main/java/com/example/hackathon/core/network/dto/response/CombinationResponse.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/CombinationResponse.kt @@ -12,5 +12,5 @@ data class CombinationResponse( val ingredients: List, val author: UserResponse, val likeCount: Int, - val createdAt: String, + val createdAt: String ) diff --git a/core/network/src/main/java/com/example/hackathon/core/network/dto/response/DetailResponse.kt b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/DetailResponse.kt index 47c3d13..24b1244 100644 --- a/core/network/src/main/java/com/example/hackathon/core/network/dto/response/DetailResponse.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/DetailResponse.kt @@ -21,5 +21,5 @@ data class DetailResponse( val isLiked: Boolean = false, val isMine: Boolean = false, val createdAt: String, - val updatedAt: String? = null, + val updatedAt: String? = null ) diff --git a/core/network/src/main/java/com/example/hackathon/core/network/dto/response/LoginResponse.kt b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/LoginResponse.kt index 20d3388..d8ea546 100644 --- a/core/network/src/main/java/com/example/hackathon/core/network/dto/response/LoginResponse.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/LoginResponse.kt @@ -8,5 +8,5 @@ data class LoginResponse( val refreshToken: String, // 서버 응답에 user 필드가 없을 수 있으므로 optional로 처리 // 사용자 정보는 /users/mypage 엔드포인트로 별도 조회 - val user: UserResponse? = null, + val user: UserResponse? = null ) diff --git a/core/network/src/main/java/com/example/hackathon/core/network/dto/response/MyPageRecipeResponse.kt b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/MyPageRecipeResponse.kt index 9387b8f..9505814 100644 --- a/core/network/src/main/java/com/example/hackathon/core/network/dto/response/MyPageRecipeResponse.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/MyPageRecipeResponse.kt @@ -14,5 +14,5 @@ data class MyPageRecipeResponse( val thumbnailUrl: String? = null, val likesCount: Int = 0, val viewCount: Int = 0, - val createdAt: String, + val createdAt: String ) diff --git a/core/network/src/main/java/com/example/hackathon/core/network/dto/response/PostPreviewDto.kt b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/PostPreviewDto.kt index 343505d..db88a6a 100644 --- a/core/network/src/main/java/com/example/hackathon/core/network/dto/response/PostPreviewDto.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/PostPreviewDto.kt @@ -15,12 +15,8 @@ data class PostPreviewDto( val author: AuthorDto, val likesCount: Int = 0, val viewCount: Int = 0, - val createdAt: String, + val createdAt: String ) @Serializable -data class AuthorDto( - val userId: String, - val nickname: String, - val profileImageUrl: String? = null, -) +data class AuthorDto(val userId: String, val nickname: String, val profileImageUrl: String? = null) diff --git a/core/network/src/main/java/com/example/hackathon/core/network/dto/response/RecipeDetailResponse.kt b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/RecipeDetailResponse.kt index c5587a5..07f0d90 100644 --- a/core/network/src/main/java/com/example/hackathon/core/network/dto/response/RecipeDetailResponse.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/RecipeDetailResponse.kt @@ -14,25 +14,14 @@ data class RecipeDetailResponse( val userInteraction: UserInteractionDto, val tags: List, val createdAt: String, - val updatedAt: String, + val updatedAt: String ) - @Serializable -data class IngredientDto( - val name: String, - val amount: String, -) - +data class IngredientDto(val name: String, val amount: String) @Serializable -data class StatsDto( - val likesCount: Int, -) - +data class StatsDto(val likesCount: Int) @Serializable -data class UserInteractionDto( - val isLiked: Boolean, - val isMine: Boolean, -) +data class UserInteractionDto(val isLiked: Boolean, val isMine: Boolean) diff --git a/core/network/src/main/java/com/example/hackathon/core/network/dto/response/RecipeRankingResponse.kt b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/RecipeRankingResponse.kt index bed7c18..5fbf45d 100644 --- a/core/network/src/main/java/com/example/hackathon/core/network/dto/response/RecipeRankingResponse.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/RecipeRankingResponse.kt @@ -5,7 +5,7 @@ import kotlinx.serialization.Serializable @Serializable data class RecipeRankingResponse( val currentCategory: String, - val list: List, + val list: List ) @Serializable @@ -15,5 +15,5 @@ data class RecipeRankingItemResponse( val viewCount: Int, val likes: Int, val username: String, - val thumbnailUrl: String? = null, + val thumbnailUrl: String? = null ) diff --git a/core/network/src/main/java/com/example/hackathon/core/network/dto/response/ReissueResponse.kt b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/ReissueResponse.kt index d9d44af..90857e3 100644 --- a/core/network/src/main/java/com/example/hackathon/core/network/dto/response/ReissueResponse.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/ReissueResponse.kt @@ -14,5 +14,6 @@ import kotlinx.serialization.Serializable @Serializable data class ReissueResponse( val accessToken: String, - val refreshToken: String? = null, // 선택사항 (RTR - Refresh Token Rotation) + // 선택사항 (RTR - Refresh Token Rotation) + val refreshToken: String? = null ) diff --git a/core/network/src/main/java/com/example/hackathon/core/network/dto/response/SignupResponse.kt b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/SignupResponse.kt index e6c8e19..94aac2b 100644 --- a/core/network/src/main/java/com/example/hackathon/core/network/dto/response/SignupResponse.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/SignupResponse.kt @@ -5,11 +5,12 @@ import kotlinx.serialization.Serializable /** * 회원가입 응답 DTO * POST /auth/signup 응답 - * + * * Swagger 스펙에 따라 id와 nickname을 반환 (email 불필요) */ @Serializable data class SignupResponse( val id: Long, - val nickname: String? = null, // 서버 응답에 따라 선택사항 + // 서버 응답에 따라 선택사항 + val nickname: String? = null ) diff --git a/core/network/src/main/java/com/example/hackathon/core/network/dto/response/UserProfileResponse.kt b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/UserProfileResponse.kt index 2fc1e69..5101e61 100644 --- a/core/network/src/main/java/com/example/hackathon/core/network/dto/response/UserProfileResponse.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/UserProfileResponse.kt @@ -13,5 +13,5 @@ data class UserProfileResponse( // 서버 응답에 없을 수 있는 필드들 (optional) val id: String? = null, val email: String? = null, - val profileImageUrl: String? = null, + val profileImageUrl: String? = null ) diff --git a/core/network/src/main/java/com/example/hackathon/core/network/dto/response/UserResponse.kt b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/UserResponse.kt index e3a2537..aac548e 100644 --- a/core/network/src/main/java/com/example/hackathon/core/network/dto/response/UserResponse.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/dto/response/UserResponse.kt @@ -3,8 +3,4 @@ package com.example.hackathon.core.network.dto.response import kotlinx.serialization.Serializable @Serializable -data class UserResponse( - val id: String, - val nickname: String, - val profileImageUrl: String? = null, -) +data class UserResponse(val id: String, val nickname: String, val profileImageUrl: String? = null) diff --git a/core/network/src/main/java/com/example/hackathon/core/network/service/AuthService.kt b/core/network/src/main/java/com/example/hackathon/core/network/service/AuthService.kt index 608e6e0..35ab042 100644 --- a/core/network/src/main/java/com/example/hackathon/core/network/service/AuthService.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/service/AuthService.kt @@ -17,16 +17,12 @@ interface AuthService { // 서버 응답: text/plain "가입이 완료되었습니다" (200 OK) // 성공 여부는 HTTP 상태 코드로 판단 @POST("auth/signup") - suspend fun signup( - @Body request: SignupRequest, - ): retrofit2.Response + suspend fun signup(@Body request: SignupRequest): retrofit2.Response // 2. 로그인 // 서버 응답: {"accessToken":"...","refreshToken":"..."} (BaseResponse 래퍼 없음) @POST("auth/login") - suspend fun login( - @Body request: LoginRequest, - ): LoginResponse + suspend fun login(@Body request: LoginRequest): LoginResponse /** * 토큰 재발급 @@ -37,13 +33,9 @@ interface AuthService { * response: {"accessToken":"...","refreshToken":"..."} (BaseResponse 래퍼 없음) */ @POST("auth/reissue") - suspend fun reissue( - @Body request: ReissueRequest, - ): ReissueResponse + suspend fun reissue(@Body request: ReissueRequest): ReissueResponse // 4. 로그아웃 @POST("auth/logout") - suspend fun logout( - @Body request: LogoutRequest, - ): BaseResponse + suspend fun logout(@Body request: LogoutRequest): BaseResponse } diff --git a/core/network/src/main/java/com/example/hackathon/core/network/service/CombinationService.kt b/core/network/src/main/java/com/example/hackathon/core/network/service/CombinationService.kt index 1e19037..1d94570 100644 --- a/core/network/src/main/java/com/example/hackathon/core/network/service/CombinationService.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/service/CombinationService.kt @@ -14,55 +14,49 @@ interface CombinationService { suspend fun getCombinations( @Query("category") category: String? = null, @Query("page") page: Int = 1, - @Query("pageSize") pageSize: Int = 10, + @Query("pageSize") pageSize: Int = 10 ): BaseResponse> @GET("combinations/{id}") - suspend fun getCombinationById( - @Path("id") id: String, - ): BaseResponse + suspend fun getCombinationById(@Path("id") id: String): BaseResponse @POST("combinations") suspend fun createCombination( - @Body request: CreateCombinationRequest, + @Body request: CreateCombinationRequest ): BaseResponse @PUT("combinations/{id}") suspend fun updateCombination( @Path("id") id: String, - @Body request: UpdateCombinationRequest, + @Body request: UpdateCombinationRequest ): BaseResponse @DELETE("combinations/{id}") - suspend fun deleteCombination( - @Path("id") id: String, - ): BaseResponse + suspend fun deleteCombination(@Path("id") id: String): BaseResponse @GET("users/me/combinations") suspend fun getMyCombinations(): BaseResponse> @POST("combinations/{id}/like") - suspend fun likeCombination( - @Path("id") id: String, - ): BaseResponse + suspend fun likeCombination(@Path("id") id: String): BaseResponse /** * 레시피 등록 (Swagger 스펙) * POST /register?userId={userId} - * + * * 참고: 서버가 200 OK를 반환하지만 빈 응답(Content-Length: 0)을 반환할 수 있음 * Response을 사용하여 빈 응답을 안전하게 처리 */ @POST("register") suspend fun register( @Query("userId") userId: Long, - @Body request: RegisterRequest, + @Body request: RegisterRequest ): retrofit2.Response /** * 레시피 수정 (Swagger 스펙) * PUT /register/{postId}?userId={userId} - * + * * 참고: 서버가 빈 응답을 반환할 수 있음 * Response을 사용하여 빈 응답을 안전하게 처리 */ @@ -70,19 +64,19 @@ interface CombinationService { suspend fun updateRegister( @Path("postId") postId: Long, @Query("userId") userId: Long, - @Body request: UpdateRegisterRequest, + @Body request: UpdateRegisterRequest ): retrofit2.Response /** * 레시피 삭제 (Swagger 스펙) * DELETE /register/{postId}?userId={userId} - * + * * 참고: 서버가 빈 응답을 반환할 수 있음 * Response을 사용하여 빈 응답을 안전하게 처리 */ @DELETE("register/{postId}") suspend fun deleteRegister( @Path("postId") postId: Long, - @Query("userId") userId: Long, + @Query("userId") userId: Long ): retrofit2.Response } diff --git a/core/network/src/main/java/com/example/hackathon/core/network/service/RecipeService.kt b/core/network/src/main/java/com/example/hackathon/core/network/service/RecipeService.kt index 6ef58ed..779f2f5 100644 --- a/core/network/src/main/java/com/example/hackathon/core/network/service/RecipeService.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/service/RecipeService.kt @@ -24,7 +24,7 @@ interface RecipeService { @GET("recipes") suspend fun getRecipes( @Query("page") page: Int? = null, - @Query("category") category: String? = null, + @Query("category") category: String? = null ): BaseResponse> /** @@ -32,9 +32,7 @@ interface RecipeService { * GET /recipes/{postId} */ @GET("recipes/{postId}") - suspend fun getRecipeDetail( - @Path("postId") postId: Long, - ): BaseResponse + suspend fun getRecipeDetail(@Path("postId") postId: Long): BaseResponse /** * 레시피 검색 @@ -42,9 +40,7 @@ interface RecipeService { * @param keyword 검색 키워드 */ @GET("recipes/search") - suspend fun searchRecipes( - @Query("keyword") keyword: String, - ): BaseResponse> + suspend fun searchRecipes(@Query("keyword") keyword: String): BaseResponse> /** * 좋아요 추가 @@ -52,9 +48,7 @@ interface RecipeService { * 서버 응답: 200 OK with empty body (0 bytes) */ @POST("recipes/{postId}/likes") - suspend fun likeRecipe( - @Path("postId") postId: Long, - ): Response + suspend fun likeRecipe(@Path("postId") postId: Long): Response /** * 좋아요 취소 @@ -62,7 +56,5 @@ interface RecipeService { * 서버 응답: 200 OK with empty body (0 bytes) */ @DELETE("recipes/{postId}/likes") - suspend fun unlikeRecipe( - @Path("postId") postId: Long, - ): Response + suspend fun unlikeRecipe(@Path("postId") postId: Long): Response } diff --git a/core/network/src/main/java/com/example/hackathon/core/network/service/UserService.kt b/core/network/src/main/java/com/example/hackathon/core/network/service/UserService.kt index 8458e2b..698c249 100644 --- a/core/network/src/main/java/com/example/hackathon/core/network/service/UserService.kt +++ b/core/network/src/main/java/com/example/hackathon/core/network/service/UserService.kt @@ -16,9 +16,7 @@ interface UserService { // 내 프로필 수정 @PATCH("users/mypage") - suspend fun updateMyPage( - @Body request: UpdateProfileRequest, - ): BaseResponse + suspend fun updateMyPage(@Body request: UpdateProfileRequest): BaseResponse // 내가 작성한 레시피 목록 @GET("users/mypage/recipes") diff --git a/feature/auth/src/main/java/com/example/hackathon/feature/auth/LoginScreen.kt b/feature/auth/src/main/java/com/example/hackathon/feature/auth/LoginScreen.kt index 0833e93..88b65c7 100644 --- a/feature/auth/src/main/java/com/example/hackathon/feature/auth/LoginScreen.kt +++ b/feature/auth/src/main/java/com/example/hackathon/feature/auth/LoginScreen.kt @@ -57,11 +57,11 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.example.hackathon.core.designsystem.R import com.example.hackathon.core.designsystem.component.TopAppLogoBar -import com.example.hackathon.feature.auth.LoginViewModel import com.example.hackathon.core.designsystem.theme.FontMedium import com.example.hackathon.core.designsystem.theme.Gray700 import com.example.hackathon.core.designsystem.theme.HackathonTheme import com.example.hackathon.core.designsystem.theme.Primary +import com.example.hackathon.feature.auth.LoginViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -72,7 +72,7 @@ fun LoginScreen( viewModel: LoginViewModel = hiltViewModel(), onNavigateBack: () -> Unit = {}, onLoginSuccess: () -> Unit = {}, - onNavigateToRegistration: () -> Unit = {}, + onNavigateToRegistration: () -> Unit = {} ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val scrollState = rememberScrollState() @@ -89,7 +89,7 @@ fun LoginScreen( Scaffold( modifier = modifier, - contentWindowInsets = WindowInsets.ime, + contentWindowInsets = WindowInsets.ime ) { paddingValues -> Column( modifier = @@ -97,18 +97,19 @@ fun LoginScreen( .fillMaxSize() .padding(paddingValues) .verticalScroll(scrollState), - horizontalAlignment = Alignment.CenterHorizontally, + horizontalAlignment = Alignment.CenterHorizontally ) { // 로고 영역 (최상단) Spacer(modifier.size(44.dp)) Image( painter = painterResource(R.drawable.ic_logo), contentDescription = "logo", - modifier = Modifier - .size( - width = 237.71429.dp, - height = 65.dp, - ) + modifier = + Modifier + .size( + width = 237.71429.dp, + height = 65.dp + ) ) Spacer(modifier = Modifier.height(4.dp)) @@ -129,7 +130,7 @@ fun LoginScreen( style = HackathonTheme.typography.Body_semibold, fontWeight = FontWeight(500), color = Color(0xFF000000), - textAlign = TextAlign.End, + textAlign = TextAlign.End ) } @@ -138,19 +139,19 @@ fun LoginScreen( Column( modifier = Modifier.padding(horizontal = 50.dp), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) ) { // 입력 필드 영역 Column( modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(2.dp), + verticalArrangement = Arrangement.spacedBy(2.dp) ) { // 닉네임 입력 필드 Text( text = "닉네임", style = HackathonTheme.typography.Body_semibold, color = Color.Black, - modifier = Modifier.padding(bottom = 8.dp), + modifier = Modifier.padding(bottom = 8.dp) ) OutlinedTextField( value = uiState.id, @@ -159,7 +160,7 @@ fun LoginScreen( Text( text = "닉네임을 입력하세요", style = HackathonTheme.typography.Body_medium, - color = Gray700, + color = Gray700 ) }, modifier = @@ -180,10 +181,10 @@ fun LoginScreen( focusedBorderColor = Primary, unfocusedBorderColor = Gray700, focusedTextColor = Color.Black, - unfocusedTextColor = Color.Black, + unfocusedTextColor = Color.Black ), textStyle = HackathonTheme.typography.Body_medium, - singleLine = true, + singleLine = true ) Spacer(modifier = Modifier.height(16.dp)) @@ -193,7 +194,7 @@ fun LoginScreen( text = "Password", style = HackathonTheme.typography.Body_semibold, color = Color.Black, - modifier = Modifier.padding(bottom = 8.dp), + modifier = Modifier.padding(bottom = 8.dp) ) OutlinedTextField( value = uiState.password, @@ -202,7 +203,7 @@ fun LoginScreen( Text( text = "비밀번호를 입력하세요", style = HackathonTheme.typography.Body_medium, - color = Gray700, + color = Gray700 ) }, modifier = @@ -240,7 +241,7 @@ fun LoginScreen( } else { "비밀번호 보기" }, - tint = Gray700, + tint = Gray700 ) } }, @@ -249,10 +250,10 @@ fun LoginScreen( focusedBorderColor = Primary, unfocusedBorderColor = Gray700, focusedTextColor = Color.Black, - unfocusedTextColor = Color.Black, + unfocusedTextColor = Color.Black ), textStyle = HackathonTheme.typography.Body_medium, - singleLine = true, + singleLine = true ) } @@ -264,7 +265,7 @@ fun LoginScreen( text = error, style = HackathonTheme.typography.Caption_medium, color = Primary, - modifier = Modifier.padding(horizontal = 4.dp), + modifier = Modifier.padding(horizontal = 4.dp) ) } @@ -273,7 +274,7 @@ fun LoginScreen( // 버튼 영역 (가로 배치) Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) ) { // 계정생성 버튼 (현재 화면에서 계정 생성 후 자동 로그인) Button( @@ -282,24 +283,26 @@ fun LoginScreen( }, modifier = Modifier.weight(0.4f).height(44.dp), shape = RoundedCornerShape(15.dp), - enabled = uiState.id.isNotBlank() && uiState.password.isNotBlank() && !uiState.isLoading, + enabled = + uiState.id.isNotBlank() && uiState.password.isNotBlank() && + !uiState.isLoading, colors = ButtonDefaults.buttonColors( containerColor = Gray700, contentColor = Color.White, disabledContainerColor = Gray700.copy(alpha = 0.5f), - disabledContentColor = Color.White.copy(alpha = 0.5f), - ), + disabledContentColor = Color.White.copy(alpha = 0.5f) + ) ) { if (uiState.isLoading) { androidx.compose.material3.CircularProgressIndicator( modifier = Modifier.size(20.dp), - color = Color.White, + color = Color.White ) } else { Text( text = "계정생성", - style = HackathonTheme.typography.Sub1_semibold, + style = HackathonTheme.typography.Sub1_semibold ) } } @@ -311,24 +314,26 @@ fun LoginScreen( }, modifier = Modifier.weight(0.6f).height(44.dp), shape = RoundedCornerShape(15.dp), - enabled = uiState.id.isNotBlank() && uiState.password.isNotBlank() && !uiState.isLoading, + enabled = + uiState.id.isNotBlank() && uiState.password.isNotBlank() && + !uiState.isLoading, colors = ButtonDefaults.buttonColors( containerColor = Primary, contentColor = Color.White, disabledContainerColor = Primary.copy(alpha = 0.5f), - disabledContentColor = Color.White.copy(alpha = 0.5f), - ), + disabledContentColor = Color.White.copy(alpha = 0.5f) + ) ) { if (uiState.isLoading) { androidx.compose.material3.CircularProgressIndicator( modifier = Modifier.size(20.dp), - color = Color.White, + color = Color.White ) } else { Text( text = "로그인", - style = HackathonTheme.typography.Sub1_semibold, + style = HackathonTheme.typography.Sub1_semibold ) } } @@ -347,7 +352,7 @@ fun LoginScreen( Text( text = "닉네임 / 비밀번호를 다시 입력해 주십시오", style = HackathonTheme.typography.Body_medium, - color = Color.Black, + color = Color.Black ) }, confirmButton = { @@ -358,17 +363,17 @@ fun LoginScreen( colors = ButtonDefaults.buttonColors( containerColor = Primary, - contentColor = Color.White, - ), + contentColor = Color.White + ) ) { Text( text = "확인", - style = HackathonTheme.typography.Sub1_semibold, + style = HackathonTheme.typography.Sub1_semibold ) } }, containerColor = Color.White, - shape = RoundedCornerShape(15.dp), + shape = RoundedCornerShape(15.dp) ) } } diff --git a/feature/auth/src/main/java/com/example/hackathon/feature/auth/LoginViewModel.kt b/feature/auth/src/main/java/com/example/hackathon/feature/auth/LoginViewModel.kt index e3d9fb2..af42528 100644 --- a/feature/auth/src/main/java/com/example/hackathon/feature/auth/LoginViewModel.kt +++ b/feature/auth/src/main/java/com/example/hackathon/feature/auth/LoginViewModel.kt @@ -2,95 +2,98 @@ package com.example.hackathon.feature.auth import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.example.hackathon.core.domain.repository.AuthRepository import com.example.hackathon.core.model.DummyData import com.example.hackathon.core.model.User -import com.example.hackathon.core.domain.repository.AuthRepository import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import javax.inject.Inject @HiltViewModel class LoginViewModel - @Inject - constructor( - private val authRepository: AuthRepository, - ) : ViewModel() { - private val _uiState = MutableStateFlow(LoginUiState()) - val uiState: StateFlow = _uiState.asStateFlow() - - fun updateId(id: String) { - _uiState.value = _uiState.value.copy(id = id) - } +@Inject +constructor(private val authRepository: AuthRepository) : + ViewModel() { + private val _uiState = MutableStateFlow(LoginUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + fun updateId(id: String) { + _uiState.value = _uiState.value.copy(id = id) + } - fun updatePassword(password: String) { - _uiState.value = _uiState.value.copy(password = password) - } + fun updatePassword(password: String) { + _uiState.value = _uiState.value.copy(password = password) + } - fun togglePasswordVisibility() { - _uiState.value = _uiState.value.copy(isPasswordVisible = !_uiState.value.isPasswordVisible) - } + fun togglePasswordVisibility() { + _uiState.value = + _uiState.value.copy(isPasswordVisible = !_uiState.value.isPasswordVisible) + } - fun toggleAutoLogin() { - _uiState.value = _uiState.value.copy(isAutoLogin = !_uiState.value.isAutoLogin) - } + fun toggleAutoLogin() { + _uiState.value = _uiState.value.copy(isAutoLogin = !_uiState.value.isAutoLogin) + } - fun login() { - viewModelScope.launch { - _uiState.value = _uiState.value.copy(isLoading = true, error = null, isSuccess = false) + fun login() { + viewModelScope.launch { + _uiState.value = + _uiState.value.copy(isLoading = true, error = null, isSuccess = false) + + // 입력 검증 + if (_uiState.value.id.isBlank() || _uiState.value.password.isBlank()) { + _uiState.value = + _uiState.value.copy( + isLoading = false, + error = "닉네임과 비밀번호를 입력해주세요" + ) + return@launch + } - // 입력 검증 - if (_uiState.value.id.isBlank() || _uiState.value.password.isBlank()) { + // API 호출 (닉네임으로 로그인) + authRepository.login(_uiState.value.id, _uiState.value.password).fold( + onSuccess = { loginResult -> + // 사용자 정보 저장 + DummyData.currentUser = loginResult.user + _uiState.value = _uiState.value.copy(isLoading = false, isSuccess = true) + }, + onFailure = { throwable -> _uiState.value = _uiState.value.copy( isLoading = false, - error = "닉네임과 비밀번호를 입력해주세요", + error = throwable.message ?: "로그인에 실패했습니다" ) - return@launch } - - // API 호출 (닉네임으로 로그인) - authRepository.login(_uiState.value.id, _uiState.value.password).fold( - onSuccess = { loginResult -> - // 사용자 정보 저장 - DummyData.currentUser = loginResult.user - _uiState.value = _uiState.value.copy(isLoading = false, isSuccess = true) - }, - onFailure = { throwable -> - _uiState.value = - _uiState.value.copy( - isLoading = false, - error = throwable.message ?: "로그인에 실패했습니다", - ) - }, - ) - } + ) } + } - /** - * 계정 생성 후 자동 로그인 - * 닉네임과 비밀번호만으로 계정을 생성하고 바로 로그인합니다. - */ - fun signupAndLogin() { - viewModelScope.launch { - _uiState.value = _uiState.value.copy(isLoading = true, error = null, isSuccess = false) - - // 입력 검증 - if (_uiState.value.id.isBlank() || _uiState.value.password.isBlank()) { - _uiState.value = - _uiState.value.copy( - isLoading = false, - error = "닉네임과 비밀번호를 입력해주세요", - ) - return@launch - } + /** + * 계정 생성 후 자동 로그인 + * 닉네임과 비밀번호만으로 계정을 생성하고 바로 로그인합니다. + */ + fun signupAndLogin() { + viewModelScope.launch { + _uiState.value = + _uiState.value.copy(isLoading = true, error = null, isSuccess = false) + + // 입력 검증 + if (_uiState.value.id.isBlank() || _uiState.value.password.isBlank()) { + _uiState.value = + _uiState.value.copy( + isLoading = false, + error = "닉네임과 비밀번호를 입력해주세요" + ) + return@launch + } - // 1. 계정 생성 (Swagger 스펙: nickname, password만 필요) - authRepository.signup( + // 1. 계정 생성 (Swagger 스펙: nickname, password만 필요) + authRepository + .signup( password = _uiState.value.password, - nickname = _uiState.value.id, + nickname = _uiState.value.id ).fold( onSuccess = { // 2. 계정 생성 성공 시 바로 로그인 (nickname으로 로그인) @@ -98,40 +101,41 @@ class LoginViewModel onSuccess = { loginResult -> // 사용자 정보 저장 DummyData.currentUser = loginResult.user - _uiState.value = _uiState.value.copy(isLoading = false, isSuccess = true) + _uiState.value = + _uiState.value.copy(isLoading = false, isSuccess = true) }, onFailure = { throwable -> _uiState.value = _uiState.value.copy( isLoading = false, - error = throwable.message ?: "로그인에 실패했습니다", + error = throwable.message ?: "로그인에 실패했습니다" ) - }, + } ) }, onFailure = { throwable -> _uiState.value = _uiState.value.copy( isLoading = false, - error = throwable.message ?: "계정 생성에 실패했습니다", + error = throwable.message ?: "계정 생성에 실패했습니다" ) - }, + } ) - } } + } - fun socialLogin(provider: SocialLoginProvider) { - viewModelScope.launch { - _uiState.value = _uiState.value.copy(isLoading = true, error = null) - // TODO: 소셜 로그인 API 연동 - _uiState.value = _uiState.value.copy(isLoading = false) - } + fun socialLogin(provider: SocialLoginProvider) { + viewModelScope.launch { + _uiState.value = _uiState.value.copy(isLoading = true, error = null) + // TODO: 소셜 로그인 API 연동 + _uiState.value = _uiState.value.copy(isLoading = false) } + } - fun clearError() { - _uiState.value = _uiState.value.copy(error = null) - } + fun clearError() { + _uiState.value = _uiState.value.copy(error = null) } +} data class LoginUiState( val id: String = "", @@ -140,11 +144,11 @@ data class LoginUiState( val isAutoLogin: Boolean = false, val isLoading: Boolean = false, val isSuccess: Boolean = false, - val error: String? = null, + val error: String? = null ) enum class SocialLoginProvider { KAKAO, NAVER, - GOOGLE, + GOOGLE } diff --git a/feature/auth/src/main/java/com/example/hackathon/feature/auth/RegistrationScreen.kt b/feature/auth/src/main/java/com/example/hackathon/feature/auth/RegistrationScreen.kt index 8d8a459..a16c95a 100644 --- a/feature/auth/src/main/java/com/example/hackathon/feature/auth/RegistrationScreen.kt +++ b/feature/auth/src/main/java/com/example/hackathon/feature/auth/RegistrationScreen.kt @@ -47,10 +47,10 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.example.hackathon.core.designsystem.component.TopAppLogoBar -import com.example.hackathon.feature.auth.RegistrationViewModel import com.example.hackathon.core.designsystem.theme.Gray700 import com.example.hackathon.core.designsystem.theme.HackathonTheme import com.example.hackathon.core.designsystem.theme.Primary +import com.example.hackathon.feature.auth.RegistrationViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -60,7 +60,7 @@ fun RegistrationScreen( modifier: Modifier = Modifier, viewModel: RegistrationViewModel = hiltViewModel(), onNavigateBack: () -> Unit = {}, - onRegistrationSuccess: () -> Unit = {}, + onRegistrationSuccess: () -> Unit = {} ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val scrollState = rememberScrollState() @@ -70,7 +70,7 @@ fun RegistrationScreen( Scaffold( modifier = modifier, - contentWindowInsets = WindowInsets.ime, + contentWindowInsets = WindowInsets.ime ) { paddingValues -> Column( modifier = @@ -78,7 +78,7 @@ fun RegistrationScreen( .fillMaxSize() .padding(paddingValues) .verticalScroll(scrollState), - horizontalAlignment = Alignment.CenterHorizontally, + horizontalAlignment = Alignment.CenterHorizontally ) { // 로고 영역 (최상단) TopAppLogoBar() @@ -89,19 +89,19 @@ fun RegistrationScreen( Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(4.dp), - modifier = Modifier.padding(horizontal = 16.dp), + modifier = Modifier.padding(horizontal = 16.dp) ) { Text( text = "어디선가 들어본 바로 그 조합", style = HackathonTheme.typography.Body_semibold, color = Gray700, - textAlign = TextAlign.Center, + textAlign = TextAlign.Center ) Text( text = "모두 쩝쩝박사에서", style = HackathonTheme.typography.Body_semibold, color = Gray700, - textAlign = TextAlign.Center, + textAlign = TextAlign.Center ) } @@ -110,14 +110,14 @@ fun RegistrationScreen( Column( modifier = Modifier.padding(horizontal = 16.dp), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) ) { // 닉네임 입력 필드 Text( text = "닉네임", style = HackathonTheme.typography.Body_medium, color = Color.Black, - modifier = Modifier.padding(bottom = 8.dp), + modifier = Modifier.padding(bottom = 8.dp) ) OutlinedTextField( value = uiState.name, @@ -126,7 +126,7 @@ fun RegistrationScreen( Text( text = "닉네임을 입력하세요", style = HackathonTheme.typography.Body_medium, - color = Gray700, + color = Gray700 ) }, modifier = @@ -147,10 +147,10 @@ fun RegistrationScreen( focusedBorderColor = Primary, unfocusedBorderColor = Gray700, focusedTextColor = Color.Black, - unfocusedTextColor = Color.Black, + unfocusedTextColor = Color.Black ), textStyle = HackathonTheme.typography.Body_medium, - singleLine = true, + singleLine = true ) Spacer(modifier = Modifier.height(16.dp)) @@ -160,7 +160,7 @@ fun RegistrationScreen( text = "이메일", style = HackathonTheme.typography.Body_medium, color = Color.Black, - modifier = Modifier.padding(bottom = 8.dp), + modifier = Modifier.padding(bottom = 8.dp) ) OutlinedTextField( value = uiState.email, @@ -169,7 +169,7 @@ fun RegistrationScreen( Text( text = "이메일을 입력하세요", style = HackathonTheme.typography.Body_medium, - color = Gray700, + color = Gray700 ) }, modifier = Modifier.fillMaxWidth(), @@ -180,10 +180,10 @@ fun RegistrationScreen( focusedBorderColor = Primary, unfocusedBorderColor = Gray700, focusedTextColor = Color.Black, - unfocusedTextColor = Color.Black, + unfocusedTextColor = Color.Black ), textStyle = HackathonTheme.typography.Body_medium, - singleLine = true, + singleLine = true ) Spacer(modifier = Modifier.height(16.dp)) @@ -193,7 +193,7 @@ fun RegistrationScreen( text = "Password", style = HackathonTheme.typography.Body_medium, color = Color.Black, - modifier = Modifier.padding(bottom = 8.dp), + modifier = Modifier.padding(bottom = 8.dp) ) OutlinedTextField( value = uiState.password, @@ -202,7 +202,7 @@ fun RegistrationScreen( Text( text = "비밀번호를 입력하세요", style = HackathonTheme.typography.Body_medium, - color = Gray700, + color = Gray700 ) }, modifier = @@ -240,7 +240,7 @@ fun RegistrationScreen( } else { "비밀번호 보기" }, - tint = Gray700, + tint = Gray700 ) } }, @@ -249,10 +249,10 @@ fun RegistrationScreen( focusedBorderColor = Primary, unfocusedBorderColor = Gray700, focusedTextColor = Color.Black, - unfocusedTextColor = Color.Black, + unfocusedTextColor = Color.Black ), textStyle = HackathonTheme.typography.Body_medium, - singleLine = true, + singleLine = true ) Spacer(modifier = Modifier.height(16.dp)) @@ -262,7 +262,7 @@ fun RegistrationScreen( text = "Password 확인", style = HackathonTheme.typography.Body_medium, color = Color.Black, - modifier = Modifier.padding(bottom = 8.dp), + modifier = Modifier.padding(bottom = 8.dp) ) OutlinedTextField( value = uiState.passwordConfirm, @@ -271,7 +271,7 @@ fun RegistrationScreen( Text( text = "비밀번호를 다시 입력하세요", style = HackathonTheme.typography.Body_medium, - color = Gray700, + color = Gray700 ) }, modifier = Modifier.fillMaxWidth(), @@ -298,7 +298,7 @@ fun RegistrationScreen( } else { "비밀번호 보기" }, - tint = Gray700, + tint = Gray700 ) } }, @@ -307,10 +307,10 @@ fun RegistrationScreen( focusedBorderColor = Primary, unfocusedBorderColor = Gray700, focusedTextColor = Color.Black, - unfocusedTextColor = Color.Black, + unfocusedTextColor = Color.Black ), textStyle = HackathonTheme.typography.Body_medium, - singleLine = true, + singleLine = true ) Spacer(modifier = Modifier.height(16.dp)) @@ -321,7 +321,7 @@ fun RegistrationScreen( text = error, style = HackathonTheme.typography.Caption_medium, color = Color.Red, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth() ) Spacer(modifier = Modifier.height(8.dp)) } @@ -329,7 +329,7 @@ fun RegistrationScreen( // 버튼 영역 (가로 배치) Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) ) { // 계정생성 버튼 Button( @@ -349,18 +349,18 @@ fun RegistrationScreen( containerColor = Primary, contentColor = Color.White, disabledContainerColor = Primary.copy(alpha = 0.5f), - disabledContentColor = Color.White.copy(alpha = 0.5f), - ), + disabledContentColor = Color.White.copy(alpha = 0.5f) + ) ) { if (uiState.isLoading) { androidx.compose.material3.CircularProgressIndicator( modifier = Modifier.size(20.dp), - color = Color.White, + color = Color.White ) } else { Text( text = "계정생성", - style = HackathonTheme.typography.Sub1_semibold, + style = HackathonTheme.typography.Sub1_semibold ) } } @@ -376,12 +376,12 @@ fun RegistrationScreen( containerColor = Gray700, contentColor = Color.White, disabledContainerColor = Gray700.copy(alpha = 0.5f), - disabledContentColor = Color.White.copy(alpha = 0.5f), - ), + disabledContentColor = Color.White.copy(alpha = 0.5f) + ) ) { Text( text = "로그인", - style = HackathonTheme.typography.Sub1_semibold, + style = HackathonTheme.typography.Sub1_semibold ) } } @@ -393,7 +393,7 @@ fun RegistrationScreen( text = "닉네임/비밀번호를 잊어버렸다면? jjupjjup@naver.com으로 문의", style = HackathonTheme.typography.Caption_medium, color = Gray700, - textAlign = TextAlign.Center, + textAlign = TextAlign.Center ) Spacer(modifier = Modifier.height(24.dp)) @@ -411,17 +411,17 @@ fun RegistrationScreen( title = { Column( modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) ) { Text( text = "닉네임 : ${uiState.name}", style = HackathonTheme.typography.Body_medium, - color = Color.Black, + color = Color.Black ) Text( text = "가입이 완료되었습니다", style = HackathonTheme.typography.Body_medium, - color = Color.Black, + color = Color.Black ) } }, @@ -436,17 +436,17 @@ fun RegistrationScreen( colors = ButtonDefaults.buttonColors( containerColor = Primary, - contentColor = Color.White, - ), + contentColor = Color.White + ) ) { Text( text = "확인", - style = HackathonTheme.typography.Sub1_semibold, + style = HackathonTheme.typography.Sub1_semibold ) } }, containerColor = Color.White, - shape = RoundedCornerShape(15.dp), + shape = RoundedCornerShape(15.dp) ) } } diff --git a/feature/auth/src/main/java/com/example/hackathon/feature/auth/RegistrationSuccessScreen.kt b/feature/auth/src/main/java/com/example/hackathon/feature/auth/RegistrationSuccessScreen.kt index 1a42858..62880b5 100644 --- a/feature/auth/src/main/java/com/example/hackathon/feature/auth/RegistrationSuccessScreen.kt +++ b/feature/auth/src/main/java/com/example/hackathon/feature/auth/RegistrationSuccessScreen.kt @@ -28,12 +28,9 @@ import com.example.hackathon.core.designsystem.theme.Primary @OptIn(ExperimentalMaterial3Api::class) @Composable -fun RegistrationSuccessScreen( - modifier: Modifier = Modifier, - onNavigateToLogin: () -> Unit = {}, -) { +fun RegistrationSuccessScreen(modifier: Modifier = Modifier, onNavigateToLogin: () -> Unit = {}) { Scaffold( - modifier = modifier, + modifier = modifier ) { paddingValues -> Column( modifier = @@ -42,7 +39,7 @@ fun RegistrationSuccessScreen( .padding(paddingValues) .background(Color.White), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, + verticalArrangement = Arrangement.Center ) { // 로고 영역 TopAppLogoBar() @@ -53,20 +50,20 @@ fun RegistrationSuccessScreen( Column( modifier = Modifier.padding(horizontal = 16.dp), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) ) { Text( text = "가입 완료", style = HackathonTheme.typography.Head1_bold, color = Color.Black, - textAlign = TextAlign.Center, + textAlign = TextAlign.Center ) Text( text = "회원가입이 완료되었습니다.\n로그인 후 이용해주세요.", style = HackathonTheme.typography.Body_medium, color = Gray700, - textAlign = TextAlign.Center, + textAlign = TextAlign.Center ) Spacer(modifier = Modifier.height(40.dp)) @@ -79,12 +76,12 @@ fun RegistrationSuccessScreen( colors = ButtonDefaults.buttonColors( containerColor = Primary, - contentColor = Color.White, - ), + contentColor = Color.White + ) ) { Text( text = "로그인하기", - style = HackathonTheme.typography.Sub1_semibold, + style = HackathonTheme.typography.Sub1_semibold ) } } diff --git a/feature/auth/src/main/java/com/example/hackathon/feature/auth/RegistrationViewModel.kt b/feature/auth/src/main/java/com/example/hackathon/feature/auth/RegistrationViewModel.kt index 013c018..e5eec69 100644 --- a/feature/auth/src/main/java/com/example/hackathon/feature/auth/RegistrationViewModel.kt +++ b/feature/auth/src/main/java/com/example/hackathon/feature/auth/RegistrationViewModel.kt @@ -2,99 +2,104 @@ package com.example.hackathon.feature.auth import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.example.hackathon.core.domain.repository.AuthRepository import com.example.hackathon.core.model.DummyData import com.example.hackathon.core.model.User -import com.example.hackathon.core.domain.repository.AuthRepository import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import javax.inject.Inject @HiltViewModel class RegistrationViewModel - @Inject - constructor( - private val authRepository: AuthRepository, - ) : ViewModel() { - private val _uiState = MutableStateFlow(RegistrationUiState()) - val uiState: StateFlow = _uiState.asStateFlow() +@Inject +constructor(private val authRepository: AuthRepository) : + ViewModel() { + private val _uiState = MutableStateFlow(RegistrationUiState()) + val uiState: StateFlow = _uiState.asStateFlow() - fun updateName(name: String) { - _uiState.value = _uiState.value.copy(name = name) - } + fun updateName(name: String) { + _uiState.value = _uiState.value.copy(name = name) + } - fun updateId(id: String) { - _uiState.value = _uiState.value.copy(id = id) - } + fun updateId(id: String) { + _uiState.value = _uiState.value.copy(id = id) + } - fun updateEmail(email: String) { - _uiState.value = _uiState.value.copy(email = email) - } + fun updateEmail(email: String) { + _uiState.value = _uiState.value.copy(email = email) + } - fun updatePassword(password: String) { - _uiState.value = _uiState.value.copy(password = password) - } + fun updatePassword(password: String) { + _uiState.value = _uiState.value.copy(password = password) + } - fun updatePasswordConfirm(passwordConfirm: String) { - _uiState.value = _uiState.value.copy(passwordConfirm = passwordConfirm) - } + fun updatePasswordConfirm(passwordConfirm: String) { + _uiState.value = _uiState.value.copy(passwordConfirm = passwordConfirm) + } - fun togglePasswordVisibility() { - _uiState.value = _uiState.value.copy(isPasswordVisible = !_uiState.value.isPasswordVisible) - } + fun togglePasswordVisibility() { + _uiState.value = + _uiState.value.copy(isPasswordVisible = !_uiState.value.isPasswordVisible) + } - fun togglePasswordConfirmVisibility() { - _uiState.value = _uiState.value.copy(isPasswordConfirmVisible = !_uiState.value.isPasswordConfirmVisible) - } + fun togglePasswordConfirmVisibility() { + _uiState.value = + _uiState.value.copy( + isPasswordConfirmVisible = !_uiState.value.isPasswordConfirmVisible + ) + } - fun register() { - viewModelScope.launch { - _uiState.value = _uiState.value.copy(isLoading = true, error = null) + fun register() { + viewModelScope.launch { + _uiState.value = _uiState.value.copy(isLoading = true, error = null) - // 입력 검증 - if (_uiState.value.name.isBlank() || _uiState.value.password.isBlank()) { - _uiState.value = - _uiState.value.copy( - isLoading = false, - error = "닉네임과 비밀번호를 입력해주세요", - ) - return@launch - } + // 입력 검증 + if (_uiState.value.name.isBlank() || _uiState.value.password.isBlank()) { + _uiState.value = + _uiState.value.copy( + isLoading = false, + error = "닉네임과 비밀번호를 입력해주세요" + ) + return@launch + } - if (_uiState.value.password != _uiState.value.passwordConfirm) { - _uiState.value = - _uiState.value.copy( - isLoading = false, - error = "비밀번호가 일치하지 않습니다", - ) - return@launch - } + if (_uiState.value.password != _uiState.value.passwordConfirm) { + _uiState.value = + _uiState.value.copy( + isLoading = false, + error = "비밀번호가 일치하지 않습니다" + ) + return@launch + } - // API 호출 (Swagger 스펙: nickname, password만 필요) - authRepository.signup( + // API 호출 (Swagger 스펙: nickname, password만 필요) + authRepository + .signup( password = _uiState.value.password, - nickname = _uiState.value.name, + nickname = _uiState.value.name ).fold( onSuccess = { - _uiState.value = _uiState.value.copy(isLoading = false, isSuccess = true) + _uiState.value = + _uiState.value.copy(isLoading = false, isSuccess = true) }, onFailure = { throwable -> _uiState.value = _uiState.value.copy( isLoading = false, - error = throwable.message ?: "회원가입에 실패했습니다", + error = throwable.message ?: "회원가입에 실패했습니다" ) - }, + } ) - } } + } - fun clearSuccess() { - _uiState.value = _uiState.value.copy(isSuccess = false) - } + fun clearSuccess() { + _uiState.value = _uiState.value.copy(isSuccess = false) } +} data class RegistrationUiState( val name: String = "", @@ -106,5 +111,5 @@ data class RegistrationUiState( val isPasswordConfirmVisible: Boolean = false, val isLoading: Boolean = false, val isSuccess: Boolean = false, - val error: String? = null, + val error: String? = null ) diff --git a/feature/combination/src/main/java/com/example/hackathon/feature/combination/CreateCombinationScreen.kt b/feature/combination/src/main/java/com/example/hackathon/feature/combination/CreateCombinationScreen.kt index 4b4138b..92d7eea 100644 --- a/feature/combination/src/main/java/com/example/hackathon/feature/combination/CreateCombinationScreen.kt +++ b/feature/combination/src/main/java/com/example/hackathon/feature/combination/CreateCombinationScreen.kt @@ -50,28 +50,28 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.example.hackathon.core.designsystem.R import com.example.hackathon.core.designsystem.component.TopAppLogoBar +import com.example.hackathon.core.designsystem.theme.Gray700 +import com.example.hackathon.core.designsystem.theme.HackathonTheme +import com.example.hackathon.core.designsystem.theme.Primary +import com.example.hackathon.feature.combination.CreateCombinationViewModel import com.example.hackathon.feature.combination.component.HashTagInputSection import com.example.hackathon.feature.combination.component.ImageUploadSection import com.example.hackathon.feature.combination.component.IngredientInputSection import com.example.hackathon.feature.combination.component.VisibilitySelectionSection -import com.example.hackathon.feature.combination.CreateCombinationViewModel -import com.example.hackathon.core.designsystem.theme.Gray700 -import com.example.hackathon.core.designsystem.theme.HackathonTheme -import com.example.hackathon.core.designsystem.theme.Primary @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @Composable fun CreateCombinationScreen( modifier: Modifier = Modifier, viewModel: CreateCombinationViewModel = hiltViewModel(), - onNavigateBack: () -> Unit = {}, + onNavigateBack: () -> Unit = {} ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() // 이미지 선택을 위한 ActivityResultLauncher val imagePickerLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.GetContent(), + contract = ActivityResultContracts.GetContent() ) { uri: Uri? -> uri?.let { viewModel.updateImageUri(it) } } @@ -79,11 +79,10 @@ fun CreateCombinationScreen( // 해시태그 입력 UI 상태 (로컬) val tagInput = rememberSaveable { mutableStateOf("") } - Scaffold( modifier = modifier, - contentWindowInsets = WindowInsets(0), - ) { innerPadding -> + contentWindowInsets = WindowInsets(0) + ) { innerPadding -> Column( modifier = Modifier @@ -101,22 +100,22 @@ fun CreateCombinationScreen( ) { IconButton( onClick = onNavigateBack, - modifier = Modifier.size(48.dp), + modifier = Modifier.size(48.dp) ) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "뒤로가기", + contentDescription = "뒤로가기" ) } Box( modifier = Modifier.weight(1f), - contentAlignment = Alignment.Center, + contentAlignment = Alignment.Center ) { Image( painter = painterResource(id = R.drawable.ic_logo), contentDescription = "logo", - modifier = Modifier.width(90.dp), + modifier = Modifier.width(90.dp) ) } @@ -132,12 +131,13 @@ fun CreateCombinationScreen( .background( brush = Brush.verticalGradient( - colors = listOf( - Color.Black.copy(alpha = 0.15f), - Color.Transparent, - ), - ), - ), + colors = + listOf( + Color.Black.copy(alpha = 0.15f), + Color.Transparent + ) + ) + ) ) } @@ -147,30 +147,30 @@ fun CreateCombinationScreen( .weight(1f) .verticalScroll(rememberScrollState()) .padding(horizontal = 16.dp, vertical = 20.dp), - verticalArrangement = Arrangement.spacedBy(24.dp), - ){ + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { // 화면 제목 Text( text = "꿀조합 레시피 등록", style = HackathonTheme.typography.Head2_semibold, - color = Color.Black, + color = Color.Black ) // 이미지 업로드 영역 ImageUploadSection( imageUris = uiState.imageUris, onImageClick = { imagePickerLauncher.launch("image/*") }, - onRemoveImage = { uri -> viewModel.removeImageUri(uri) }, + onRemoveImage = { uri -> viewModel.removeImageUri(uri) } ) // 제목 입력 Column( - verticalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) ) { Text( text = "제목", style = HackathonTheme.typography.Sub2_semibold, - color = Color.Black, + color = Color.Black ) OutlinedTextField( value = uiState.title, @@ -179,7 +179,7 @@ fun CreateCombinationScreen( Text( text = "조합 이름을 적어주세요 (최대 15자)", style = MaterialTheme.typography.bodyMedium, - color = Gray700, + color = Gray700 ) }, modifier = Modifier.fillMaxWidth(), @@ -188,19 +188,19 @@ fun CreateCombinationScreen( colors = OutlinedTextFieldDefaults.colors( focusedBorderColor = Primary, - unfocusedBorderColor = Primary.copy(alpha = 0.5f), - ), + unfocusedBorderColor = Primary.copy(alpha = 0.5f) + ) ) } // 카테고리 입력 Column( - verticalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) ) { Text( text = "카테고리", style = HackathonTheme.typography.Sub2_semibold, - color = Color.Black, + color = Color.Black ) HashTagInputSection( tagInput = tagInput.value, @@ -212,41 +212,45 @@ fun CreateCombinationScreen( tagInput.value = "" } }, - onRemoveTag = { viewModel.removeTag(it) }, + onRemoveTag = { viewModel.removeTag(it) } ) } // 재료 입력 IngredientInputSection( ingredients = uiState.ingredientsList, - onIngredientNameChange = { index, name -> viewModel.updateIngredientName(index, name) }, - onIngredientQuantityChange = { index, quantity -> viewModel.updateIngredientQuantity(index, quantity) }, - onAddIngredient = { viewModel.addIngredient() }, + onIngredientNameChange = { index, name -> + viewModel.updateIngredientName(index, name) + }, + onIngredientQuantityChange = { index, quantity -> + viewModel.updateIngredientQuantity(index, quantity) + }, + onAddIngredient = { viewModel.addIngredient() } ) // 공개 여부 선택 Column( - verticalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) ) { Text( text = "공개 여부", style = HackathonTheme.typography.Sub2_semibold, - color = Color.Black, + color = Color.Black ) VisibilitySelectionSection( isPublic = uiState.isPublic, - onPublicChange = { viewModel.updateIsPublic(it) }, + onPublicChange = { viewModel.updateIsPublic(it) } ) } // 설명 입력 Column( - verticalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) ) { Text( text = "설명", style = HackathonTheme.typography.Sub2_semibold, - color = Color.Black, + color = Color.Black ) OutlinedTextField( value = uiState.description, @@ -255,7 +259,7 @@ fun CreateCombinationScreen( Text( text = "레시피에 대한 설명을 적어주세요 (최대 300자)", style = MaterialTheme.typography.bodyMedium, - color = Gray700, + color = Gray700 ) }, modifier = Modifier.fillMaxWidth(), @@ -264,8 +268,8 @@ fun CreateCombinationScreen( colors = OutlinedTextFieldDefaults.colors( focusedBorderColor = Primary, - unfocusedBorderColor = Primary.copy(alpha = 0.5f), - ), + unfocusedBorderColor = Primary.copy(alpha = 0.5f) + ) ) } @@ -274,7 +278,7 @@ fun CreateCombinationScreen( Text( text = error, color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodyMedium, + style = MaterialTheme.typography.bodyMedium ) } @@ -288,24 +292,26 @@ fun CreateCombinationScreen( } }, modifier = Modifier.fillMaxWidth(), - enabled = !uiState.isLoading && uiState.title.isNotBlank() && uiState.description.isNotBlank(), + enabled = + !uiState.isLoading && uiState.title.isNotBlank() && + uiState.description.isNotBlank(), shape = RoundedCornerShape(15.dp), colors = ButtonDefaults.buttonColors( containerColor = Primary, - contentColor = Color.White, - ), + contentColor = Color.White + ) ) { if (uiState.isLoading) { CircularProgressIndicator( modifier = Modifier.height(16.dp), color = Color.White, - strokeWidth = 2.dp, + strokeWidth = 2.dp ) } else { Text( text = "꿀조합 등록하기", - style = MaterialTheme.typography.titleMedium, + style = MaterialTheme.typography.titleMedium ) } } diff --git a/feature/combination/src/main/java/com/example/hackathon/feature/combination/CreateCombinationViewModel.kt b/feature/combination/src/main/java/com/example/hackathon/feature/combination/CreateCombinationViewModel.kt index 42c1233..bc0ca96 100644 --- a/feature/combination/src/main/java/com/example/hackathon/feature/combination/CreateCombinationViewModel.kt +++ b/feature/combination/src/main/java/com/example/hackathon/feature/combination/CreateCombinationViewModel.kt @@ -2,150 +2,145 @@ package com.example.hackathon.feature.combination import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.example.hackathon.core.domain.repository.CombinationRepository import com.example.hackathon.core.model.Category import com.example.hackathon.core.model.Combination -import com.example.hackathon.core.domain.repository.CombinationRepository import com.example.hackathon.feature.combination.component.IngredientItem import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import javax.inject.Inject @HiltViewModel class CreateCombinationViewModel - @Inject - constructor( - private val repository: CombinationRepository, - ) : ViewModel() { - private val _uiState = MutableStateFlow(CreateCombinationUiState()) - val uiState: StateFlow = _uiState.asStateFlow() - - fun updateTitle(title: String) { - _uiState.value = _uiState.value.copy(title = title) - } - - fun updateDescription(description: String) { - _uiState.value = _uiState.value.copy(description = description) - } +@Inject +constructor( + private val repository: CombinationRepository +) : ViewModel() { + private val _uiState = MutableStateFlow(CreateCombinationUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + fun updateTitle(title: String) { + _uiState.value = _uiState.value.copy(title = title) + } - fun updateCategory(category: Category) { - _uiState.value = _uiState.value.copy(category = category) - } + fun updateDescription(description: String) { + _uiState.value = _uiState.value.copy(description = description) + } - fun updateIngredients(ingredients: String) { - _uiState.value = _uiState.value.copy(ingredients = ingredients) - } + fun updateCategory(category: Category) { + _uiState.value = _uiState.value.copy(category = category) + } - fun addIngredientTag(tag: String) { - val currentTags = _uiState.value.ingredientTags.toMutableList() - if (!currentTags.contains(tag)) { - currentTags.add(tag) - _uiState.value = _uiState.value.copy(ingredientTags = currentTags) - } - } + fun updateIngredients(ingredients: String) { + _uiState.value = _uiState.value.copy(ingredients = ingredients) + } - fun removeIngredientTag(tag: String) { - val currentTags = _uiState.value.ingredientTags.toMutableList() - currentTags.remove(tag) + fun addIngredientTag(tag: String) { + val currentTags = _uiState.value.ingredientTags.toMutableList() + if (!currentTags.contains(tag)) { + currentTags.add(tag) _uiState.value = _uiState.value.copy(ingredientTags = currentTags) } + } - fun updateIngredientName( - index: Int, - name: String, - ) { - val currentIngredients = _uiState.value.ingredientsList.toMutableList() - if (index < currentIngredients.size) { - currentIngredients[index] = currentIngredients[index].copy(name = name) - } else { - currentIngredients.add(IngredientItem(name, "")) - } - _uiState.value = _uiState.value.copy(ingredientsList = currentIngredients) - } + fun removeIngredientTag(tag: String) { + val currentTags = _uiState.value.ingredientTags.toMutableList() + currentTags.remove(tag) + _uiState.value = _uiState.value.copy(ingredientTags = currentTags) + } - fun updateIngredientQuantity( - index: Int, - quantity: String, - ) { - val currentIngredients = _uiState.value.ingredientsList.toMutableList() - if (index < currentIngredients.size) { - currentIngredients[index] = currentIngredients[index].copy(quantity = quantity) - } else { - currentIngredients.add(IngredientItem("", quantity)) - } - _uiState.value = _uiState.value.copy(ingredientsList = currentIngredients) + fun updateIngredientName(index: Int, name: String) { + val currentIngredients = _uiState.value.ingredientsList.toMutableList() + if (index < currentIngredients.size) { + currentIngredients[index] = currentIngredients[index].copy(name = name) + } else { + currentIngredients.add(IngredientItem(name, "")) } + _uiState.value = _uiState.value.copy(ingredientsList = currentIngredients) + } - fun addIngredient() { - val currentIngredients = _uiState.value.ingredientsList.toMutableList() - currentIngredients.add(IngredientItem("", "")) - _uiState.value = _uiState.value.copy(ingredientsList = currentIngredients) + fun updateIngredientQuantity(index: Int, quantity: String) { + val currentIngredients = _uiState.value.ingredientsList.toMutableList() + if (index < currentIngredients.size) { + currentIngredients[index] = currentIngredients[index].copy(quantity = quantity) + } else { + currentIngredients.add(IngredientItem("", quantity)) } + _uiState.value = _uiState.value.copy(ingredientsList = currentIngredients) + } - fun updateIsPublic(isPublic: Boolean) { - _uiState.value = _uiState.value.copy(isPublic = isPublic) - } + fun addIngredient() { + val currentIngredients = _uiState.value.ingredientsList.toMutableList() + currentIngredients.add(IngredientItem("", "")) + _uiState.value = _uiState.value.copy(ingredientsList = currentIngredients) + } - fun updateImageUri(uri: android.net.Uri?) { - val currentUris = _uiState.value.imageUris.toMutableList() - if (uri != null && currentUris.size < 5) { - currentUris.add(uri) - _uiState.value = _uiState.value.copy(imageUris = currentUris) - } - } + fun updateIsPublic(isPublic: Boolean) { + _uiState.value = _uiState.value.copy(isPublic = isPublic) + } - fun removeImageUri(uri: android.net.Uri) { - val currentUris = _uiState.value.imageUris.toMutableList() - currentUris.remove(uri) + fun updateImageUri(uri: android.net.Uri?) { + val currentUris = _uiState.value.imageUris.toMutableList() + if (uri != null && currentUris.size < 5) { + currentUris.add(uri) _uiState.value = _uiState.value.copy(imageUris = currentUris) } + } - fun addTag(tag: String) { - val currentTags = _uiState.value.tags.toMutableList() - if (!currentTags.contains(tag)) { - currentTags.add(tag) - _uiState.value = _uiState.value.copy(tags = currentTags) - } - } + fun removeImageUri(uri: android.net.Uri) { + val currentUris = _uiState.value.imageUris.toMutableList() + currentUris.remove(uri) + _uiState.value = _uiState.value.copy(imageUris = currentUris) + } - fun removeTag(tag: String) { - val currentTags = _uiState.value.tags.toMutableList() - currentTags.remove(tag) + fun addTag(tag: String) { + val currentTags = _uiState.value.tags.toMutableList() + if (!currentTags.contains(tag)) { + currentTags.add(tag) _uiState.value = _uiState.value.copy(tags = currentTags) } + } + + fun removeTag(tag: String) { + val currentTags = _uiState.value.tags.toMutableList() + currentTags.remove(tag) + _uiState.value = _uiState.value.copy(tags = currentTags) + } - fun createCombination(onSuccess: (Combination) -> Unit) { - val state = _uiState.value + fun createCombination(onSuccess: (Combination) -> Unit) { + val state = _uiState.value - if (state.title.isBlank() || state.description.isBlank()) { - _uiState.value = state.copy(error = "제목과 설명을 입력해주세요") - return - } + if (state.title.isBlank() || state.description.isBlank()) { + _uiState.value = state.copy(error = "제목과 설명을 입력해주세요") + return + } - // 재료 리스트에서 유효한 재료만 필터링 - val validIngredients = - state.ingredientsList - .filter { it.name.isNotBlank() && it.quantity.isNotBlank() } - .map { "${it.name} ${it.quantity}" } + // 재료 리스트에서 유효한 재료만 필터링 + val validIngredients = + state.ingredientsList + .filter { it.name.isNotBlank() && it.quantity.isNotBlank() } + .map { "${it.name} ${it.quantity}" } - if (validIngredients.isEmpty()) { - _uiState.value = state.copy(error = "재료를 입력해주세요") - return - } + if (validIngredients.isEmpty()) { + _uiState.value = state.copy(error = "재료를 입력해주세요") + return + } - viewModelScope.launch { - _uiState.value = state.copy(isLoading = true, error = null) + viewModelScope.launch { + _uiState.value = state.copy(isLoading = true, error = null) - repository.createCombination( + repository + .createCombination( title = state.title, description = state.description, category = state.category, ingredients = validIngredients, tags = state.tags, imageUri = state.imageUris.firstOrNull(), - isPublic = state.isPublic, + isPublic = state.isPublic ).fold( onSuccess = { combination -> _uiState.value = state.copy(isLoading = false) @@ -155,17 +150,17 @@ class CreateCombinationViewModel _uiState.value = state.copy( isLoading = false, - error = error.message ?: "등록에 실패했습니다", + error = error.message ?: "등록에 실패했습니다" ) - }, + } ) - } } + } - fun clearError() { - _uiState.value = _uiState.value.copy(error = null) - } + fun clearError() { + _uiState.value = _uiState.value.copy(error = null) } +} data class CreateCombinationUiState( val title: String = "", @@ -184,5 +179,5 @@ data class CreateCombinationUiState( val ingredientTags: List = emptyList(), val isPublic: Boolean = true, val isLoading: Boolean = false, - val error: String? = null, + val error: String? = null ) diff --git a/feature/combination/src/main/java/com/example/hackathon/feature/combination/DetailScreen.kt b/feature/combination/src/main/java/com/example/hackathon/feature/combination/DetailScreen.kt index adafcee..3f97e4d 100644 --- a/feature/combination/src/main/java/com/example/hackathon/feature/combination/DetailScreen.kt +++ b/feature/combination/src/main/java/com/example/hackathon/feature/combination/DetailScreen.kt @@ -49,10 +49,10 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage import com.example.hackathon.core.designsystem.R -import com.example.hackathon.core.model.DummyData -import com.example.hackathon.feature.combination.DetailViewModel import com.example.hackathon.core.designsystem.theme.HackathonTheme import com.example.hackathon.core.designsystem.theme.Primary +import com.example.hackathon.core.model.DummyData +import com.example.hackathon.feature.combination.DetailViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -60,7 +60,7 @@ fun DetailScreen( modifier: Modifier = Modifier, recipeId: Long, viewModel: DetailViewModel = hiltViewModel(), - onNavigateBack: () -> Unit = {}, + onNavigateBack: () -> Unit = {} ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -73,7 +73,7 @@ fun DetailScreen( val recipe = uiState.recipeDetail Column( - modifier = modifier.fillMaxSize(), + modifier = modifier.fillMaxSize() ) { Row( modifier = @@ -81,26 +81,26 @@ fun DetailScreen( .fillMaxWidth() .statusBarsPadding() // 상태바 침범 방지 (중요) .height(80.dp), - verticalAlignment = Alignment.CenterVertically, + verticalAlignment = Alignment.CenterVertically ) { IconButton( onClick = onNavigateBack, - modifier = Modifier.size(48.dp), + modifier = Modifier.size(48.dp) ) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "뒤로가기", + contentDescription = "뒤로가기" ) } Box( modifier = Modifier.weight(1f), - contentAlignment = Alignment.Center, + contentAlignment = Alignment.Center ) { Image( painter = painterResource(id = R.drawable.ic_logo), contentDescription = "logo", - modifier = Modifier.width(90.dp), + modifier = Modifier.width(90.dp) ) } @@ -118,23 +118,23 @@ fun DetailScreen( colors = listOf( Color.Black.copy(alpha = 0.15f), - Color.Transparent, - ), - ), - ), + Color.Transparent + ) + ) + ) ) LazyColumn( modifier = Modifier .fillMaxSize() - .padding(start = 20.dp, end = 20.dp, top = 20.dp), + .padding(start = 20.dp, end = 20.dp, top = 20.dp) ) { item { when { uiState.isLoading -> { CircularProgressIndicator( - modifier = Modifier.padding(20.dp), + modifier = Modifier.padding(20.dp) ) } @@ -142,14 +142,14 @@ fun DetailScreen( Text( text = uiState.error ?: "오류가 발생했습니다", color = MaterialTheme.colorScheme.error, - modifier = Modifier.padding(20.dp), + modifier = Modifier.padding(20.dp) ) } recipe == null -> { Text( text = "레시피를 찾을 수 없습니다", - modifier = Modifier.padding(20.dp), + modifier = Modifier.padding(20.dp) ) } @@ -163,7 +163,7 @@ fun DetailScreen( modifier = Modifier .fillMaxWidth() - .height(260.dp), + .height(260.dp) ) { page -> AsyncImage( model = recipe.images[page], @@ -172,7 +172,7 @@ fun DetailScreen( modifier = Modifier .fillMaxSize() - .clip(RoundedCornerShape(16.dp)), + .clip(RoundedCornerShape(16.dp)) ) } @@ -180,7 +180,7 @@ fun DetailScreen( Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center, + horizontalArrangement = Arrangement.Center ) { repeat(recipe.images.size) { index -> Box( @@ -188,16 +188,21 @@ fun DetailScreen( Modifier .padding(4.dp) .size( - if (pagerState.currentPage == index) 8.dp else 6.dp, - ) - .clip(CircleShape) + if (pagerState.currentPage == + index + ) { + 8.dp + } else { + 6.dp + } + ).clip(CircleShape) .background( if (pagerState.currentPage == index) { HackathonTheme.colors.primary } else { HackathonTheme.colors.gray50 - }, - ), + } + ) ) } } @@ -209,14 +214,14 @@ fun DetailScreen( Modifier .fillMaxWidth() .padding(vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically, + verticalAlignment = Alignment.CenterVertically ) { Text( text = recipe.title, style = HackathonTheme.typography.Head2_bold, color = HackathonTheme.colors.black, maxLines = 1, - overflow = TextOverflow.Ellipsis, + overflow = TextOverflow.Ellipsis ) Spacer(modifier = Modifier.weight(1f)) @@ -240,7 +245,7 @@ fun DetailScreen( .size(20.dp) .clickable { viewModel.toggleLike() - }, + } ) Spacer(modifier = Modifier.width(4.dp)) @@ -248,7 +253,7 @@ fun DetailScreen( Text( text = recipe.stats.likesCount.toString(), style = HackathonTheme.typography.Body_medium, - color = HackathonTheme.colors.gray700, + color = HackathonTheme.colors.gray700 ) } @@ -258,14 +263,14 @@ fun DetailScreen( text = authorText, style = HackathonTheme.typography.Body_medium, color = Color(0xFF555555), - modifier = Modifier.padding(bottom = 10.dp), + modifier = Modifier.padding(bottom = 10.dp) ) if (recipe.tags.isNotEmpty()) { FlowRow( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(6.dp), - verticalArrangement = Arrangement.spacedBy(6.dp), + verticalArrangement = Arrangement.spacedBy(6.dp) ) { recipe.tags.forEachIndexed { index, tag -> val isFirstTag = index == 0 @@ -276,27 +281,25 @@ fun DetailScreen( if (isFirstTag) { Modifier.background( Primary, - RoundedCornerShape(15.dp), + RoundedCornerShape(15.dp) ) } else { Modifier .background( Color.White, - RoundedCornerShape(15.dp), - ) - .border( + RoundedCornerShape(15.dp) + ).border( width = 1.dp, color = Primary, - shape = RoundedCornerShape(15.dp), + shape = RoundedCornerShape(15.dp) ) - }, - ) - .padding(horizontal = 10.dp, vertical = 4.dp), + } + ).padding(horizontal = 10.dp, vertical = 4.dp) ) { Text( text = tag, style = HackathonTheme.typography.Caption_medium, - color = if (isFirstTag) Color.White else Primary, + color = if (isFirstTag) Color.White else Primary ) } } @@ -308,7 +311,7 @@ fun DetailScreen( text = recipe.description, style = HackathonTheme.typography.Caption_medium, color = HackathonTheme.colors.gray700, - modifier = Modifier.padding(vertical = 15.dp), + modifier = Modifier.padding(vertical = 15.dp) ) Spacer(modifier = Modifier.height(12.dp)) @@ -319,34 +322,33 @@ fun DetailScreen( .fillMaxWidth() .background( Color(0xFFF7F7F7), - shape = RoundedCornerShape(10.dp), - ) - .border( + shape = RoundedCornerShape(10.dp) + ).border( shape = RoundedCornerShape(10.dp), color = Color(0xFFCCCCCC), - width = 1.dp, - ) - .padding(horizontal = 20.dp, vertical = 15.dp), + width = 1.dp + ).padding(horizontal = 20.dp, vertical = 15.dp) ) { Text( text = authorText + "님의 레시피", style = HackathonTheme.typography.Sub1_semibold, - color = HackathonTheme.colors.black, + color = HackathonTheme.colors.black ) Spacer(modifier = Modifier.height(12.dp)) recipe.ingredients.forEach { Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically ) { Text( text = it.name, style = HackathonTheme.typography.Body_medium, - color = HackathonTheme.colors.black, + color = HackathonTheme.colors.black ) Spacer(modifier = Modifier.weight(1f)) @@ -354,7 +356,7 @@ fun DetailScreen( Text( text = it.amount, style = HackathonTheme.typography.Body_medium, - color = HackathonTheme.colors.black, + color = HackathonTheme.colors.black ) } } diff --git a/feature/combination/src/main/java/com/example/hackathon/feature/combination/DetailViewModel.kt b/feature/combination/src/main/java/com/example/hackathon/feature/combination/DetailViewModel.kt index f89bfb4..2da5344 100644 --- a/feature/combination/src/main/java/com/example/hackathon/feature/combination/DetailViewModel.kt +++ b/feature/combination/src/main/java/com/example/hackathon/feature/combination/DetailViewModel.kt @@ -2,110 +2,110 @@ package com.example.hackathon.feature.combination import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.example.hackathon.core.model.RecipeDetail import com.example.hackathon.core.domain.repository.CombinationRepository import com.example.hackathon.core.domain.repository.RecipeRepository +import com.example.hackathon.core.model.RecipeDetail import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import javax.inject.Inject // TODO: 디자인 확인 후 UI 조정 필요 @HiltViewModel class DetailViewModel - @Inject - constructor( - private val repository: CombinationRepository, - private val recipeRepository: RecipeRepository, - ) : ViewModel() { - private val _uiState = MutableStateFlow(DetailUiState()) - val uiState: StateFlow = _uiState.asStateFlow() +@Inject +constructor( + private val repository: CombinationRepository, + private val recipeRepository: RecipeRepository +) : ViewModel() { + private val _uiState = MutableStateFlow(DetailUiState()) + val uiState: StateFlow = _uiState.asStateFlow() - fun loadRecipeDetail(id: Long) { - viewModelScope.launch { - _uiState.value = - _uiState.value.copy( - isLoading = true, - error = null, - ) - - repository.getRecipeDetail(id).fold( - onSuccess = { result -> - _uiState.value = - _uiState.value.copy( - isLoading = false, - recipeDetail = result, - ) - }, - onFailure = { throwable -> - _uiState.value = - _uiState.value.copy( - isLoading = false, - error = throwable.message ?: "레시피를 불러오지 못했습니다", - ) - }, + fun loadRecipeDetail(id: Long) { + viewModelScope.launch { + _uiState.value = + _uiState.value.copy( + isLoading = true, + error = null ) - } + + repository.getRecipeDetail(id).fold( + onSuccess = { result -> + _uiState.value = + _uiState.value.copy( + isLoading = false, + recipeDetail = result + ) + }, + onFailure = { throwable -> + _uiState.value = + _uiState.value.copy( + isLoading = false, + error = throwable.message ?: "레시피를 불러오지 못했습니다" + ) + } + ) } + } - fun toggleLike() { - val current = _uiState.value.recipeDetail ?: return + fun toggleLike() { + val current = _uiState.value.recipeDetail ?: return - val isLiked = current.userInteraction.isLiked - val currentCount = current.stats.likesCount + val isLiked = current.userInteraction.isLiked + val currentCount = current.stats.likesCount - // 낙관적 업데이트: UI를 먼저 업데이트 - val updated = - current.copy( - stats = - current.stats.copy( - likesCount = - if (isLiked) currentCount - 1 else currentCount + 1, - ), - userInteraction = - current.userInteraction.copy( - isLiked = !isLiked, - ), - ) + // 낙관적 업데이트: UI를 먼저 업데이트 + val updated = + current.copy( + stats = + current.stats.copy( + likesCount = + if (isLiked) currentCount - 1 else currentCount + 1 + ), + userInteraction = + current.userInteraction.copy( + isLiked = !isLiked + ) + ) - _uiState.value = - _uiState.value.copy( - recipeDetail = updated, - ) + _uiState.value = + _uiState.value.copy( + recipeDetail = updated + ) - // API 호출 - viewModelScope.launch { - val result = - if (isLiked) { - recipeRepository.unlikeRecipe(current.id) - } else { - recipeRepository.likeRecipe(current.id) - } + // API 호출 + viewModelScope.launch { + val result = + if (isLiked) { + recipeRepository.unlikeRecipe(current.id) + } else { + recipeRepository.likeRecipe(current.id) + } - result.fold( - onSuccess = { - // API 호출 성공 - 이미 UI는 업데이트됨 - }, - onFailure = { throwable -> - // API 호출 실패 시 원래 상태로 롤백 - _uiState.value = - _uiState.value.copy( - recipeDetail = current, - ) - _uiState.value = - _uiState.value.copy( - error = throwable.message ?: "좋아요 처리에 실패했습니다", - ) - }, - ) - } + result.fold( + onSuccess = { + // API 호출 성공 - 이미 UI는 업데이트됨 + }, + onFailure = { throwable -> + // API 호출 실패 시 원래 상태로 롤백 + _uiState.value = + _uiState.value.copy( + recipeDetail = current + ) + _uiState.value = + _uiState.value.copy( + error = throwable.message ?: "좋아요 처리에 실패했습니다" + ) + } + ) } } +} data class DetailUiState( val isLoading: Boolean = false, val error: String? = null, - val recipeDetail: RecipeDetail? = null, + val recipeDetail: RecipeDetail? = null ) diff --git a/feature/combination/src/main/java/com/example/hackathon/feature/combination/component/HashTagInputSection.kt b/feature/combination/src/main/java/com/example/hackathon/feature/combination/component/HashTagInputSection.kt index ec42fd4..73fde89 100644 --- a/feature/combination/src/main/java/com/example/hackathon/feature/combination/component/HashTagInputSection.kt +++ b/feature/combination/src/main/java/com/example/hackathon/feature/combination/component/HashTagInputSection.kt @@ -28,11 +28,11 @@ fun HashTagInputSection( onTagInputChange: (String) -> Unit, onAddTag: (String) -> Unit, onRemoveTag: (String) -> Unit, - modifier: Modifier = Modifier, + modifier: Modifier = Modifier ) { Column( modifier = modifier, - verticalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) ) { // 해시태그 입력 OutlinedTextField( @@ -41,7 +41,8 @@ fun HashTagInputSection( // 공백, 쉼표, 엔터 입력 시 자동으로 태그 추가 if (newValue.endsWith(" ") || newValue.endsWith(",") || newValue.endsWith("\n")) { val tagText = - newValue.substringBeforeLast(" ") + newValue + .substringBeforeLast(" ") .substringBeforeLast(",") .substringBeforeLast("\n") val normalized = tagText.trim().trimStart('#').trim() @@ -59,23 +60,27 @@ fun HashTagInputSection( Text( text = "띄어쓰기로 입력 (1개~5개)", style = MaterialTheme.typography.bodyMedium, - color = com.example.hackathon.core.designsystem.theme.Gray700, + color = com.example.hackathon.core.designsystem.theme.Gray700 ) }, modifier = Modifier.fillMaxWidth(), singleLine = true, - shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp), + shape = + androidx.compose.foundation.shape + .RoundedCornerShape(12.dp), colors = androidx.compose.material3.OutlinedTextFieldDefaults.colors( focusedBorderColor = com.example.hackathon.core.designsystem.theme.Primary, - unfocusedBorderColor = com.example.hackathon.core.designsystem.theme.Primary.copy(alpha = 0.5f), + unfocusedBorderColor = + com.example.hackathon.core.designsystem.theme.Primary + .copy(alpha = 0.5f) ), leadingIcon = { Text( text = "#", style = MaterialTheme.typography.bodyLarge, color = com.example.hackathon.core.designsystem.theme.Primary, - modifier = Modifier.padding(start = 16.dp), + modifier = Modifier.padding(start = 16.dp) ) }, trailingIcon = { @@ -87,27 +92,27 @@ fun HashTagInputSection( onAddTag(normalized) onTagInputChange("") } - }, + } ) { Icon( imageVector = Icons.Default.Add, - contentDescription = "태그 추가", + contentDescription = "태그 추가" ) } } - }, + } ) if (tags.isNotEmpty()) { FlowRow( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) ) { tags.forEach { tag -> TagChip( text = tag, - onRemove = { onRemoveTag(tag) }, + onRemove = { onRemoveTag(tag) } ) } } @@ -123,6 +128,6 @@ private fun HashTagInputSectionPreview() { tags = listOf("#서브웨이", "#편의점"), onTagInputChange = {}, onAddTag = { }, - onRemoveTag = {}, + onRemoveTag = {} ) } diff --git a/feature/combination/src/main/java/com/example/hackathon/feature/combination/component/ImageUploadSection.kt b/feature/combination/src/main/java/com/example/hackathon/feature/combination/component/ImageUploadSection.kt index 36df216..ada50b3 100644 --- a/feature/combination/src/main/java/com/example/hackathon/feature/combination/component/ImageUploadSection.kt +++ b/feature/combination/src/main/java/com/example/hackathon/feature/combination/component/ImageUploadSection.kt @@ -47,11 +47,11 @@ fun ImageUploadSection( imageUris: List, onImageClick: () -> Unit, onRemoveImage: (Uri) -> Unit, - modifier: Modifier = Modifier, + modifier: Modifier = Modifier ) { Column( modifier = modifier, - verticalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) ) { // 메인 이미지 업로드 영역 if (imageUris.isEmpty()) { @@ -72,33 +72,31 @@ fun ImageUploadSection( width = strokeWidth, pathEffect = PathEffect.dashPathEffect( - floatArrayOf(dashWidth, dashGap), - ), + floatArrayOf(dashWidth, dashGap) + ) ), - cornerRadius = CornerRadius(12.dp.toPx()), + cornerRadius = CornerRadius(12.dp.toPx()) ) - } - .background( + }.background( color = Color(0xFFFFF3F3), - shape = RoundedCornerShape(12.dp), - ) - .clickable { onImageClick() }, - contentAlignment = Alignment.Center, + shape = RoundedCornerShape(12.dp) + ).clickable { onImageClick() }, + contentAlignment = Alignment.Center ) { Column( horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) ) { Icon( painter = painterResource(R.drawable.ic_camera), contentDescription = "이미지 등록", tint = Gray700, - modifier = Modifier.size(32.dp), + modifier = Modifier.size(32.dp) ) Text( text = "사진 등록 (최대 5장)", style = MaterialTheme.typography.bodyMedium, - color = Gray700, + color = Gray700 ) } } @@ -108,7 +106,7 @@ fun ImageUploadSection( modifier = Modifier .fillMaxWidth() - .height(180.dp), + .height(180.dp) ) { AsyncImage( model = imageUris.first(), @@ -118,7 +116,7 @@ fun ImageUploadSection( .fillMaxWidth() .height(180.dp) .clip(RoundedCornerShape(12.dp)), - contentScale = ContentScale.Crop, + contentScale = ContentScale.Crop ) // 이미지 제거 버튼 IconButton( @@ -126,7 +124,7 @@ fun ImageUploadSection( modifier = Modifier .align(Alignment.TopEnd) - .padding(8.dp), + .padding(8.dp) ) { Box( modifier = @@ -134,15 +132,15 @@ fun ImageUploadSection( .size(32.dp) .background( Color.Black.copy(alpha = 0.6f), - CircleShape, + CircleShape ), - contentAlignment = Alignment.Center, + contentAlignment = Alignment.Center ) { Icon( imageVector = Icons.Default.Close, contentDescription = "이미지 제거", tint = Color.White, - modifier = Modifier.size(20.dp), + modifier = Modifier.size(20.dp) ) } } @@ -152,7 +150,7 @@ fun ImageUploadSection( if (imageUris.size > 1) { Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) ) { imageUris.drop(1).forEach { uri -> Box( @@ -160,20 +158,20 @@ fun ImageUploadSection( Modifier .weight(1f) .height(60.dp) - .clip(RoundedCornerShape(8.dp)), + .clip(RoundedCornerShape(8.dp)) ) { AsyncImage( model = uri, contentDescription = "썸네일", modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop, + contentScale = ContentScale.Crop ) IconButton( onClick = { onRemoveImage(uri) }, modifier = Modifier .align(Alignment.TopEnd) - .size(20.dp), + .size(20.dp) ) { Icon( imageVector = Icons.Default.Close, @@ -184,9 +182,8 @@ fun ImageUploadSection( .size(16.dp) .background( Color.Black.copy(alpha = 0.6f), - CircleShape, - ) - .padding(4.dp), + CircleShape + ).padding(4.dp) ) } } @@ -199,15 +196,19 @@ fun ImageUploadSection( .weight(1f) .height(60.dp) .background(Gray50, RoundedCornerShape(8.dp)) - .border(1.dp, Gray700.copy(alpha = 0.3f), RoundedCornerShape(8.dp)) + .border( + 1.dp, + Gray700.copy(alpha = 0.3f), + RoundedCornerShape(8.dp) + ) .clickable { onImageClick() }, - contentAlignment = Alignment.Center, + contentAlignment = Alignment.Center ) { Icon( imageVector = Icons.Default.Add, contentDescription = "이미지 추가", tint = Gray700, - modifier = Modifier.size(24.dp), + modifier = Modifier.size(24.dp) ) } } @@ -216,7 +217,7 @@ fun ImageUploadSection( // 첫 번째 이미지만 있을 때 추가 버튼 Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) ) { repeat(4) { Box( @@ -225,15 +226,19 @@ fun ImageUploadSection( .weight(1f) .height(60.dp) .background(Gray50, RoundedCornerShape(8.dp)) - .border(1.dp, Gray700.copy(alpha = 0.3f), RoundedCornerShape(8.dp)) + .border( + 1.dp, + Gray700.copy(alpha = 0.3f), + RoundedCornerShape(8.dp) + ) .clickable { onImageClick() }, - contentAlignment = Alignment.Center, + contentAlignment = Alignment.Center ) { Icon( imageVector = Icons.Default.Add, contentDescription = "이미지 추가", tint = Gray700, - modifier = Modifier.size(24.dp), + modifier = Modifier.size(24.dp) ) } } @@ -249,6 +254,6 @@ private fun ImageUploadSectionPreview() { ImageUploadSection( imageUris = emptyList(), onImageClick = {}, - onRemoveImage = {}, + onRemoveImage = {} ) } diff --git a/feature/combination/src/main/java/com/example/hackathon/feature/combination/component/IngredientInputSection.kt b/feature/combination/src/main/java/com/example/hackathon/feature/combination/component/IngredientInputSection.kt index 1c363b6..340ae35 100644 --- a/feature/combination/src/main/java/com/example/hackathon/feature/combination/component/IngredientInputSection.kt +++ b/feature/combination/src/main/java/com/example/hackathon/feature/combination/component/IngredientInputSection.kt @@ -19,10 +19,7 @@ import androidx.compose.ui.unit.dp import com.example.hackathon.core.designsystem.theme.Gray700 import com.example.hackathon.core.designsystem.theme.Primary -data class IngredientItem( - val name: String, - val quantity: String, -) +data class IngredientItem(val name: String, val quantity: String) @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -31,24 +28,24 @@ fun IngredientInputSection( onIngredientNameChange: (Int, String) -> Unit, onIngredientQuantityChange: (Int, String) -> Unit, onAddIngredient: () -> Unit, - modifier: Modifier = Modifier, + modifier: Modifier = Modifier ) { Column( modifier = modifier, - verticalArrangement = Arrangement.spacedBy(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) ) { // 재료 레이블 Text( text = "재료", style = MaterialTheme.typography.titleMedium, - color = Color.Black, + color = Color.Black ) // 재료 입력 필드들 ingredients.forEachIndexed { index, ingredient -> Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) ) { // 재료명 입력 OutlinedTextField( @@ -58,7 +55,7 @@ fun IngredientInputSection( Text( text = if (index == 0) "재료명 (예: 땅콩소스)" else "재료명", style = MaterialTheme.typography.bodyMedium, - color = Gray700, + color = Gray700 ) }, modifier = Modifier.weight(1f), @@ -67,8 +64,8 @@ fun IngredientInputSection( colors = androidx.compose.material3.OutlinedTextFieldDefaults.colors( focusedBorderColor = Primary, - unfocusedBorderColor = Primary.copy(alpha = 0.5f), - ), + unfocusedBorderColor = Primary.copy(alpha = 0.5f) + ) ) // 용량 입력 @@ -79,7 +76,7 @@ fun IngredientInputSection( Text( text = if (index == 0) "용량 (예: 2스푼)" else "용량", style = MaterialTheme.typography.bodyMedium, - color = Gray700, + color = Gray700 ) }, modifier = Modifier.weight(1f), @@ -88,8 +85,8 @@ fun IngredientInputSection( colors = androidx.compose.material3.OutlinedTextFieldDefaults.colors( focusedBorderColor = Primary, - unfocusedBorderColor = Primary.copy(alpha = 0.5f), - ), + unfocusedBorderColor = Primary.copy(alpha = 0.5f) + ) ) } } @@ -102,7 +99,7 @@ fun IngredientInputSection( modifier = Modifier .padding(start = 4.dp) - .clickable { onAddIngredient() }, + .clickable { onAddIngredient() } ) } } @@ -115,10 +112,10 @@ private fun IngredientInputSectionPreview() { listOf( IngredientItem("땅콩소스", "2스푼"), IngredientItem("굴소스", "1스푼"), - IngredientItem("", ""), + IngredientItem("", "") ), onIngredientNameChange = { _, _ -> }, onIngredientQuantityChange = { _, _ -> }, - onAddIngredient = {}, + onAddIngredient = {} ) } diff --git a/feature/combination/src/main/java/com/example/hackathon/feature/combination/component/VisibilitySelectionSection.kt b/feature/combination/src/main/java/com/example/hackathon/feature/combination/component/VisibilitySelectionSection.kt index ca0c732..1688a60 100644 --- a/feature/combination/src/main/java/com/example/hackathon/feature/combination/component/VisibilitySelectionSection.kt +++ b/feature/combination/src/main/java/com/example/hackathon/feature/combination/component/VisibilitySelectionSection.kt @@ -22,15 +22,15 @@ import com.example.hackathon.core.designsystem.theme.Primary fun VisibilitySelectionSection( isPublic: Boolean, onPublicChange: (Boolean) -> Unit, - modifier: Modifier = Modifier, + modifier: Modifier = Modifier ) { Column( modifier = modifier, - verticalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) ) { Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(12.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp) ) { // 전체 공개 버튼 if (isPublic) { @@ -40,8 +40,8 @@ fun VisibilitySelectionSection( colors = ButtonDefaults.buttonColors( containerColor = Primary, - contentColor = Color.White, - ), + contentColor = Color.White + ) ) { Text("전체 공개") } @@ -51,9 +51,9 @@ fun VisibilitySelectionSection( modifier = Modifier.weight(1f), colors = ButtonDefaults.outlinedButtonColors( - contentColor = Primary, + contentColor = Primary ), - border = BorderStroke(1.dp, Gray700), + border = BorderStroke(1.dp, Gray700) ) { Text("전체 공개") } @@ -66,8 +66,8 @@ fun VisibilitySelectionSection( colors = ButtonDefaults.buttonColors( containerColor = Primary, - contentColor = Color.White, - ), + contentColor = Color.White + ) ) { Text("나만 보기") } @@ -77,9 +77,9 @@ fun VisibilitySelectionSection( modifier = Modifier.weight(1f), colors = ButtonDefaults.outlinedButtonColors( - contentColor = Primary, + contentColor = Primary ), - border = BorderStroke(1.dp, Gray700), + border = BorderStroke(1.dp, Gray700) ) { Text("나만 보기") } @@ -93,6 +93,6 @@ fun VisibilitySelectionSection( private fun VisibilitySelectionSectionPreview() { VisibilitySelectionSection( isPublic = true, - onPublicChange = {}, + onPublicChange = {} ) } diff --git a/feature/combination/src/test/java/com/example/hackathon/feature/combination/CreateCombinationViewModelTest.kt b/feature/combination/src/test/java/com/example/hackathon/feature/combination/CreateCombinationViewModelTest.kt index 3f248c3..d08cc6c 100644 --- a/feature/combination/src/test/java/com/example/hackathon/feature/combination/CreateCombinationViewModelTest.kt +++ b/feature/combination/src/test/java/com/example/hackathon/feature/combination/CreateCombinationViewModelTest.kt @@ -1,10 +1,10 @@ package com.example.hackathon.feature.combination import app.cash.turbine.test +import com.example.hackathon.core.domain.repository.CombinationRepository import com.example.hackathon.core.model.Category import com.example.hackathon.core.model.Combination import com.example.hackathon.core.model.User -import com.example.hackathon.core.domain.repository.CombinationRepository import io.mockk.coEvery import io.mockk.mockk import kotlinx.coroutines.test.advanceUntilIdle @@ -31,161 +31,155 @@ class CreateCombinationViewModelTest { } @Test - fun `제목과 설명 입력 시 상태가 업데이트된다`() = - runTest { - // Given - val title = "서브웨이 꿀조합" - val description = "에그마요에 아보카도 추가" - - // When - viewModel.updateTitle(title) - viewModel.updateDescription(description) - - // Then - viewModel.uiState.test { - val state = awaitItem() - assertEquals(title, state.title) - assertEquals(description, state.description) - cancelAndIgnoreRemainingEvents() - } + fun `제목과 설명 입력 시 상태가 업데이트된다`() = runTest { + // Given + val title = "서브웨이 꿀조합" + val description = "에그마요에 아보카도 추가" + + // When + viewModel.updateTitle(title) + viewModel.updateDescription(description) + + // Then + viewModel.uiState.test { + val state = awaitItem() + assertEquals(title, state.title) + assertEquals(description, state.description) + cancelAndIgnoreRemainingEvents() } + } @Test - fun `태그 추가 시 태그 리스트에 추가된다`() = - runTest { - // Given - val tag = "#서브웨이" - - // When - viewModel.addTag(tag) - - // Then - viewModel.uiState.test { - val state = awaitItem() - assertEquals(1, state.tags.size) - assertEquals(tag, state.tags.first()) - cancelAndIgnoreRemainingEvents() - } + fun `태그 추가 시 태그 리스트에 추가된다`() = runTest { + // Given + val tag = "#서브웨이" + + // When + viewModel.addTag(tag) + + // Then + viewModel.uiState.test { + val state = awaitItem() + assertEquals(1, state.tags.size) + assertEquals(tag, state.tags.first()) + cancelAndIgnoreRemainingEvents() } + } @Test - fun `조합 등록 성공 시 로딩 상태가 변경된다`() = - runTest { - // Given - val mockCombination = - Combination( - id = "1", - title = "테스트 조합", - description = "테스트 설명", - imageUrl = null, - category = Category.SUBWAY, - ingredients = listOf("재료1 수량1", "재료2 수량2"), - tags = emptyList(), - author = User(id = "user1", nickname = "테스트", profileImageUrl = null), - likeCount = 0, - createdAt = "2024-01-01", - ) - - viewModel.updateTitle("테스트 조합") - viewModel.updateDescription("테스트 설명") - viewModel.updateIngredientName(0, "재료1") - viewModel.updateIngredientQuantity(0, "수량1") - viewModel.updateIngredientName(1, "재료2") - viewModel.updateIngredientQuantity(1, "수량2") - - coEvery { - repository.createCombination( - title = any(), - description = any(), - category = any(), - ingredients = any(), - tags = any(), - imageUri = any(), - ) - } returns Result.success(mockCombination) - - var successCallbackCalled = false - - // When - viewModel.createCombination { - successCallbackCalled = true - } - this.advanceUntilIdle() - - // Then - val finalState = viewModel.uiState.value - assertEquals(false, finalState.isLoading) - assertEquals(null, finalState.error) - assertEquals(true, successCallbackCalled) + fun `조합 등록 성공 시 로딩 상태가 변경된다`() = runTest { + // Given + val mockCombination = + Combination( + id = "1", + title = "테스트 조합", + description = "테스트 설명", + imageUrl = null, + category = Category.SUBWAY, + ingredients = listOf("재료1 수량1", "재료2 수량2"), + tags = emptyList(), + author = User(id = "user1", nickname = "테스트", profileImageUrl = null), + likeCount = 0, + createdAt = "2024-01-01" + ) + + viewModel.updateTitle("테스트 조합") + viewModel.updateDescription("테스트 설명") + viewModel.updateIngredientName(0, "재료1") + viewModel.updateIngredientQuantity(0, "수량1") + viewModel.updateIngredientName(1, "재료2") + viewModel.updateIngredientQuantity(1, "수량2") + + coEvery { + repository.createCombination( + title = any(), + description = any(), + category = any(), + ingredients = any(), + tags = any(), + imageUri = any() + ) + } returns Result.success(mockCombination) + + var successCallbackCalled = false + + // When + viewModel.createCombination { + successCallbackCalled = true } + this.advanceUntilIdle() + + // Then + val finalState = viewModel.uiState.value + assertEquals(false, finalState.isLoading) + assertEquals(null, finalState.error) + assertEquals(true, successCallbackCalled) + } @Test - fun `제목이 비어있으면 에러 메시지가 표시된다`() = - runTest { - // Given - viewModel.updateTitle("") - viewModel.updateDescription("설명") - viewModel.updateIngredientName(0, "재료1") - viewModel.updateIngredientQuantity(0, "수량1") - - // When - viewModel.createCombination {} - - // Then - viewModel.uiState.test { - val state = awaitItem() - assertEquals("제목과 설명을 입력해주세요", state.error) - cancelAndIgnoreRemainingEvents() - } + fun `제목이 비어있으면 에러 메시지가 표시된다`() = runTest { + // Given + viewModel.updateTitle("") + viewModel.updateDescription("설명") + viewModel.updateIngredientName(0, "재료1") + viewModel.updateIngredientQuantity(0, "수량1") + + // When + viewModel.createCombination {} + + // Then + viewModel.uiState.test { + val state = awaitItem() + assertEquals("제목과 설명을 입력해주세요", state.error) + cancelAndIgnoreRemainingEvents() } + } @Test - fun `재료가 비어있으면 에러 메시지가 표시된다`() = - runTest { - // Given - viewModel.updateTitle("제목") - viewModel.updateDescription("설명") - // ingredientsList는 기본값으로 빈 IngredientItem("", "")이 있으므로 - // name과 quantity가 모두 비어있어서 validIngredients가 비어있게 됨 - - // When - viewModel.createCombination {} - - // Then - viewModel.uiState.test { - val state = awaitItem() - assertEquals("재료를 입력해주세요", state.error) - cancelAndIgnoreRemainingEvents() - } + fun `재료가 비어있으면 에러 메시지가 표시된다`() = runTest { + // Given + viewModel.updateTitle("제목") + viewModel.updateDescription("설명") + // ingredientsList는 기본값으로 빈 IngredientItem("", "")이 있으므로 + // name과 quantity가 모두 비어있어서 validIngredients가 비어있게 됨 + + // When + viewModel.createCombination {} + + // Then + viewModel.uiState.test { + val state = awaitItem() + assertEquals("재료를 입력해주세요", state.error) + cancelAndIgnoreRemainingEvents() } + } @Test - fun `조합 등록 실패 시 에러 상태가 표시된다`() = - runTest { - // Given - viewModel.updateTitle("제목") - viewModel.updateDescription("설명") - viewModel.updateIngredientName(0, "재료1") - viewModel.updateIngredientQuantity(0, "수량1") - - coEvery { - repository.createCombination( - title = any(), - description = any(), - category = any(), - ingredients = any(), - tags = any(), - imageUri = any(), - ) - } returns Result.failure(Exception("네트워크 오류")) - - // When - viewModel.createCombination {} - this.advanceUntilIdle() - - // Then - val errorState = viewModel.uiState.value - assertEquals(false, errorState.isLoading) - assertEquals("네트워크 오류", errorState.error) - } + fun `조합 등록 실패 시 에러 상태가 표시된다`() = runTest { + // Given + viewModel.updateTitle("제목") + viewModel.updateDescription("설명") + viewModel.updateIngredientName(0, "재료1") + viewModel.updateIngredientQuantity(0, "수량1") + + coEvery { + repository.createCombination( + title = any(), + description = any(), + category = any(), + ingredients = any(), + tags = any(), + imageUri = any() + ) + } returns Result.failure(Exception("네트워크 오류")) + + // When + viewModel.createCombination {} + this.advanceUntilIdle() + + // Then + val errorState = viewModel.uiState.value + assertEquals(false, errorState.isLoading) + assertEquals("네트워크 오류", errorState.error) + } } diff --git a/feature/combination/src/test/java/com/example/hackathon/feature/combination/MainDispatcherRule.kt b/feature/combination/src/test/java/com/example/hackathon/feature/combination/MainDispatcherRule.kt index efd8d94..5b83499 100644 --- a/feature/combination/src/test/java/com/example/hackathon/feature/combination/MainDispatcherRule.kt +++ b/feature/combination/src/test/java/com/example/hackathon/feature/combination/MainDispatcherRule.kt @@ -11,9 +11,8 @@ import org.junit.runner.Description * 코루틴 테스트를 위한 Main Dispatcher Rule * ViewModel 테스트에서 viewModelScope의 코루틴을 제어하기 위해 사용 */ -class MainDispatcherRule( - private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(), -) : TestWatcher() { +class MainDispatcherRule(private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()) : + TestWatcher() { override fun starting(description: Description) { kotlinx.coroutines.Dispatchers.setMain(testDispatcher) } diff --git a/feature/home/src/main/java/com/example/hackathon/feature/home/HomeCombinationList.kt b/feature/home/src/main/java/com/example/hackathon/feature/home/HomeCombinationList.kt index cfb6aad..814c309 100644 --- a/feature/home/src/main/java/com/example/hackathon/feature/home/HomeCombinationList.kt +++ b/feature/home/src/main/java/com/example/hackathon/feature/home/HomeCombinationList.kt @@ -1,3 +1,5 @@ +@file:Suppress("ktlint:standard:function-naming") // LazyListScope DSL 확장은 PascalCase 관용 + package com.example.hackathon.feature.home import androidx.compose.foundation.layout.fillMaxWidth @@ -9,20 +11,20 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.example.hackathon.core.designsystem.component.CombinationCard +import com.example.hackathon.core.designsystem.theme.HackathonTheme import com.example.hackathon.core.model.Category import com.example.hackathon.core.model.Combination import com.example.hackathon.core.model.User -import com.example.hackathon.core.designsystem.theme.HackathonTheme fun LazyListScope.HomeCombinationList( combinations: List, - onCombinationClick: (String) -> Unit, + onCombinationClick: (String) -> Unit ) { items(combinations) { combination -> CombinationCard( combination = combination, onClick = { onCombinationClick(combination.id) }, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp), + modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp) ) } } @@ -31,11 +33,12 @@ fun LazyListScope.HomeCombinationList( @Composable private fun HomeCombinationListPreview() { HackathonTheme { - val mockUser = User( - id = "user1", - nickname = "테스트유저", - profileImageUrl = null, - ) + val mockUser = + User( + id = "user1", + nickname = "테스트유저", + profileImageUrl = null + ) val mockCombinations = listOf( Combination( @@ -49,7 +52,7 @@ private fun HomeCombinationListPreview() { author = mockUser, likeCount = 10, isLiked = false, - createdAt = "2024-01-01", + createdAt = "2024-01-01" ), Combination( id = "2", @@ -62,13 +65,13 @@ private fun HomeCombinationListPreview() { author = mockUser, likeCount = 5, isLiked = true, - createdAt = "2024-01-02", - ), + createdAt = "2024-01-02" + ) ) androidx.compose.foundation.lazy.LazyColumn { HomeCombinationList( combinations = mockCombinations, - onCombinationClick = {}, + onCombinationClick = {} ) } } diff --git a/feature/home/src/main/java/com/example/hackathon/feature/home/HomeContent.kt b/feature/home/src/main/java/com/example/hackathon/feature/home/HomeContent.kt index e13067c..8555a7e 100644 --- a/feature/home/src/main/java/com/example/hackathon/feature/home/HomeContent.kt +++ b/feature/home/src/main/java/com/example/hackathon/feature/home/HomeContent.kt @@ -4,21 +4,21 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.example.hackathon.core.designsystem.theme.HackathonTheme import com.example.hackathon.core.model.Category import com.example.hackathon.core.model.Combination import com.example.hackathon.core.model.User -import com.example.hackathon.feature.home.component.FilterBar import com.example.hackathon.feature.home.HomeUiState -import com.example.hackathon.core.designsystem.theme.HackathonTheme +import com.example.hackathon.feature.home.component.FilterBar @Composable fun HomeContent( @@ -27,7 +27,7 @@ fun HomeContent( onCategorySelected: (Category) -> Unit, onCombinationClick: (String) -> Unit, onLoadMore: () -> Unit = {}, - modifier: Modifier = Modifier, + modifier: Modifier = Modifier ) { val listState = rememberLazyListState() @@ -51,13 +51,13 @@ fun HomeContent( LazyColumn( state = listState, - modifier = modifier.fillMaxWidth().padding(top = 5.dp), + modifier = modifier.fillMaxWidth().padding(top = 5.dp) ) { item { FilterBar( categories = Category.entries.toList(), selectedCategory = selectedCategory, - onCategorySelected = onCategorySelected, + onCategorySelected = onCategorySelected ) } @@ -78,16 +78,17 @@ fun HomeContent( // 조합 목록 HomeCombinationList( combinations = uiState.combinations, - onCombinationClick = onCombinationClick, + onCombinationClick = onCombinationClick ) // 더 불러오기 로딩 인디케이터 if (uiState.isLoadingMore) { item { CircularProgressIndicator( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(16.dp) ) } } @@ -105,7 +106,7 @@ private fun HomeContentLoadingPreview() { selectedCategory = Category.ALL, onCategorySelected = {}, onCombinationClick = {}, - onLoadMore = {}, + onLoadMore = {} ) } } @@ -119,7 +120,7 @@ private fun HomeContentErrorPreview() { selectedCategory = Category.ALL, onCategorySelected = {}, onCombinationClick = {}, - onLoadMore = {}, + onLoadMore = {} ) } } @@ -133,7 +134,7 @@ private fun HomeContentEmptyPreview() { selectedCategory = Category.ALL, onCategorySelected = {}, onCombinationClick = {}, - onLoadMore = {}, + onLoadMore = {} ) } } @@ -142,11 +143,12 @@ private fun HomeContentEmptyPreview() { @Composable private fun HomeContentWithCombinationsPreview() { HackathonTheme { - val mockUser = User( - id = "user1", - nickname = "테스트유저", - profileImageUrl = null, - ) + val mockUser = + User( + id = "user1", + nickname = "테스트유저", + profileImageUrl = null + ) val mockCombinations = listOf( Combination( @@ -160,7 +162,7 @@ private fun HomeContentWithCombinationsPreview() { author = mockUser, likeCount = 10, isLiked = false, - createdAt = "2024-01-01", + createdAt = "2024-01-01" ), Combination( id = "2", @@ -173,15 +175,15 @@ private fun HomeContentWithCombinationsPreview() { author = mockUser, likeCount = 5, isLiked = true, - createdAt = "2024-01-02", - ), + createdAt = "2024-01-02" + ) ) HomeContent( uiState = HomeUiState(combinations = mockCombinations), selectedCategory = Category.ALL, onCategorySelected = {}, onCombinationClick = {}, - onLoadMore = {}, + onLoadMore = {} ) } } diff --git a/feature/home/src/main/java/com/example/hackathon/feature/home/HomeScreen.kt b/feature/home/src/main/java/com/example/hackathon/feature/home/HomeScreen.kt index 3d3e568..558c197 100644 --- a/feature/home/src/main/java/com/example/hackathon/feature/home/HomeScreen.kt +++ b/feature/home/src/main/java/com/example/hackathon/feature/home/HomeScreen.kt @@ -11,12 +11,12 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController +import com.example.hackathon.core.designsystem.theme.HackathonTheme import com.example.hackathon.core.model.Category import com.example.hackathon.core.model.Combination import com.example.hackathon.core.model.User import com.example.hackathon.feature.home.HomeUiState import com.example.hackathon.feature.home.HomeViewModel -import com.example.hackathon.core.designsystem.theme.HackathonTheme @Composable fun HomeScreen( @@ -24,7 +24,7 @@ fun HomeScreen( viewModel: HomeViewModel = hiltViewModel(), navController: NavController, onCombinationClick: (String) -> Unit = {}, - onCreateClick: () -> Unit = {}, + onCreateClick: () -> Unit = {} ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val selectedCategory by viewModel.selectedCategory.collectAsStateWithLifecycle() @@ -33,7 +33,7 @@ fun HomeScreen( topBar = { HomeTopBar(navController = navController) }, - contentWindowInsets = WindowInsets(0), + contentWindowInsets = WindowInsets(0) ) { innerPadding -> HomeContent( uiState = uiState, @@ -43,9 +43,10 @@ fun HomeScreen( }, onCombinationClick = onCombinationClick, onLoadMore = { viewModel.loadMore() }, - modifier = modifier - .fillMaxWidth() - .padding(innerPadding), + modifier = + modifier + .fillMaxWidth() + .padding(innerPadding) ) } } @@ -54,11 +55,12 @@ fun HomeScreen( @Composable private fun HomeScreenPreview() { HackathonTheme { - val mockUser = User( - id = "user1", - nickname = "테스트유저", - profileImageUrl = null, - ) + val mockUser = + User( + id = "user1", + nickname = "테스트유저", + profileImageUrl = null + ) val mockCombinations = listOf( Combination( @@ -72,25 +74,26 @@ private fun HomeScreenPreview() { author = mockUser, likeCount = 10, isLiked = false, - createdAt = "2024-01-01", - ), + createdAt = "2024-01-01" + ) ) // ViewModel 없이 직접 상태를 전달하는 프리뷰 Scaffold( topBar = { HomeTopBar( - onSearchClick = {}, + onSearchClick = {} ) - }, + } ) { innerPadding -> HomeContent( uiState = HomeUiState(combinations = mockCombinations), selectedCategory = Category.ALL, onCategorySelected = {}, onCombinationClick = {}, - modifier = Modifier - .fillMaxWidth() - .padding(innerPadding), + modifier = + Modifier + .fillMaxWidth() + .padding(innerPadding) ) } } diff --git a/feature/home/src/main/java/com/example/hackathon/feature/home/HomeStateContent.kt b/feature/home/src/main/java/com/example/hackathon/feature/home/HomeStateContent.kt index dc34001..e687849 100644 --- a/feature/home/src/main/java/com/example/hackathon/feature/home/HomeStateContent.kt +++ b/feature/home/src/main/java/com/example/hackathon/feature/home/HomeStateContent.kt @@ -1,3 +1,5 @@ +@file:Suppress("ktlint:standard:function-naming") // LazyListScope DSL 확장은 PascalCase 관용 + package com.example.hackathon.feature.home import androidx.compose.foundation.layout.Box @@ -22,7 +24,7 @@ fun LazyListScope.HomeLoadingState() { Modifier .fillMaxWidth() .padding(top = 40.dp, bottom = 200.dp), - contentAlignment = Alignment.Center, + contentAlignment = Alignment.Center ) { CircularProgressIndicator() } @@ -36,13 +38,13 @@ fun LazyListScope.HomeErrorState(error: String) { Modifier .fillMaxWidth() .padding(top = 40.dp, bottom = 200.dp), - contentAlignment = Alignment.Center, + contentAlignment = Alignment.Center ) { Text( text = error, style = HackathonTheme.typography.Body_medium, color = Gray700, - textAlign = TextAlign.Center, + textAlign = TextAlign.Center ) } } @@ -55,13 +57,13 @@ fun LazyListScope.HomeEmptyState() { Modifier .fillMaxWidth() .padding(top = 40.dp, bottom = 200.dp), - contentAlignment = Alignment.Center, + contentAlignment = Alignment.Center ) { Text( text = "아직 등록된 조합이 없어요 ㅠㅠ", style = HackathonTheme.typography.Body_medium, color = Gray700, - textAlign = TextAlign.Center, + textAlign = TextAlign.Center ) } } diff --git a/feature/home/src/main/java/com/example/hackathon/feature/home/HomeTopBar.kt b/feature/home/src/main/java/com/example/hackathon/feature/home/HomeTopBar.kt index 7a593be..86001aa 100644 --- a/feature/home/src/main/java/com/example/hackathon/feature/home/HomeTopBar.kt +++ b/feature/home/src/main/java/com/example/hackathon/feature/home/HomeTopBar.kt @@ -12,47 +12,40 @@ import androidx.compose.ui.draw.shadow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.navigation.NavController -import com.example.hackathon.feature.home.component.SearchComponent import com.example.hackathon.core.designsystem.theme.HackathonTheme +import com.example.hackathon.feature.home.component.SearchComponent @Composable -fun HomeTopBar( - navController: NavController, - modifier: Modifier = Modifier, -) { +fun HomeTopBar(navController: NavController, modifier: Modifier = Modifier) { HomeTopBar( onSearchClick = { navController.navigate("search") }, - modifier = modifier, + modifier = modifier ) } @Composable -fun HomeTopBar( - onSearchClick: () -> Unit, - modifier: Modifier = Modifier, -) { +fun HomeTopBar(onSearchClick: () -> Unit, modifier: Modifier = Modifier) { Box( modifier = modifier .fillMaxWidth() .shadow( elevation = 6.dp, - clip = false, - ) - .background(HackathonTheme.colors.white) - .padding(top = 30.dp, bottom = 20.dp), + clip = false + ).background(HackathonTheme.colors.white) + .padding(top = 30.dp, bottom = 20.dp) ) { Row( modifier = Modifier .fillMaxWidth() .padding(start = 10.dp, end = 10.dp, top = 25.dp), - horizontalArrangement = Arrangement.Center, + horizontalArrangement = Arrangement.Center ) { SearchComponent( - onSearchClick = onSearchClick, + onSearchClick = onSearchClick ) } } @@ -63,7 +56,7 @@ fun HomeTopBar( private fun HomeTopBarPreview() { HackathonTheme { HomeTopBar( - onSearchClick = {}, + onSearchClick = {} ) } } diff --git a/feature/home/src/main/java/com/example/hackathon/feature/home/HomeViewModel.kt b/feature/home/src/main/java/com/example/hackathon/feature/home/HomeViewModel.kt index a74723c..a6bf2e8 100644 --- a/feature/home/src/main/java/com/example/hackathon/feature/home/HomeViewModel.kt +++ b/feature/home/src/main/java/com/example/hackathon/feature/home/HomeViewModel.kt @@ -2,184 +2,198 @@ package com.example.hackathon.feature.home import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.example.hackathon.core.domain.repository.CombinationRepository import com.example.hackathon.core.model.Category import com.example.hackathon.core.model.Combination -import com.example.hackathon.core.domain.repository.CombinationRepository import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import javax.inject.Inject // TODO: 디자인 확인 후 UI 조정 필요 @HiltViewModel class HomeViewModel - @Inject - constructor( - private val repository: CombinationRepository, - ) : ViewModel() { - private val _uiState = MutableStateFlow(HomeUiState()) - val uiState: StateFlow = _uiState.asStateFlow() - - private val _searchQuery = MutableStateFlow("") - val searchQuery: StateFlow = _searchQuery.asStateFlow() - - private val _selectedCategory = MutableStateFlow(Category.ALL) - val selectedCategory: StateFlow = _selectedCategory.asStateFlow() - - private var currentPage = 1 - private val pageSize = 10 - private var hasMore = true - private var isLoadingMore = false - - init { - loadCombinations(reset = true) - } +@Inject +constructor(private val repository: CombinationRepository) : + ViewModel() { + private val _uiState = MutableStateFlow(HomeUiState()) + val uiState: StateFlow = _uiState.asStateFlow() - fun loadCombinations(reset: Boolean = false) { - viewModelScope.launch { - if (reset) { - currentPage = 1 - hasMore = true - _uiState.value = _uiState.value.copy(isLoading = true, error = null, combinations = emptyList()) - } else { - if (isLoadingMore || !hasMore) return@launch - isLoadingMore = true - _uiState.value = _uiState.value.copy(isLoadingMore = true) - } + private val _searchQuery = MutableStateFlow("") + val searchQuery: StateFlow = _searchQuery.asStateFlow() - val category = if (_selectedCategory.value == Category.ALL) null else _selectedCategory.value - repository.getCombinations(category, currentPage, pageSize).fold( - onSuccess = { newCombinations -> - val filtered = - if (_searchQuery.value.isBlank()) { - newCombinations - } else { - newCombinations.filter { - it.title.contains(_searchQuery.value, ignoreCase = true) || - it.description.contains(_searchQuery.value, ignoreCase = true) - } + private val _selectedCategory = MutableStateFlow(Category.ALL) + val selectedCategory: StateFlow = _selectedCategory.asStateFlow() + + private var currentPage = 1 + private val pageSize = 10 + private var hasMore = true + private var isLoadingMore = false + + init { + loadCombinations(reset = true) + } + + fun loadCombinations(reset: Boolean = false) { + viewModelScope.launch { + if (reset) { + currentPage = 1 + hasMore = true + _uiState.value = + _uiState.value.copy( + isLoading = true, + error = null, + combinations = emptyList() + ) + } else { + if (isLoadingMore || !hasMore) return@launch + isLoadingMore = true + _uiState.value = _uiState.value.copy(isLoadingMore = true) + } + + val category = if (_selectedCategory.value == + Category.ALL + ) { + null + } else { + _selectedCategory.value + } + repository.getCombinations(category, currentPage, pageSize).fold( + onSuccess = { newCombinations -> + val filtered = + if (_searchQuery.value.isBlank()) { + newCombinations + } else { + newCombinations.filter { + it.title.contains(_searchQuery.value, ignoreCase = true) || + it.description.contains( + _searchQuery.value, + ignoreCase = true + ) } - - hasMore = filtered.size == pageSize - - val updatedCombinations = if (reset) { + } + + hasMore = filtered.size == pageSize + + val updatedCombinations = + if (reset) { filtered } else { _uiState.value.combinations + filtered } - - _uiState.value = - _uiState.value.copy( - combinations = updatedCombinations, - isLoading = false, - isLoadingMore = false, - hasMore = hasMore, - ) - - if (!reset && hasMore) { - currentPage++ - } - isLoadingMore = false - }, - onFailure = { error -> - _uiState.value = - _uiState.value.copy( - isLoading = false, - isLoadingMore = false, - error = error.message ?: "오류가 발생했습니다", - ) - isLoadingMore = false - }, - ) - } - } - fun loadMore() { - if (!isLoadingMore && hasMore) { - loadCombinations(reset = false) - } - } + _uiState.value = + _uiState.value.copy( + combinations = updatedCombinations, + isLoading = false, + isLoadingMore = false, + hasMore = hasMore + ) - fun updateSearchQuery(query: String) { - _searchQuery.value = query - loadCombinations(reset = true) + if (!reset && hasMore) { + currentPage++ + } + isLoadingMore = false + }, + onFailure = { error -> + _uiState.value = + _uiState.value.copy( + isLoading = false, + isLoadingMore = false, + error = error.message ?: "오류가 발생했습니다" + ) + isLoadingMore = false + } + ) } + } - fun selectCategory(category: Category) { - _selectedCategory.value = category - loadCombinations(reset = true) + fun loadMore() { + if (!isLoadingMore && hasMore) { + loadCombinations(reset = false) } + } - fun refresh() { - loadCombinations(reset = true) - } + fun updateSearchQuery(query: String) { + _searchQuery.value = query + loadCombinations(reset = true) + } - fun toggleLike(combinationId: String) { - viewModelScope.launch { - repository.likeCombination(combinationId).fold( - onSuccess = { - // 홈 화면 조합 목록 업데이트 - val currentCombinations = _uiState.value.combinations - val updatedCombinations = - currentCombinations.map { combination -> - if (combination.id == combinationId) { - val isLiked = combination.isLiked - val newLikeCount = - if (isLiked) { - combination.likeCount - 1 - } else { - combination.likeCount + 1 - } - combination.copy( - isLiked = !isLiked, - likeCount = newLikeCount, - ) - } else { - combination - } + fun selectCategory(category: Category) { + _selectedCategory.value = category + loadCombinations(reset = true) + } + + fun refresh() { + loadCombinations(reset = true) + } + + fun toggleLike(combinationId: String) { + viewModelScope.launch { + repository.likeCombination(combinationId).fold( + onSuccess = { + // 홈 화면 조합 목록 업데이트 + val currentCombinations = _uiState.value.combinations + val updatedCombinations = + currentCombinations.map { combination -> + if (combination.id == combinationId) { + val isLiked = combination.isLiked + val newLikeCount = + if (isLiked) { + combination.likeCount - 1 + } else { + combination.likeCount + 1 + } + combination.copy( + isLiked = !isLiked, + likeCount = newLikeCount + ) + } else { + combination } - _uiState.value = - _uiState.value.copy( - combinations = updatedCombinations, - ) - }, - onFailure = { - // 에러 발생 시에도 UI는 업데이트 (낙관적 업데이트) - val currentCombinations = _uiState.value.combinations - val updatedCombinations = - currentCombinations.map { combination -> - if (combination.id == combinationId) { - val isLiked = combination.isLiked - val newLikeCount = - if (isLiked) { - combination.likeCount - 1 - } else { - combination.likeCount + 1 - } - combination.copy( - isLiked = !isLiked, - likeCount = newLikeCount, - ) - } else { - combination - } + } + _uiState.value = + _uiState.value.copy( + combinations = updatedCombinations + ) + }, + onFailure = { + // 에러 발생 시에도 UI는 업데이트 (낙관적 업데이트) + val currentCombinations = _uiState.value.combinations + val updatedCombinations = + currentCombinations.map { combination -> + if (combination.id == combinationId) { + val isLiked = combination.isLiked + val newLikeCount = + if (isLiked) { + combination.likeCount - 1 + } else { + combination.likeCount + 1 + } + combination.copy( + isLiked = !isLiked, + likeCount = newLikeCount + ) + } else { + combination } - _uiState.value = - _uiState.value.copy( - combinations = updatedCombinations, - ) - }, - ) - } + } + _uiState.value = + _uiState.value.copy( + combinations = updatedCombinations + ) + } + ) } } +} data class HomeUiState( val combinations: List = emptyList(), val isLoading: Boolean = false, val isLoadingMore: Boolean = false, val hasMore: Boolean = true, - val error: String? = null, + val error: String? = null ) diff --git a/feature/home/src/main/java/com/example/hackathon/feature/home/SearchScreen.kt b/feature/home/src/main/java/com/example/hackathon/feature/home/SearchScreen.kt index 6cee362..1ffea04 100644 --- a/feature/home/src/main/java/com/example/hackathon/feature/home/SearchScreen.kt +++ b/feature/home/src/main/java/com/example/hackathon/feature/home/SearchScreen.kt @@ -38,17 +38,17 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.example.hackathon.core.designsystem.R -import com.example.hackathon.feature.home.component.CombinationList -import com.example.hackathon.feature.home.SearchViewModel import com.example.hackathon.core.designsystem.theme.Gray700 import com.example.hackathon.core.designsystem.theme.HackathonTheme +import com.example.hackathon.feature.home.SearchViewModel +import com.example.hackathon.feature.home.component.CombinationList @Composable fun SearchScreen( modifier: Modifier = Modifier, viewModel: SearchViewModel = hiltViewModel(), onNavigateBack: () -> Unit = {}, - onCombinationClick: (String) -> Unit = {}, + onCombinationClick: (String) -> Unit = {} ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val keyboardController = LocalSoftwareKeyboardController.current @@ -61,10 +61,9 @@ fun SearchScreen( .fillMaxWidth() .shadow( elevation = 6.dp, - clip = false, - ) - .background(HackathonTheme.colors.white) - .padding(top = 52.dp, bottom = 20.dp), + clip = false + ).background(HackathonTheme.colors.white) + .padding(top = 52.dp, bottom = 20.dp) ) { Row( modifier = @@ -72,7 +71,7 @@ fun SearchScreen( .fillMaxWidth() .padding(horizontal = 20.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(10.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp) ) { Image( painter = painterResource(R.drawable.ic_logo_rec), @@ -81,7 +80,7 @@ fun SearchScreen( Modifier .size(50.dp) .clickable { onNavigateBack() }, - contentScale = ContentScale.Fit, + contentScale = ContentScale.Fit ) TextField( @@ -95,7 +94,7 @@ fun SearchScreen( .border( width = 1.5.dp, color = HackathonTheme.colors.primary, - shape = RoundedCornerShape(30.dp), + shape = RoundedCornerShape(30.dp) ), trailingIcon = { Icon( @@ -106,21 +105,21 @@ fun SearchScreen( Modifier.clickable { viewModel.onSearch(uiState.query) keyboardController?.hide() - }, + } ) }, singleLine = true, textStyle = HackathonTheme.typography.Body_medium, keyboardOptions = KeyboardOptions( - imeAction = ImeAction.Search, + imeAction = ImeAction.Search ), keyboardActions = KeyboardActions( onSearch = { viewModel.onSearch(uiState.query) keyboardController?.hide() - }, + } ), colors = TextFieldDefaults.colors( @@ -129,12 +128,12 @@ fun SearchScreen( disabledContainerColor = HackathonTheme.colors.white, focusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent, - cursorColor = HackathonTheme.colors.black, - ), + cursorColor = HackathonTheme.colors.black + ) ) } } - }, + } ) { innerPadding -> when { !uiState.hasSearched -> { @@ -144,12 +143,12 @@ fun SearchScreen( Modifier .fillMaxSize() .padding(innerPadding), - contentAlignment = Alignment.Center, + contentAlignment = Alignment.Center ) { Text( text = "쩝쩝박사님들의 레시피를 검색해보세요", style = HackathonTheme.typography.Body_medium, - color = HackathonTheme.colors.gray700, + color = HackathonTheme.colors.gray700 ) } } @@ -161,7 +160,7 @@ fun SearchScreen( Modifier .fillMaxSize() .padding(innerPadding), - contentAlignment = Alignment.Center, + contentAlignment = Alignment.Center ) { CircularProgressIndicator() } @@ -174,13 +173,13 @@ fun SearchScreen( Modifier .fillMaxSize() .padding(innerPadding), - contentAlignment = Alignment.Center, + contentAlignment = Alignment.Center ) { Text( text = uiState.error ?: "검색 중 오류가 발생했습니다", style = HackathonTheme.typography.Body_medium, color = Gray700, - textAlign = TextAlign.Center, + textAlign = TextAlign.Center ) } } @@ -193,7 +192,7 @@ fun SearchScreen( .fillMaxSize() .padding(innerPadding), results = uiState.results, - onCombinationClick = onCombinationClick, + onCombinationClick = onCombinationClick ) } } diff --git a/feature/home/src/main/java/com/example/hackathon/feature/home/SearchViewModel.kt b/feature/home/src/main/java/com/example/hackathon/feature/home/SearchViewModel.kt index 37323cf..a01df98 100644 --- a/feature/home/src/main/java/com/example/hackathon/feature/home/SearchViewModel.kt +++ b/feature/home/src/main/java/com/example/hackathon/feature/home/SearchViewModel.kt @@ -2,126 +2,125 @@ package com.example.hackathon.feature.home import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.example.hackathon.core.model.Combination import com.example.hackathon.core.domain.repository.CombinationRepository +import com.example.hackathon.core.model.Combination import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import javax.inject.Inject data class SearchUiState( val query: String = "", val hasSearched: Boolean = false, val results: List = emptyList(), val isLoading: Boolean = false, - val error: String? = null, + val error: String? = null ) @HiltViewModel class SearchViewModel - @Inject - constructor( - private val repository: CombinationRepository, - ) : ViewModel() { - private val _uiState = MutableStateFlow(SearchUiState()) - val uiState: StateFlow = _uiState.asStateFlow() +@Inject +constructor(private val repository: CombinationRepository) : + ViewModel() { + private val _uiState = MutableStateFlow(SearchUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + fun onQueryChange(query: String) { + _uiState.value = + _uiState.value.copy( + query = query + ) + } + + fun onSearch(query: String) { + if (query.isBlank()) return - fun onQueryChange(query: String) { + viewModelScope.launch { _uiState.value = _uiState.value.copy( - query = query, + hasSearched = true, + isLoading = true, + error = null, + results = emptyList() ) - } - - fun onSearch(query: String) { - if (query.isBlank()) return - - viewModelScope.launch { - _uiState.value = - _uiState.value.copy( - hasSearched = true, - isLoading = true, - error = null, - results = emptyList(), - ) - repository.searchCombinations(query).fold( - onSuccess = { results -> - _uiState.value = - _uiState.value.copy( - results = results, - isLoading = false, - ) - }, - onFailure = { error -> - _uiState.value = - _uiState.value.copy( - isLoading = false, - error = error.message ?: "검색 중 오류가 발생했습니다", - ) - }, - ) - } + repository.searchCombinations(query).fold( + onSuccess = { results -> + _uiState.value = + _uiState.value.copy( + results = results, + isLoading = false + ) + }, + onFailure = { error -> + _uiState.value = + _uiState.value.copy( + isLoading = false, + error = error.message ?: "검색 중 오류가 발생했습니다" + ) + } + ) } + } - fun toggleLike(combinationId: String) { - viewModelScope.launch { - repository.likeCombination(combinationId).fold( - onSuccess = { - // 검색 결과 업데이트 - val currentResults = _uiState.value.results - val updatedResults = - currentResults.map { combination -> - if (combination.id == combinationId) { - val isLiked = combination.isLiked - val newLikeCount = - if (isLiked) { - combination.likeCount - 1 - } else { - combination.likeCount + 1 - } - combination.copy( - isLiked = !isLiked, - likeCount = newLikeCount, - ) - } else { - combination - } + fun toggleLike(combinationId: String) { + viewModelScope.launch { + repository.likeCombination(combinationId).fold( + onSuccess = { + // 검색 결과 업데이트 + val currentResults = _uiState.value.results + val updatedResults = + currentResults.map { combination -> + if (combination.id == combinationId) { + val isLiked = combination.isLiked + val newLikeCount = + if (isLiked) { + combination.likeCount - 1 + } else { + combination.likeCount + 1 + } + combination.copy( + isLiked = !isLiked, + likeCount = newLikeCount + ) + } else { + combination } - _uiState.value = - _uiState.value.copy( - results = updatedResults, - ) - }, - onFailure = { - // 에러 발생 시에도 UI는 업데이트 (낙관적 업데이트) - val currentResults = _uiState.value.results - val updatedResults = - currentResults.map { combination -> - if (combination.id == combinationId) { - val isLiked = combination.isLiked - val newLikeCount = - if (isLiked) { - combination.likeCount - 1 - } else { - combination.likeCount + 1 - } - combination.copy( - isLiked = !isLiked, - likeCount = newLikeCount, - ) - } else { - combination - } + } + _uiState.value = + _uiState.value.copy( + results = updatedResults + ) + }, + onFailure = { + // 에러 발생 시에도 UI는 업데이트 (낙관적 업데이트) + val currentResults = _uiState.value.results + val updatedResults = + currentResults.map { combination -> + if (combination.id == combinationId) { + val isLiked = combination.isLiked + val newLikeCount = + if (isLiked) { + combination.likeCount - 1 + } else { + combination.likeCount + 1 + } + combination.copy( + isLiked = !isLiked, + likeCount = newLikeCount + ) + } else { + combination } - _uiState.value = - _uiState.value.copy( - results = updatedResults, - ) - }, - ) - } + } + _uiState.value = + _uiState.value.copy( + results = updatedResults + ) + } + ) } } +} diff --git a/feature/home/src/main/java/com/example/hackathon/feature/home/component/CombinationList.kt b/feature/home/src/main/java/com/example/hackathon/feature/home/component/CombinationList.kt index 3106f55..44abbd0 100644 --- a/feature/home/src/main/java/com/example/hackathon/feature/home/component/CombinationList.kt +++ b/feature/home/src/main/java/com/example/hackathon/feature/home/component/CombinationList.kt @@ -15,15 +15,15 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.example.hackathon.core.designsystem.component.CombinationCard -import com.example.hackathon.core.model.Combination import com.example.hackathon.core.designsystem.theme.Gray700 import com.example.hackathon.core.designsystem.theme.HackathonTheme +import com.example.hackathon.core.model.Combination @Composable fun CombinationList( modifier: Modifier = Modifier, results: List, - onCombinationClick: (String) -> Unit = {}, + onCombinationClick: (String) -> Unit = {} ) { if (results.isEmpty()) { Box( @@ -31,26 +31,26 @@ fun CombinationList( modifier .fillMaxSize() .padding(top = 40.dp), - contentAlignment = Alignment.Center, + contentAlignment = Alignment.Center ) { Text( text = "검색 결과가 없어요 ㅠㅠ", style = HackathonTheme.typography.Body_medium, color = Gray700, - textAlign = TextAlign.Center, + textAlign = TextAlign.Center ) } } else { LazyColumn( modifier = modifier.fillMaxSize(), contentPadding = PaddingValues(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) ) { items(results) { combination -> CombinationCard( combination = combination, onClick = { onCombinationClick(combination.id) }, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth() ) } } diff --git a/feature/home/src/main/java/com/example/hackathon/feature/home/component/FilterBar.kt b/feature/home/src/main/java/com/example/hackathon/feature/home/component/FilterBar.kt index 69b9fab..40d634a 100644 --- a/feature/home/src/main/java/com/example/hackathon/feature/home/component/FilterBar.kt +++ b/feature/home/src/main/java/com/example/hackathon/feature/home/component/FilterBar.kt @@ -17,15 +17,15 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import com.example.hackathon.core.designsystem.R import com.example.hackathon.core.designsystem.component.CategoryChip -import com.example.hackathon.core.model.Category import com.example.hackathon.core.designsystem.theme.HackathonTheme +import com.example.hackathon.core.model.Category @Composable fun FilterBar( categories: List, selectedCategory: Category, onCategorySelected: (Category) -> Unit, - modifier: Modifier = Modifier, + modifier: Modifier = Modifier ) { Column { Row( @@ -33,13 +33,13 @@ fun FilterBar( Modifier .fillMaxWidth() .padding(start = 20.dp), - verticalAlignment = Alignment.CenterVertically, + verticalAlignment = Alignment.CenterVertically ) { Icon( painter = painterResource(R.drawable.ic_logo), contentDescription = "hot", modifier = Modifier.width(90.dp), - tint = HackathonTheme.colors.primary, + tint = HackathonTheme.colors.primary ) } @@ -49,7 +49,7 @@ fun FilterBar( .fillMaxWidth() .background(HackathonTheme.colors.white) .padding(horizontal = 20.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp) ) { items(categories) { category -> CategoryChip( @@ -57,7 +57,7 @@ fun FilterBar( isSelected = category == selectedCategory, onClick = { onCategorySelected(category) - }, + } ) } } diff --git a/feature/home/src/main/java/com/example/hackathon/feature/home/component/SearchComponent.kt b/feature/home/src/main/java/com/example/hackathon/feature/home/component/SearchComponent.kt index cf639b7..b41ac1d 100644 --- a/feature/home/src/main/java/com/example/hackathon/feature/home/component/SearchComponent.kt +++ b/feature/home/src/main/java/com/example/hackathon/feature/home/component/SearchComponent.kt @@ -19,27 +19,24 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import com.example.hackathon.core.designsystem.R -import com.example.hackathon.core.designsystem.util.noRippleClickable import com.example.hackathon.core.designsystem.theme.HackathonTheme +import com.example.hackathon.core.designsystem.util.noRippleClickable @Composable -fun SearchComponent( - modifier: Modifier = Modifier, - onSearchClick: () -> Unit, -) { +fun SearchComponent(modifier: Modifier = Modifier, onSearchClick: () -> Unit) { Row( modifier = modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy( space = 10.dp, - alignment = Alignment.CenterHorizontally, - ), + alignment = Alignment.CenterHorizontally + ) ) { Image( painter = painterResource(R.drawable.ic_logo_rec), contentDescription = "logo", modifier = Modifier.size(width = 50.dp, height = 50.dp), - contentScale = ContentScale.Fit, + contentScale = ContentScale.Fit ) Row( modifier = @@ -49,24 +46,22 @@ fun SearchComponent( .border( color = HackathonTheme.colors.primary, width = 1.5.dp, - shape = RoundedCornerShape(30.dp), - ) - .background( + shape = RoundedCornerShape(30.dp) + ).background( HackathonTheme.colors.white, - shape = RoundedCornerShape(30.dp), - ) - .padding(start = 14.dp), - verticalAlignment = Alignment.CenterVertically, + shape = RoundedCornerShape(30.dp) + ).padding(start = 14.dp), + verticalAlignment = Alignment.CenterVertically ) { Icon( painter = painterResource(R.drawable.ic_search), contentDescription = "search component", - tint = Color(0xFF8B91A1), + tint = Color(0xFF8B91A1) ) Text( text = "오늘은 어떤 꿀조합을 찾고 싶으신가요?", style = HackathonTheme.typography.Caption_medium, - color = Color(0xFF8B91A1), + color = Color(0xFF8B91A1) ) } } diff --git a/feature/onboarding/src/main/java/com/example/hackathon/feature/onboarding/OnboardingScreen.kt b/feature/onboarding/src/main/java/com/example/hackathon/feature/onboarding/OnboardingScreen.kt index cfa82f3..1cbcbde 100644 --- a/feature/onboarding/src/main/java/com/example/hackathon/feature/onboarding/OnboardingScreen.kt +++ b/feature/onboarding/src/main/java/com/example/hackathon/feature/onboarding/OnboardingScreen.kt @@ -30,10 +30,7 @@ import kotlinx.coroutines.delay @OptIn(ExperimentalMaterial3Api::class) @Composable -fun OnboardingScreen( - modifier: Modifier = Modifier, - onNavigateToLogin: () -> Unit = {}, -) { +fun OnboardingScreen(modifier: Modifier = Modifier, onNavigateToLogin: () -> Unit = {}) { // 3초 후 자동으로 로그인 화면으로 이동 LaunchedEffect(Unit) { delay(3000) @@ -41,7 +38,7 @@ fun OnboardingScreen( } Scaffold( - modifier = modifier, + modifier = modifier ) { paddingValues -> Box( modifier = @@ -49,19 +46,19 @@ fun OnboardingScreen( .fillMaxSize() .padding(paddingValues) .background(Color.White), - contentAlignment = Alignment.Center, + contentAlignment = Alignment.Center ) { Column( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, + verticalArrangement = Arrangement.Center ) { // 로고 영역 (앱 아이콘) Image( painter = painterResource(R.drawable.ic_logo_rec), contentDescription = "쩝쩝박사 로고", modifier = Modifier.size(120.dp), - contentScale = ContentScale.Fit, + contentScale = ContentScale.Fit ) Spacer(modifier = Modifier.height(40.dp)) @@ -69,19 +66,19 @@ fun OnboardingScreen( // 슬로건 텍스트 (2줄, 중앙 정렬) Column( horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(4.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) ) { Text( text = "어디선가 들어본 바로 그 조합", style = HackathonTheme.typography.Head2_bold, color = Color.Black, - textAlign = TextAlign.Center, + textAlign = TextAlign.Center ) Text( text = "모두 쩝쩝박사에서", style = HackathonTheme.typography.Head2_bold, color = Color.Black, - textAlign = TextAlign.Center, + textAlign = TextAlign.Center ) } } diff --git a/feature/profile/src/main/java/com/example/hackathon/feature/profile/EditProfileScreen.kt b/feature/profile/src/main/java/com/example/hackathon/feature/profile/EditProfileScreen.kt index 2b12bf2..7ed7eb2 100644 --- a/feature/profile/src/main/java/com/example/hackathon/feature/profile/EditProfileScreen.kt +++ b/feature/profile/src/main/java/com/example/hackathon/feature/profile/EditProfileScreen.kt @@ -41,10 +41,10 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage -import com.example.hackathon.feature.profile.EditProfileViewModel import com.example.hackathon.core.designsystem.theme.Gray700 import com.example.hackathon.core.designsystem.theme.HackathonTheme import com.example.hackathon.core.designsystem.theme.Primary +import com.example.hackathon.feature.profile.EditProfileViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -52,7 +52,7 @@ fun EditProfileScreen( modifier: Modifier = Modifier, viewModel: EditProfileViewModel = hiltViewModel(), onNavigateBack: () -> Unit = {}, - onProfileUpdated: () -> Unit = {}, + onProfileUpdated: () -> Unit = {} ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -72,7 +72,7 @@ fun EditProfileScreen( Text( text = "프로필 수정", style = HackathonTheme.typography.Head2_bold, - color = Color.White, + color = Color.White ) }, navigationIcon = { @@ -80,16 +80,16 @@ fun EditProfileScreen( Icon( imageVector = Icons.Default.ArrowBack, contentDescription = "뒤로가기", - tint = Color.White, + tint = Color.White ) } }, colors = TopAppBarDefaults.topAppBarColors( - containerColor = Primary, - ), + containerColor = Primary + ) ) - }, + } ) { paddingValues -> when { uiState.isLoading -> { @@ -98,11 +98,12 @@ fun EditProfileScreen( Modifier .fillMaxSize() .padding(paddingValues), - contentAlignment = Alignment.Center, + contentAlignment = Alignment.Center ) { CircularProgressIndicator() } } + uiState.error != null && uiState.nickname.isEmpty() -> { // 프로필 로드 실패 Column( @@ -112,13 +113,13 @@ fun EditProfileScreen( .padding(paddingValues) .padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, + verticalArrangement = Arrangement.Center ) { Text( text = uiState.error ?: "프로필을 불러올 수 없습니다", style = HackathonTheme.typography.Body_medium, color = androidx.compose.material3.MaterialTheme.colorScheme.error, - textAlign = TextAlign.Center, + textAlign = TextAlign.Center ) Spacer(modifier = Modifier.height(16.dp)) Button( @@ -126,16 +127,17 @@ fun EditProfileScreen( colors = ButtonDefaults.buttonColors( containerColor = Primary, - contentColor = Color.White, - ), + contentColor = Color.White + ) ) { Text( text = "다시 시도", - style = HackathonTheme.typography.Body_medium, + style = HackathonTheme.typography.Body_medium ) } } } + else -> { // 정상 상태 - 프로필 수정 폼 Column( @@ -146,7 +148,7 @@ fun EditProfileScreen( .verticalScroll(rememberScrollState()) .padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(24.dp), + verticalArrangement = Arrangement.spacedBy(24.dp) ) { // 프로필 이미지 영역 Box( @@ -159,22 +161,26 @@ fun EditProfileScreen( Color.Transparent } else { Primary.copy(alpha = 0.2f) - }, + } ), - contentAlignment = Alignment.Center, + contentAlignment = Alignment.Center ) { if (uiState.profileImageUrl != null) { AsyncImage( model = uiState.profileImageUrl, contentDescription = "프로필 이미지", - modifier = Modifier.fillMaxSize(), + modifier = Modifier.fillMaxSize() ) } else { - val displayText = uiState.nickname.trim().firstOrNull()?.toString() ?: "?" + val displayText = + uiState.nickname + .trim() + .firstOrNull() + ?.toString() ?: "?" Text( text = displayText, style = HackathonTheme.typography.Head2_bold, - color = Color.White, + color = Color.White ) } } @@ -182,12 +188,12 @@ fun EditProfileScreen( // 닉네임 입력 필드 Column( modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) ) { Text( text = "닉네임", style = HackathonTheme.typography.Body_medium, - color = Color.Black, + color = Color.Black ) OutlinedTextField( value = uiState.nickname, @@ -197,7 +203,7 @@ fun EditProfileScreen( Text( text = "닉네임을 입력하세요", style = HackathonTheme.typography.Body_medium, - color = Gray700, + color = Gray700 ) }, modifier = Modifier.fillMaxWidth(), @@ -206,8 +212,8 @@ fun EditProfileScreen( colors = OutlinedTextFieldDefaults.colors( focusedBorderColor = Primary, - unfocusedBorderColor = Gray700, - ), + unfocusedBorderColor = Gray700 + ) ) } @@ -221,25 +227,25 @@ fun EditProfileScreen( containerColor = Primary, contentColor = Color.White, disabledContainerColor = Gray700.copy(alpha = 0.3f), - disabledContentColor = Color.White.copy(alpha = 0.6f), + disabledContentColor = Color.White.copy(alpha = 0.6f) ), - shape = RoundedCornerShape(15.dp), + shape = RoundedCornerShape(15.dp) ) { if (uiState.isSaving) { CircularProgressIndicator( modifier = Modifier.size(20.dp), color = Color.White, - strokeWidth = 2.dp, + strokeWidth = 2.dp ) Spacer(modifier = Modifier.size(8.dp)) Text( text = "저장 중...", - style = HackathonTheme.typography.Body_medium, + style = HackathonTheme.typography.Body_medium ) } else { Text( text = "저장", - style = HackathonTheme.typography.Body_medium, + style = HackathonTheme.typography.Body_medium ) } } @@ -251,7 +257,7 @@ fun EditProfileScreen( style = HackathonTheme.typography.Caption_medium, color = androidx.compose.material3.MaterialTheme.colorScheme.error, textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth() ) } } diff --git a/feature/profile/src/main/java/com/example/hackathon/feature/profile/EditProfileViewModel.kt b/feature/profile/src/main/java/com/example/hackathon/feature/profile/EditProfileViewModel.kt index fcdc37f..d9acb20 100644 --- a/feature/profile/src/main/java/com/example/hackathon/feature/profile/EditProfileViewModel.kt +++ b/feature/profile/src/main/java/com/example/hackathon/feature/profile/EditProfileViewModel.kt @@ -4,83 +4,83 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.example.hackathon.core.domain.repository.UserRepository import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import javax.inject.Inject @HiltViewModel class EditProfileViewModel - @Inject - constructor( - private val userRepository: UserRepository, - ) : ViewModel() { - private val _uiState = MutableStateFlow(EditProfileUiState()) - val uiState: StateFlow = _uiState.asStateFlow() +@Inject +constructor(private val userRepository: UserRepository) : + ViewModel() { + private val _uiState = MutableStateFlow(EditProfileUiState()) + val uiState: StateFlow = _uiState.asStateFlow() - init { - loadProfile() - } + init { + loadProfile() + } - fun loadProfile() { - viewModelScope.launch { - _uiState.value = _uiState.value.copy(isLoading = true, error = null) - userRepository.getProfile().fold( - onSuccess = { user -> - _uiState.value = - _uiState.value.copy( - nickname = user.nickname ?: "", - profileImageUrl = user.profileImageUrl, - isLoading = false, - ) - }, - onFailure = { error -> - _uiState.value = - _uiState.value.copy( - isLoading = false, - error = error.message ?: "프로필을 불러올 수 없습니다", - ) - }, - ) - } + fun loadProfile() { + viewModelScope.launch { + _uiState.value = _uiState.value.copy(isLoading = true, error = null) + userRepository.getProfile().fold( + onSuccess = { user -> + _uiState.value = + _uiState.value.copy( + nickname = user.nickname ?: "", + profileImageUrl = user.profileImageUrl, + isLoading = false + ) + }, + onFailure = { error -> + _uiState.value = + _uiState.value.copy( + isLoading = false, + error = error.message ?: "프로필을 불러올 수 없습니다" + ) + } + ) } + } - fun updateNickname(nickname: String) { - _uiState.value = _uiState.value.copy(nickname = nickname, error = null) - } + fun updateNickname(nickname: String) { + _uiState.value = _uiState.value.copy(nickname = nickname, error = null) + } - fun saveProfile() { - val nickname = _uiState.value.nickname.trim() - if (nickname.isBlank()) { - _uiState.value = _uiState.value.copy(error = "닉네임을 입력해주세요") - return - } + fun saveProfile() { + val nickname = _uiState.value.nickname.trim() + if (nickname.isBlank()) { + _uiState.value = _uiState.value.copy(error = "닉네임을 입력해주세요") + return + } - viewModelScope.launch { - _uiState.value = _uiState.value.copy(isSaving = true, error = null) - userRepository.updateProfile( + viewModelScope.launch { + _uiState.value = _uiState.value.copy(isSaving = true, error = null) + userRepository + .updateProfile( nickname = nickname, - profileImageUrl = _uiState.value.profileImageUrl, + profileImageUrl = _uiState.value.profileImageUrl ).fold( onSuccess = { user -> _uiState.value = _uiState.value.copy( isSaving = false, - isSuccess = true, + isSuccess = true ) }, onFailure = { error -> _uiState.value = _uiState.value.copy( isSaving = false, - error = error.message ?: "프로필 저장에 실패했습니다", + error = error.message ?: "프로필 저장에 실패했습니다" ) - }, + } ) - } } } +} data class EditProfileUiState( val nickname: String = "", @@ -88,5 +88,5 @@ data class EditProfileUiState( val isLoading: Boolean = false, val isSaving: Boolean = false, val isSuccess: Boolean = false, - val error: String? = null, + val error: String? = null ) diff --git a/feature/profile/src/main/java/com/example/hackathon/feature/profile/MyPageViewModel.kt b/feature/profile/src/main/java/com/example/hackathon/feature/profile/MyPageViewModel.kt index aeb4ce8..d0af90a 100644 --- a/feature/profile/src/main/java/com/example/hackathon/feature/profile/MyPageViewModel.kt +++ b/feature/profile/src/main/java/com/example/hackathon/feature/profile/MyPageViewModel.kt @@ -2,172 +2,181 @@ package com.example.hackathon.feature.profile import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.example.hackathon.core.model.Combination -import com.example.hackathon.core.model.User import com.example.hackathon.core.domain.repository.AuthRepository import com.example.hackathon.core.domain.repository.CombinationRepository import com.example.hackathon.core.domain.repository.UserRepository +import com.example.hackathon.core.model.Combination +import com.example.hackathon.core.model.User import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import javax.inject.Inject @HiltViewModel class MyPageViewModel - @Inject - constructor( - private val userRepository: UserRepository, - private val combinationRepository: CombinationRepository, - private val authRepository: AuthRepository, - ) : ViewModel() { - private val _uiState = MutableStateFlow(MyPageUiState()) - val uiState: StateFlow = _uiState.asStateFlow() +@Inject +constructor( + private val userRepository: UserRepository, + private val combinationRepository: CombinationRepository, + private val authRepository: AuthRepository +) : ViewModel() { + private val _uiState = MutableStateFlow(MyPageUiState()) + val uiState: StateFlow = _uiState.asStateFlow() - init { - // 로그인 상태 확인 후 프로필 로드 - viewModelScope.launch { - val hasTokens = authRepository.hasValidTokens() - if (hasTokens) { - loadProfile() - loadMyRecipes() - } else { - // 로그인 안 됨: isLoading을 false로 설정하여 UI가 로그인 화면으로 리다이렉트할 수 있도록 - _uiState.value = _uiState.value.copy( + init { + // 로그인 상태 확인 후 프로필 로드 + viewModelScope.launch { + val hasTokens = authRepository.hasValidTokens() + if (hasTokens) { + loadProfile() + loadMyRecipes() + } else { + // 로그인 안 됨: isLoading을 false로 설정하여 UI가 로그인 화면으로 리다이렉트할 수 있도록 + _uiState.value = + _uiState.value.copy( isLoading = false, - user = null, + user = null ) - } } } + } - fun loadProfile() { - viewModelScope.launch { - _uiState.value = _uiState.value.copy(isLoading = true, error = null) - userRepository.getProfile().fold( - onSuccess = { user -> - _uiState.value = - _uiState.value.copy( - user = user, - isLoading = false, - error = null, - ) - }, - onFailure = { error -> - // 프로필 로드 실패: 에러만 표시하고 user는 유지 (이전 사용자 정보가 있으면 계속 표시) - // 단, 인증 에러(401, 403)인 경우에만 로그아웃 처리 - val errorMessage = error.message ?: "프로필을 불러올 수 없습니다" - val isAuthError = errorMessage.contains("401", ignoreCase = true) || + fun loadProfile() { + viewModelScope.launch { + _uiState.value = _uiState.value.copy(isLoading = true, error = null) + userRepository.getProfile().fold( + onSuccess = { user -> + _uiState.value = + _uiState.value.copy( + user = user, + isLoading = false, + error = null + ) + }, + onFailure = { error -> + // 프로필 로드 실패: 에러만 표시하고 user는 유지 (이전 사용자 정보가 있으면 계속 표시) + // 단, 인증 에러(401, 403)인 경우에만 로그아웃 처리 + val errorMessage = error.message ?: "프로필을 불러올 수 없습니다" + val isAuthError = + errorMessage.contains("401", ignoreCase = true) || errorMessage.contains("403", ignoreCase = true) || errorMessage.contains("Unauthorized", ignoreCase = true) || errorMessage.contains("Forbidden", ignoreCase = true) - - if (isAuthError) { - // 인증 에러: 로그아웃 처리 - _uiState.value = _uiState.value.copy( + + if (isAuthError) { + // 인증 에러: 로그아웃 처리 + _uiState.value = + _uiState.value.copy( user = null, isLoading = false, error = errorMessage, - isLoggedOut = true, + isLoggedOut = true ) - } else { - // 기타 에러: 에러만 표시하고 user는 유지 - _uiState.value = _uiState.value.copy( + } else { + // 기타 에러: 에러만 표시하고 user는 유지 + _uiState.value = + _uiState.value.copy( isLoading = false, - error = errorMessage, + error = errorMessage ) - } - }, - ) - } + } + } + ) } + } - fun loadMyRecipes() { - viewModelScope.launch { - _uiState.value = _uiState.value.copy(isLoadingMyRecipes = true) - combinationRepository.getMyCombinations().fold( - onSuccess = { combinations -> - _uiState.value = - _uiState.value.copy( - myRecipes = combinations, - isLoadingMyRecipes = false, - ) - }, - onFailure = { error -> - _uiState.value = - _uiState.value.copy( - isLoadingMyRecipes = false, - error = error.message, - ) - }, - ) - } + fun loadMyRecipes() { + viewModelScope.launch { + _uiState.value = _uiState.value.copy(isLoadingMyRecipes = true) + combinationRepository.getMyCombinations().fold( + onSuccess = { combinations -> + _uiState.value = + _uiState.value.copy( + myRecipes = combinations, + isLoadingMyRecipes = false + ) + }, + onFailure = { error -> + _uiState.value = + _uiState.value.copy( + isLoadingMyRecipes = false, + error = error.message + ) + } + ) } + } - fun loadLikedCombinations() { - viewModelScope.launch { - _uiState.value = _uiState.value.copy(isLoadingLikedCombinations = true) - combinationRepository.getLikedCombinations().fold( - onSuccess = { combinations -> - _uiState.value = - _uiState.value.copy( - likedCombinations = combinations, - isLoadingLikedCombinations = false, - ) - }, - onFailure = { error -> - _uiState.value = - _uiState.value.copy( - isLoadingLikedCombinations = false, - error = error.message, - ) - }, - ) - } + fun loadLikedCombinations() { + viewModelScope.launch { + _uiState.value = _uiState.value.copy(isLoadingLikedCombinations = true) + combinationRepository.getLikedCombinations().fold( + onSuccess = { combinations -> + _uiState.value = + _uiState.value.copy( + likedCombinations = combinations, + isLoadingLikedCombinations = false + ) + }, + onFailure = { error -> + _uiState.value = + _uiState.value.copy( + isLoadingLikedCombinations = false, + error = error.message + ) + } + ) } + } - fun selectTab(tab: MyPageTab) { - _uiState.value = _uiState.value.copy(selectedTab = tab) - when (tab) { - MyPageTab.MY_RECIPES -> { - if (_uiState.value.myRecipes.isEmpty() && !_uiState.value.isLoadingMyRecipes) { - loadMyRecipes() - } + fun selectTab(tab: MyPageTab) { + _uiState.value = _uiState.value.copy(selectedTab = tab) + when (tab) { + MyPageTab.MY_RECIPES -> { + if (_uiState.value.myRecipes.isEmpty() && !_uiState.value.isLoadingMyRecipes) { + loadMyRecipes() } - MyPageTab.LIKED_COMBINATIONS -> { - if (_uiState.value.likedCombinations.isEmpty() && !_uiState.value.isLoadingLikedCombinations) { - loadLikedCombinations() - } + } + + MyPageTab.LIKED_COMBINATIONS -> { + if (_uiState.value.likedCombinations.isEmpty() && + !_uiState.value.isLoadingLikedCombinations + ) { + loadLikedCombinations() } } } + } - fun logout() { - viewModelScope.launch { - authRepository.logout().fold( - onSuccess = { - // 로그아웃 성공: UI 상태 초기화 - _uiState.value = _uiState.value.copy( + fun logout() { + viewModelScope.launch { + authRepository.logout().fold( + onSuccess = { + // 로그아웃 성공: UI 상태 초기화 + _uiState.value = + _uiState.value.copy( isLoggedOut = true, - user = null, // 사용자 정보 초기화 + user = null // 사용자 정보 초기화 ) - }, - onFailure = { error -> - // 로그아웃 실패해도 로컬 토큰은 삭제하고 로그아웃 처리 - _uiState.value = _uiState.value.copy( + }, + onFailure = { error -> + // 로그아웃 실패해도 로컬 토큰은 삭제하고 로그아웃 처리 + _uiState.value = + _uiState.value.copy( isLoggedOut = true, - user = null, // 사용자 정보 초기화 + user = null // 사용자 정보 초기화 ) - }, - ) - } + } + ) } } +} enum class MyPageTab { MY_RECIPES, - LIKED_COMBINATIONS, + LIKED_COMBINATIONS } data class MyPageUiState( @@ -179,5 +188,5 @@ data class MyPageUiState( val isLoadingMyRecipes: Boolean = false, val isLoadingLikedCombinations: Boolean = false, val error: String? = null, - val isLoggedOut: Boolean = false, + val isLoggedOut: Boolean = false ) diff --git a/feature/profile/src/main/java/com/example/hackathon/feature/profile/MyScreen.kt b/feature/profile/src/main/java/com/example/hackathon/feature/profile/MyScreen.kt index 7210304..3980b8f 100644 --- a/feature/profile/src/main/java/com/example/hackathon/feature/profile/MyScreen.kt +++ b/feature/profile/src/main/java/com/example/hackathon/feature/profile/MyScreen.kt @@ -54,12 +54,12 @@ import coil3.compose.AsyncImage import com.example.hackathon.core.designsystem.R import com.example.hackathon.core.designsystem.component.CombinationCard import com.example.hackathon.core.designsystem.component.TopAppLogoBar -import com.example.hackathon.feature.profile.MyPageTab -import com.example.hackathon.feature.profile.MyPageViewModel import com.example.hackathon.core.designsystem.theme.Gray50 import com.example.hackathon.core.designsystem.theme.Gray700 import com.example.hackathon.core.designsystem.theme.HackathonTheme import com.example.hackathon.core.designsystem.theme.Primary +import com.example.hackathon.feature.profile.MyPageTab +import com.example.hackathon.feature.profile.MyPageViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -68,7 +68,7 @@ fun MyScreen( viewModel: MyPageViewModel = hiltViewModel(), onCombinationClick: (String) -> Unit = {}, onLogout: () -> Unit = {}, - onChangeNickname: () -> Unit = {}, + onChangeNickname: () -> Unit = {} ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -98,7 +98,7 @@ fun MyScreen( Scaffold( modifier = modifier, - contentWindowInsets = WindowInsets(0), + contentWindowInsets = WindowInsets(0) ) { paddingValues -> Column( @@ -106,7 +106,7 @@ fun MyScreen( Modifier .fillMaxSize() .padding(paddingValues) - .background(Color.White), + .background(Color.White) ) { Column { Row( @@ -117,11 +117,11 @@ fun MyScreen( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center ) { - Image( - painter = painterResource(id = R.drawable.ic_logo), - contentDescription = "logo", - modifier = Modifier.width(90.dp), - ) + Image( + painter = painterResource(id = R.drawable.ic_logo), + contentDescription = "logo", + modifier = Modifier.width(90.dp) + ) } Box( @@ -132,18 +132,18 @@ fun MyScreen( .background( brush = Brush.verticalGradient( - colors = listOf( - Color.Black.copy(alpha = 0.15f), - Color.Transparent, - ), - ), - ), + colors = + listOf( + Color.Black.copy(alpha = 0.15f), + Color.Transparent + ) + ) + ) ) } LazyColumn( - modifier = Modifier.fillMaxSize(), + modifier = Modifier.fillMaxSize() ) { - item { val user = uiState.user Row( @@ -151,7 +151,7 @@ fun MyScreen( Modifier .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 20.dp), - verticalAlignment = Alignment.CenterVertically, + verticalAlignment = Alignment.CenterVertically ) { Box( modifier = @@ -163,21 +163,21 @@ fun MyScreen( Color.Transparent } else { Primary.copy(alpha = 0.2f) - }, + } ), - contentAlignment = Alignment.Center, + contentAlignment = Alignment.Center ) { if (user?.profileImageUrl != null) { AsyncImage( model = user.profileImageUrl, contentDescription = "프로필 이미지", - modifier = Modifier.fillMaxSize(), + modifier = Modifier.fillMaxSize() ) } else { Text( text = user?.nickname?.firstOrNull()?.toString() ?: "?", style = HackathonTheme.typography.Head2_bold, - color = Color.White, + color = Color.White ) } } @@ -187,14 +187,14 @@ fun MyScreen( Column(modifier = Modifier.weight(1f)) { Text( text = user?.nickname ?: "사용자", - style = HackathonTheme.typography.Head2_bold, + style = HackathonTheme.typography.Head2_bold ) Spacer(modifier = Modifier.height(4.dp)) Text( text = "로그아웃", style = HackathonTheme.typography.Body_medium, color = Gray700, - modifier = Modifier.clickable { viewModel.logout() }, + modifier = Modifier.clickable { viewModel.logout() } ) } @@ -203,9 +203,9 @@ fun MyScreen( colors = ButtonDefaults.buttonColors( containerColor = Gray50, - contentColor = Color.Black, + contentColor = Color.Black ), - shape = RoundedCornerShape(15.dp), + shape = RoundedCornerShape(15.dp) ) { Text("닉네임 변경") } @@ -217,17 +217,17 @@ fun MyScreen( item { TabRow( selectedTabIndex = - if (uiState.selectedTab == MyPageTab.MY_RECIPES) 0 else 1, + if (uiState.selectedTab == MyPageTab.MY_RECIPES) 0 else 1 ) { Tab( selected = uiState.selectedTab == MyPageTab.MY_RECIPES, onClick = { viewModel.selectTab(MyPageTab.MY_RECIPES) }, - text = { Text("나의 레시피") }, + text = { Text("나의 레시피") } ) Tab( selected = uiState.selectedTab == MyPageTab.LIKED_COMBINATIONS, onClick = { viewModel.selectTab(MyPageTab.LIKED_COMBINATIONS) }, - text = { Text("좋아요한 조합") }, + text = { Text("좋아요한 조합") } ) } } @@ -237,11 +237,11 @@ fun MyScreen( MyPageTab.MY_RECIPES -> { items(uiState.myRecipes) { combination -> Box( - modifier = Modifier.padding(horizontal = 10.dp), + modifier = Modifier.padding(horizontal = 10.dp) ) { CombinationCard( combination = combination, - onClick = { onCombinationClick(combination.id) }, + onClick = { onCombinationClick(combination.id) } ) } } @@ -250,11 +250,11 @@ fun MyScreen( MyPageTab.LIKED_COMBINATIONS -> { items(uiState.likedCombinations) { combination -> Box( - modifier = Modifier.padding(horizontal = 10.dp), + modifier = Modifier.padding(horizontal = 10.dp) ) { CombinationCard( combination = combination, - onClick = { onCombinationClick(combination.id) }, + onClick = { onCombinationClick(combination.id) } ) } } From 2f840aa2b46c6657d068a8496f1fbe764b9a4675 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=BC=ED=98=81?= Date: Sun, 31 May 2026 18:08:07 +0900 Subject: [PATCH 4/4] =?UTF-8?q?build:=20Gradle=209.4,=20AGP=209.2=20?= =?UTF-8?q?=EB=B0=8F=20Kotlin=202.3=20=EC=97=85=EA=B7=B8=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Gradle 버전을 9.4.1로, AGP를 9.2.1로 업그레이드 - Kotlin 버전을 2.3.21로 업데이트 및 관련 KSP 버전 조정 - AGP 9 대응을 위해 build-logic의 `CommonExtension` 설정을 Application과 Library 타입별로 분리 및 리팩터링 - SDK 설정 방식을 AGP 9에서 권장하는 블록 형식(`version = release(36)`)으로 변경 - Foojay 툴체인 리졸버 플러그인 추가 및 JVM 도구 모음(toolchain) 설정 도입 - Compose BOM(2026.05.01), Retrofit(3.0.0), Hilt(2.59.2) 등 주요 의존성 라이브러리 대폭 업데이트 - `gradle.properties`에 AGP 및 빌드 성능 관련 최신 플래그 추가 --- app/build.gradle.kts | 2 +- ...droidApplicationComposeConventionPlugin.kt | 4 +- .../AndroidApplicationConventionPlugin.kt | 9 ++-- .../AndroidLibraryComposeConventionPlugin.kt | 8 +-- .../kotlin/AndroidLibraryConventionPlugin.kt | 7 +-- .../hackathon/buildlogic/AndroidCompose.kt | 26 ++++++---- .../hackathon/buildlogic/KotlinAndroid.kt | 36 +++++++++---- .../CombinationRepositoryImpl.kt | 6 ++- gradle.properties | 12 ++++- gradle/gradle-daemon-jvm.properties | 13 +++++ gradle/libs.versions.toml | 51 +++++++++---------- gradle/wrapper/gradle-wrapper.properties | 2 +- settings.gradle.kts | 3 ++ 13 files changed, 109 insertions(+), 70 deletions(-) create mode 100644 gradle/gradle-daemon-jvm.properties diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 66f0da5..18cca39 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -26,7 +26,7 @@ android { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro", + "proguard-rules.pro" ) } } diff --git a/build-logic/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt b/build-logic/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt index 173d1f0..4b4eea7 100644 --- a/build-logic/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt +++ b/build-logic/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt @@ -1,5 +1,5 @@ import com.android.build.api.dsl.ApplicationExtension -import com.example.hackathon.buildlogic.configureAndroidCompose +import com.example.hackathon.buildlogic.configureComposeApplication import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.getByType @@ -14,7 +14,7 @@ class AndroidApplicationComposeConventionPlugin : Plugin { pluginManager.apply("org.jetbrains.kotlin.plugin.compose") val extension = extensions.getByType() - configureAndroidCompose(extension) + configureComposeApplication(extension) } } } diff --git a/build-logic/src/main/kotlin/AndroidApplicationConventionPlugin.kt b/build-logic/src/main/kotlin/AndroidApplicationConventionPlugin.kt index 2140a00..dc1887b 100644 --- a/build-logic/src/main/kotlin/AndroidApplicationConventionPlugin.kt +++ b/build-logic/src/main/kotlin/AndroidApplicationConventionPlugin.kt @@ -1,12 +1,12 @@ import com.android.build.api.dsl.ApplicationExtension -import com.example.hackathon.buildlogic.configureKotlinAndroid +import com.example.hackathon.buildlogic.configureAndroidApplication import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.configure /** * 안드로이드 애플리케이션(:app) 모듈 공통 플러그인. - * com.android.application + Kotlin 적용 + 공통 SDK/Java/Kotlin 설정 + targetSdk. + * com.android.application + Kotlin 적용 + 공통 SDK/Java/Kotlin 설정. */ class AndroidApplicationConventionPlugin : Plugin { override fun apply(target: Project) { @@ -17,10 +17,7 @@ class AndroidApplicationConventionPlugin : Plugin { } extensions.configure { - configureKotlinAndroid(this) - defaultConfig.targetSdk = 36 - defaultConfig.testInstrumentationRunner = - "androidx.test.runner.AndroidJUnitRunner" + configureAndroidApplication(this) } } } diff --git a/build-logic/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt b/build-logic/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt index 4d0c7b1..ed3efed 100644 --- a/build-logic/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt +++ b/build-logic/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt @@ -1,8 +1,8 @@ import com.android.build.api.dsl.LibraryExtension -import com.example.hackathon.buildlogic.configureAndroidCompose +import com.example.hackathon.buildlogic.configureComposeLibrary import org.gradle.api.Plugin import org.gradle.api.Project -import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.getByType /** * Compose 를 사용하는 안드로이드 라이브러리 모듈 플러그인. @@ -16,8 +16,8 @@ class AndroidLibraryComposeConventionPlugin : Plugin { apply("org.jetbrains.kotlin.plugin.compose") } - val extension = extensions.getByType(LibraryExtension::class.java) - configureAndroidCompose(extension) + val extension = extensions.getByType() + configureComposeLibrary(extension) } } } diff --git a/build-logic/src/main/kotlin/AndroidLibraryConventionPlugin.kt b/build-logic/src/main/kotlin/AndroidLibraryConventionPlugin.kt index 589d981..a75ae41 100644 --- a/build-logic/src/main/kotlin/AndroidLibraryConventionPlugin.kt +++ b/build-logic/src/main/kotlin/AndroidLibraryConventionPlugin.kt @@ -1,9 +1,8 @@ import com.android.build.api.dsl.LibraryExtension -import com.example.hackathon.buildlogic.configureKotlinAndroid +import com.example.hackathon.buildlogic.configureAndroidLibrary import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.configure -import org.gradle.kotlin.dsl.dependencies /** * 안드로이드 라이브러리 모듈 공통 플러그인. @@ -18,9 +17,7 @@ class AndroidLibraryConventionPlugin : Plugin { } extensions.configure { - configureKotlinAndroid(this) - defaultConfig.testInstrumentationRunner = - "androidx.test.runner.AndroidJUnitRunner" + configureAndroidLibrary(this) } } } diff --git a/build-logic/src/main/kotlin/com/example/hackathon/buildlogic/AndroidCompose.kt b/build-logic/src/main/kotlin/com/example/hackathon/buildlogic/AndroidCompose.kt index c4ed766..c90d9ff 100644 --- a/build-logic/src/main/kotlin/com/example/hackathon/buildlogic/AndroidCompose.kt +++ b/build-logic/src/main/kotlin/com/example/hackathon/buildlogic/AndroidCompose.kt @@ -1,23 +1,27 @@ package com.example.hackathon.buildlogic -import com.android.build.api.dsl.CommonExtension +import com.android.build.api.dsl.ApplicationExtension +import com.android.build.api.dsl.LibraryExtension import org.gradle.api.Project -import org.gradle.api.artifacts.VersionCatalog import org.gradle.kotlin.dsl.dependencies -import org.gradle.kotlin.dsl.getByType /** * Jetpack Compose 공통 설정. - * buildFeatures.compose 활성화 + Compose BOM 기반 의존성을 모듈에 주입한다. - * org.jetbrains.kotlin.plugin.compose 플러그인은 각 Compose 모듈에서 별도 적용한다. + * AGP 9 에서 buildFeatures 가 concrete 타입으로 이동해 Application/Library 별로 활성화한다. + * org.jetbrains.kotlin.plugin.compose 플러그인은 각 Compose 모듈에서 별도 적용. */ -internal fun Project.configureAndroidCompose(commonExtension: CommonExtension<*, *, *, *, *, *>) { - commonExtension.apply { - buildFeatures { - compose = true - } - } +internal fun Project.configureComposeApplication(extension: ApplicationExtension) { + extension.buildFeatures.compose = true + addComposeDependencies() +} + +internal fun Project.configureComposeLibrary(extension: LibraryExtension) { + extension.buildFeatures.compose = true + addComposeDependencies() +} +/** Compose BOM 기반 공통 의존성 주입. */ +private fun Project.addComposeDependencies() { dependencies { val bom = libs.findLibrary("androidx-compose-bom").get() add("implementation", platform(bom)) diff --git a/build-logic/src/main/kotlin/com/example/hackathon/buildlogic/KotlinAndroid.kt b/build-logic/src/main/kotlin/com/example/hackathon/buildlogic/KotlinAndroid.kt index 88d338e..d96e839 100644 --- a/build-logic/src/main/kotlin/com/example/hackathon/buildlogic/KotlinAndroid.kt +++ b/build-logic/src/main/kotlin/com/example/hackathon/buildlogic/KotlinAndroid.kt @@ -1,30 +1,48 @@ package com.example.hackathon.buildlogic -import com.android.build.api.dsl.CommonExtension +import com.android.build.api.dsl.ApplicationExtension +import com.android.build.api.dsl.LibraryExtension import org.gradle.api.JavaVersion import org.gradle.api.Project -import org.gradle.kotlin.dsl.dependencies import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension /** * Android Application/Library 모듈 공통 SDK·Java·Kotlin 설정. - * compileSdk 36 / minSdk 36 / JVM 17 을 단일 지점에서 강제한다. + * AGP 9 에서 CommonExtension 의 제네릭/블록 메서드가 제거돼 concrete 타입(Application/Library)별로 설정한다. + * compileSdk 36 / minSdk 36 / JVM 17 을 단일 지점에서 강제. */ -internal fun Project.configureKotlinAndroid(commonExtension: CommonExtension<*, *, *, *, *, *>) { - commonExtension.apply { - compileSdk = 36 - +internal fun Project.configureAndroidApplication(extension: ApplicationExtension) { + extension.apply { + compileSdk { version = release(36) } defaultConfig { - minSdk = 36 + // AGP 9 부터 minSdk/targetSdk 의 Int setter 는 컨벤션 플러그인에서 신뢰성 있게 적용되지 않음. + // 블록 형식 minSdk { version = release(N) } 사용 필수. + minSdk { version = release(36) } + targetSdk { version = release(36) } + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } - compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } } + configureKotlin() +} +internal fun Project.configureAndroidLibrary(extension: LibraryExtension) { + extension.apply { + compileSdk { version = release(36) } + defaultConfig { + // AGP 9: 블록 형식 minSdk 사용 (Int setter 는 컨벤션 플러그인에서 무효). + minSdk { version = release(36) } + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + } configureKotlin() } diff --git a/core/data/src/main/java/com/example/hackathon/core/data/repositoryimpl/CombinationRepositoryImpl.kt b/core/data/src/main/java/com/example/hackathon/core/data/repositoryimpl/CombinationRepositoryImpl.kt index d02cdb6..799226e 100644 --- a/core/data/src/main/java/com/example/hackathon/core/data/repositoryimpl/CombinationRepositoryImpl.kt +++ b/core/data/src/main/java/com/example/hackathon/core/data/repositoryimpl/CombinationRepositoryImpl.kt @@ -235,11 +235,13 @@ constructor( val registerRequest = RegisterRequest( title = title, - categories = listOf(category.name), // 단일 카테고리를 배열로 변환 + // 단일 카테고리를 배열로 변환 + categories = listOf(category.name), ingredients = ingredientDtos, images = imageStrings, description = description, - isPrivate = !isPublic // isPublic = true면 isPrivate = false (전체 공개), isPublic = false면 isPrivate = true (나만 보기) + // isPublic=true → isPrivate=false(전체 공개), false → true(나만 보기) + isPrivate = !isPublic ) val response = combinationService.register(userId, registerRequest) diff --git a/gradle.properties b/gradle.properties index b61f4b1..2a35e05 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,4 +20,14 @@ kotlin.code.style=official # Enables namespacing of each library's R class so that its R class includes only the # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true \ No newline at end of file +android.nonTransitiveRClass=true +android.defaults.buildfeatures.resvalues=true +android.sdk.defaultTargetSdkToCompileSdkIfUnset=false +android.enableAppCompileTimeRClass=false +android.usesSdkInManifest.disallowed=false +android.uniquePackageNames=false +android.dependency.useConstraints=true +android.r8.strictFullModeForKeepRules=false +android.r8.optimizedResourceShrinking=false +android.builtInKotlin=false +android.newDsl=false diff --git a/gradle/gradle-daemon-jvm.properties b/gradle/gradle-daemon-jvm.properties new file mode 100644 index 0000000..baa28d1 --- /dev/null +++ b/gradle/gradle-daemon-jvm.properties @@ -0,0 +1,13 @@ +#This file is generated by updateDaemonJvm +toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect +toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect +toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect +toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect +toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/7083b89563e7ce20943037b8cd2b8cc2/redirect +toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/060bbb778a1f55ea705fdebd2ccfeab9/redirect +toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect +toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect +toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/d09679dc60fe5aa05ef7d03efdefac20/redirect +toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/ed4e3bf2f5e7c5d9aabc4cbd8acd555e/redirect +toolchainVendor=JETBRAINS +toolchainVersion=21 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ce44fde..ea6e407 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,33 +1,29 @@ [versions] -agp = "8.13.2" -kotlin = "2.0.21" -coreKtx = "1.10.1" +agp = "9.2.1" +kotlin = "2.3.21" +coreKtx = "1.18.0" junit = "4.13.2" -junitVersion = "1.1.5" -espressoCore = "3.5.1" -lifecycleRuntimeKtx = "2.6.1" -activityCompose = "1.8.0" -composeBom = "2024.09.00" -navigationCompose = "2.8.9" -okhttp = "4.11.0" -retrofit = "2.11.0" -kotlinxSerializationJson = "1.8.0" -coilCompose = "3.2.0" -coilNetworkOkhttp = "3.2.0" -hilt = "2.56.1" -hiltNavigationCompose = "1.2.0" -hiltManager = "1.0.0" -ksp = "2.0.21-1.0.28" -composeRules = "1.2.2" -mockk = "1.13.8" -turbine = "1.0.0" -kotlinxCoroutinesTest = "1.7.3" +junitVersion = "1.3.0" +espressoCore = "3.7.0" +lifecycleRuntimeKtx = "2.10.0" +activityCompose = "1.13.0" +composeBom = "2026.05.01" +navigationCompose = "2.9.8" +okhttp = "5.3.2" +retrofit = "3.0.0" +kotlinxSerializationJson = "1.11.0" +coilCompose = "3.4.0" +coilNetworkOkhttp = "3.4.0" +hilt = "2.59.2" +hiltNavigationCompose = "1.3.0" +ksp = "2.3.9" +mockk = "1.14.11" +turbine = "1.2.1" +kotlinxCoroutinesTest = "1.11.0" googleServices = "4.4.4" -# 34.14.0 은 play-services-measurement 가 Kotlin metadata 2.2.0 으로 빌드돼 본 프로젝트 Kotlin 2.0.21 과 비호환. -# (IDE 의 "newer version available" 경고는 호환성 미고려 → kotlin 2.1+ 업그레이드 전까지 34.7.0 고정) -firebaseBom = "34.7.0" -datastore = "1.1.1" -foundationLayout = "1.10.0" +firebaseBom = "34.14.0" +datastore = "1.2.1" +foundationLayout = "1.11.2" coreSplashscreen = "1.2.0" [libraries] @@ -71,7 +67,6 @@ kotlin-metadata-jvm = { group = "org.jetbrains.kotlin", name = "kotlin-metadata- # build-logic 컨벤션 플러그인이 Gradle 플러그인 API 에 접근하기 위한 의존성 (compileOnly) android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "agp" } kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } -ksp-gradlePlugin = { group = "com.google.devtools.ksp", name = "symbol-processing-gradle-plugin", version.ref = "ksp" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a346632..25de37c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ #Tue Jan 06 13:49:23 KST 2026 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/settings.gradle.kts b/settings.gradle.kts index df1d907..2687ce1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -12,6 +12,9 @@ pluginManagement { gradlePluginPortal() } } +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" +} dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories {