From fd7b14907b7a117473bcf098109d926d6382b37e Mon Sep 17 00:00:00 2001 From: Taewan Park Date: Fri, 8 Dec 2023 10:35:03 +0900 Subject: [PATCH] v0.3.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit πŸš€ Feature Updates - κ·Έλž˜ν”„ 데이터 λ³€ν™˜ 버그 μˆ˜μ • - λ‹€μ΄λ‚˜λ―Ή ν…Œλ§ˆ 지원 - μƒν’ˆ μ •λ³΄λ‘œκΉŒμ§€ λ”₯링크 지원 - μ ‘κ·Όμ„± κ°œμ„  (Talkback μ „λΆ€ 지원) - ν‘Έμ‹œ μ•Œλ¦Ό 지원 🚧Notice ν˜„μž¬ μ•Œλ¦Ό μ„€μ • 토글에 버그가 μžˆμŠ΅λ‹ˆλ‹€. λΉ λ₯Έ μ‹œμΌ 내에 μˆ˜μ •ν•˜κ² μŠ΅λ‹ˆλ‹€. Co-Authored-By: EunhoKang Co-Authored-By: ootr47 <83055885+ootr47@users.noreply.github.com> Co-Authored-By: 손문기 <39684860+Muungi@users.noreply.github.com> Co-Authored-By: ByeongIk Choi --- .gitmodules | 4 - android/.gitignore | 1 + android/.idea/gradle.xml | 6 +- android/.idea/migrations.xml | 10 + android/app/build.gradle.kts | 14 +- android/app/src/main/AndroidManifest.xml | 49 +- .../app/priceguard/data/GraphDataConverter.kt | 97 + .../data/datastore/ConfigDataSource.kt | 8 + .../data/datastore/ConfigDataSourceImpl.kt | 42 + .../data/datastore/TokenDataSourceImpl.kt | 5 +- .../app/priceguard/data/dto/LoginResponse.kt | 23 - .../app/priceguard/data/dto/ProductInfo.kt | 8 - .../priceguard/data/dto/ProductVerifyDTO.kt | 12 - .../app/priceguard/data/dto/SignupResponse.kt | 24 - .../data/dto/{ => add}/ProductAddRequest.kt | 2 +- .../data/dto/{ => add}/ProductAddResponse.kt | 2 +- .../data/dto/alert/AlertUpdateResponse.kt | 9 + .../dto/{ => delete}/ProductDeleteResponse.kt | 2 +- .../data/dto/{ => detail}/ProductResponse.kt | 17 +- .../firebase/FirebaseTokenUpdateRequest.kt | 8 + .../firebase/FirebaseTokenUpdateResponse.kt | 9 + .../data/dto/{ => list}/ProductDTO.kt | 15 +- .../dto/{ => list}/ProductListResponse.kt | 2 +- .../data/dto/{ => login}/LoginRequest.kt | 2 +- .../data/dto/login/LoginResponse.kt | 11 + .../data/dto/{ => patch}/PricePatchRequest.kt | 2 +- .../dto/{ => patch}/PricePatchResponse.kt | 2 +- .../{ => recommend}/RecommendProductDTO.kt | 14 +- .../RecommendProductResponse.kt | 2 +- .../data/dto/{ => renew}/RenewResponse.kt | 9 +- .../data/dto/{ => signup}/SignupRequest.kt | 2 +- .../data/dto/signup/SignupResponse.kt | 11 + .../dto/{ => verify}/ProductVerifyRequest.kt | 2 +- .../dto/{ => verify}/ProductVerifyResponse.kt | 2 +- .../data/graph/GraphDataConverter.kt | 24 - .../data/graph/ProductChartDataset.kt | 5 +- .../app/priceguard/data/network/AuthAPI.kt | 2 +- .../app/priceguard/data/network/ProductAPI.kt | 26 +- .../data/network/ProductRepositoryResult.kt | 10 - .../data/network/RequestInterceptor.kt | 2 +- .../app/priceguard/data/network/UserAPI.kt | 18 +- .../data/{network => repository}/APIResult.kt | 2 +- .../data/repository/ProductRepository.kt | 29 - .../data/repository/RepositoryResult.kt | 8 + .../data/repository/TokenRepository.kt | 13 - .../data/repository/TokenRepositoryImpl.kt | 75 - .../data/repository/UserRepository.kt | 11 - .../data/repository/UserRepositoryImpl.kt | 65 - .../data/repository/auth/AuthErrorState.kt | 7 + .../data/repository/auth/AuthRepository.kt | 12 + .../repository/auth/AuthRepositoryImpl.kt | 76 + .../product}/ProductErrorState.kt | 2 +- .../repository/product/ProductRepository.kt | 28 + .../{ => product}/ProductRepositoryImpl.kt | 138 +- .../data/repository/token/TokenErrorState.kt | 7 + .../data/repository/token/TokenRepository.kt | 15 + .../repository/token/TokenRepositoryImpl.kt | 111 + .../token/TokenUserData.kt} | 9 +- ...itoryModule.kt => AuthRepositoryModule.kt} | 8 +- .../priceguard/di/ConfigDataSourceModule.kt | 21 + .../java/app/priceguard/di/DataStoreModule.kt | 27 +- .../priceguard/di/ProductRepositoryModule.kt | 8 +- .../priceguard/di/TokenDataSourceModule.kt | 2 +- .../priceguard/di/TokenRepositoryModule.kt | 10 +- .../PriceGuardFirebaseMessagingService.kt | 11 + .../priceguard/service/UpdateAlarmWorker.kt | 58 + .../priceguard/service/UpdateTokenWorker.kt | 46 + .../java/app/priceguard/ui/PriceGuardApp.kt | 75 +- .../priceguard/ui/additem/AddItemActivity.kt | 21 +- .../confirm/ConfirmItemLinkFragment.kt | 42 +- .../confirm/ConfirmItemLinkViewModel.kt | 21 +- .../additem/link/LinkHelperWebViewActivity.kt | 19 + .../additem/link/RegisterItemLinkFragment.kt | 55 +- .../additem/link/RegisterItemLinkViewModel.kt | 24 +- .../setprice/SetTargetPriceFragment.kt | 76 +- .../setprice/SetTargetPriceViewModel.kt | 32 +- .../app/priceguard/ui/data/LoginResult.kt | 6 + .../priceguard/ui/data/PricePatchResult.kt | 6 + .../priceguard/ui/data/ProductAddResult.kt | 6 + .../app/priceguard/ui/data/ProductData.kt | 14 + .../priceguard/ui/data/ProductDetailResult.kt | 16 + .../priceguard/ui/data/ProductVerifyResult.kt | 9 + .../ui/data/RecommendProductData.kt | 13 + .../app/priceguard/ui/data/SignupResult.kt | 6 + .../app/priceguard/ui/data/UserDataResult.kt | 6 + .../priceguard/ui/detail/DetailActivity.kt | 105 +- .../ui/detail/ProductDetailViewModel.kt | 59 +- .../app/priceguard/ui/home/HomeActivity.kt | 112 + .../ui/home/ProductSummaryAdapter.kt | 44 +- .../ui/home/ProductSummaryClickListener.kt | 7 + .../ui/home/list/ProductListFragment.kt | 46 +- .../ui/home/list/ProductListViewModel.kt | 33 +- .../ui/home/mypage/MyPageFragment.kt | 8 +- .../ui/home/mypage/MyPageViewModel.kt | 2 +- .../recommend/RecommendedProductFragment.kt | 21 +- .../recommend/RecommendedProductViewModel.kt | 23 +- .../ui/home/theme/ThemeDialogFragment.kt | 124 + .../app/priceguard/ui/login/LoginActivity.kt | 19 +- .../app/priceguard/ui/login/LoginViewModel.kt | 73 +- .../priceguard/ui/signup/SignupActivity.kt | 36 +- .../priceguard/ui/signup/SignupViewModel.kt | 79 +- .../ui/splash/SplashScreenActivity.kt | 22 +- .../ui/splash/SplashScreenViewModel.kt | 25 +- .../priceguard/ui/util/ui/NetworkDialog.kt | 2 +- .../ui/util/ui/NotificationSetting.kt | 11 + .../app/src/main/res/anim/from_left_enter.xml | 9 + .../src/main/res/anim/from_right_enter.xml | 9 + .../app/src/main/res/anim/to_left_exit.xml | 9 + .../app/src/main/res/anim/to_right_exit.xml | 9 + .../ic_priceguard_notification.xml | 20 + .../ic_priceguard_notification.png | Bin 0 -> 656 bytes .../ic_priceguard_notification.png | Bin 0 -> 425 bytes .../ic_priceguard_notification.png | Bin 0 -> 839 bytes .../ic_priceguard_notification.png | Bin 0 -> 1321 bytes .../app/src/main/res/drawable/ic_share.xml | 10 + .../main/res/layout-land/activity_login.xml | 4 +- .../src/main/res/layout/activity_add_item.xml | 4 +- .../src/main/res/layout/activity_detail.xml | 29 +- .../app/src/main/res/layout/activity_home.xml | 3 +- .../layout/activity_link_helper_web_view.xml | 21 + .../src/main/res/layout/activity_login.xml | 7 +- .../src/main/res/layout/activity_signup.xml | 1 + .../res/layout/activity_splash_screen.xml | 8 +- .../res/layout/fragment_confirm_item_link.xml | 17 +- .../src/main/res/layout/fragment_my_page.xml | 6 +- .../main/res/layout/fragment_product_list.xml | 2 +- .../layout/fragment_register_item_link.xml | 2 + .../res/layout/fragment_set_target_price.xml | 238 +- .../main/res/layout/fragment_theme_dialog.xml | 94 + .../src/main/res/layout/item_my_page_list.xml | 4 +- .../main/res/layout/item_product_summary.xml | 4 +- .../main/res/menu/list_top_app_bar_menu.xml | 9 - .../app/src/main/res/navigation/nav_graph.xml | 30 +- .../main/res/navigation/nav_graph_home.xml | 11 +- .../app/src/main/res/values-night/themes.xml | 1 - android/app/src/main/res/values/strings.xml | 44 +- android/app/src/main/res/values/styles.xml | 4 + android/app/src/main/res/values/themes.xml | 6 +- android/materialchart | 1 - android/release_notes.txt | 18 +- android/settings.gradle | 3 +- backend/package-lock.json | 2567 +++++++++++++---- backend/package.json | 9 +- backend/src/app.module.ts | 3 + backend/src/auth/auth.module.ts | 7 +- backend/src/auth/auth.service.ts | 18 +- backend/src/auth/jwt/jwt.service.ts | 16 +- backend/src/configs/redis.config.ts | 10 + backend/src/constants.ts | 16 +- backend/src/cron/cron.service.ts | 160 + backend/src/dto/firebase.token.dto.ts | 12 + backend/src/dto/product.cache.dto.ts | 7 + backend/src/dto/product.info.dto.ts | 2 + backend/src/dto/product.rank.cache.dto.ts | 8 + backend/src/dto/product.swagger.dto.ts | 14 + backend/src/dto/product.tracking.dto.ts | 1 + backend/src/dto/user.swagger.dto.ts | 13 + .../src/entities/trackingProduct.entity.ts | 12 +- backend/src/exceptions/exception.fillter.ts | 2 +- backend/src/firebase/firebase.config.ts | 27 + backend/src/firebase/firebase.service.ts | 16 + backend/src/product/product.controller.ts | 14 +- backend/src/product/product.module.ts | 4 +- backend/src/product/product.repository.ts | 22 + backend/src/product/product.service.ts | 200 +- .../src/product/trackingProduct.repository.ts | 41 +- backend/src/schema/product.schema.ts | 3 +- backend/src/user/user.controller.ts | 41 +- backend/src/user/user.service.spec.ts | 89 + backend/src/user/user.service.ts | 2 +- backend/src/utils/cache.ts | 114 + backend/src/utils/openapi.11st.ts | 3 +- 172 files changed, 5097 insertions(+), 1605 deletions(-) delete mode 100644 .gitmodules create mode 100644 android/.idea/migrations.xml create mode 100644 android/app/src/main/java/app/priceguard/data/GraphDataConverter.kt create mode 100644 android/app/src/main/java/app/priceguard/data/datastore/ConfigDataSource.kt create mode 100644 android/app/src/main/java/app/priceguard/data/datastore/ConfigDataSourceImpl.kt delete mode 100644 android/app/src/main/java/app/priceguard/data/dto/LoginResponse.kt delete mode 100644 android/app/src/main/java/app/priceguard/data/dto/ProductInfo.kt delete mode 100644 android/app/src/main/java/app/priceguard/data/dto/ProductVerifyDTO.kt delete mode 100644 android/app/src/main/java/app/priceguard/data/dto/SignupResponse.kt rename android/app/src/main/java/app/priceguard/data/dto/{ => add}/ProductAddRequest.kt (80%) rename android/app/src/main/java/app/priceguard/data/dto/{ => add}/ProductAddResponse.kt (79%) create mode 100644 android/app/src/main/java/app/priceguard/data/dto/alert/AlertUpdateResponse.kt rename android/app/src/main/java/app/priceguard/data/dto/{ => delete}/ProductDeleteResponse.kt (78%) rename android/app/src/main/java/app/priceguard/data/dto/{ => detail}/ProductResponse.kt (55%) create mode 100644 android/app/src/main/java/app/priceguard/data/dto/firebase/FirebaseTokenUpdateRequest.kt create mode 100644 android/app/src/main/java/app/priceguard/data/dto/firebase/FirebaseTokenUpdateResponse.kt rename android/app/src/main/java/app/priceguard/data/dto/{ => list}/ProductDTO.kt (52%) rename android/app/src/main/java/app/priceguard/data/dto/{ => list}/ProductListResponse.kt (83%) rename android/app/src/main/java/app/priceguard/data/dto/{ => login}/LoginRequest.kt (77%) create mode 100644 android/app/src/main/java/app/priceguard/data/dto/login/LoginResponse.kt rename android/app/src/main/java/app/priceguard/data/dto/{ => patch}/PricePatchRequest.kt (79%) rename android/app/src/main/java/app/priceguard/data/dto/{ => patch}/PricePatchResponse.kt (78%) rename android/app/src/main/java/app/priceguard/data/dto/{ => recommend}/RecommendProductDTO.kt (52%) rename android/app/src/main/java/app/priceguard/data/dto/{ => recommend}/RecommendProductResponse.kt (82%) rename android/app/src/main/java/app/priceguard/data/dto/{ => renew}/RenewResponse.kt (61%) rename android/app/src/main/java/app/priceguard/data/dto/{ => signup}/SignupRequest.kt (80%) create mode 100644 android/app/src/main/java/app/priceguard/data/dto/signup/SignupResponse.kt rename android/app/src/main/java/app/priceguard/data/dto/{ => verify}/ProductVerifyRequest.kt (75%) rename android/app/src/main/java/app/priceguard/data/dto/{ => verify}/ProductVerifyResponse.kt (89%) delete mode 100644 android/app/src/main/java/app/priceguard/data/graph/GraphDataConverter.kt delete mode 100644 android/app/src/main/java/app/priceguard/data/network/ProductRepositoryResult.kt rename android/app/src/main/java/app/priceguard/data/{network => repository}/APIResult.kt (94%) delete mode 100644 android/app/src/main/java/app/priceguard/data/repository/ProductRepository.kt create mode 100644 android/app/src/main/java/app/priceguard/data/repository/RepositoryResult.kt delete mode 100644 android/app/src/main/java/app/priceguard/data/repository/TokenRepository.kt delete mode 100644 android/app/src/main/java/app/priceguard/data/repository/TokenRepositoryImpl.kt delete mode 100644 android/app/src/main/java/app/priceguard/data/repository/UserRepository.kt delete mode 100644 android/app/src/main/java/app/priceguard/data/repository/UserRepositoryImpl.kt create mode 100644 android/app/src/main/java/app/priceguard/data/repository/auth/AuthErrorState.kt create mode 100644 android/app/src/main/java/app/priceguard/data/repository/auth/AuthRepository.kt create mode 100644 android/app/src/main/java/app/priceguard/data/repository/auth/AuthRepositoryImpl.kt rename android/app/src/main/java/app/priceguard/data/{dto => repository/product}/ProductErrorState.kt (72%) create mode 100644 android/app/src/main/java/app/priceguard/data/repository/product/ProductRepository.kt rename android/app/src/main/java/app/priceguard/data/repository/{ => product}/ProductRepositoryImpl.kt (60%) create mode 100644 android/app/src/main/java/app/priceguard/data/repository/token/TokenErrorState.kt create mode 100644 android/app/src/main/java/app/priceguard/data/repository/token/TokenRepository.kt create mode 100644 android/app/src/main/java/app/priceguard/data/repository/token/TokenRepositoryImpl.kt rename android/app/src/main/java/app/priceguard/data/{dto/UserDataDTO.kt => repository/token/TokenUserData.kt} (55%) rename android/app/src/main/java/app/priceguard/di/{UserRepositoryModule.kt => AuthRepositoryModule.kt} (55%) create mode 100644 android/app/src/main/java/app/priceguard/di/ConfigDataSourceModule.kt create mode 100644 android/app/src/main/java/app/priceguard/service/PriceGuardFirebaseMessagingService.kt create mode 100644 android/app/src/main/java/app/priceguard/service/UpdateAlarmWorker.kt create mode 100644 android/app/src/main/java/app/priceguard/service/UpdateTokenWorker.kt create mode 100644 android/app/src/main/java/app/priceguard/ui/additem/link/LinkHelperWebViewActivity.kt create mode 100644 android/app/src/main/java/app/priceguard/ui/data/LoginResult.kt create mode 100644 android/app/src/main/java/app/priceguard/ui/data/PricePatchResult.kt create mode 100644 android/app/src/main/java/app/priceguard/ui/data/ProductAddResult.kt create mode 100644 android/app/src/main/java/app/priceguard/ui/data/ProductData.kt create mode 100644 android/app/src/main/java/app/priceguard/ui/data/ProductDetailResult.kt create mode 100644 android/app/src/main/java/app/priceguard/ui/data/ProductVerifyResult.kt create mode 100644 android/app/src/main/java/app/priceguard/ui/data/RecommendProductData.kt create mode 100644 android/app/src/main/java/app/priceguard/ui/data/SignupResult.kt create mode 100644 android/app/src/main/java/app/priceguard/ui/data/UserDataResult.kt create mode 100644 android/app/src/main/java/app/priceguard/ui/home/ProductSummaryClickListener.kt create mode 100644 android/app/src/main/java/app/priceguard/ui/home/theme/ThemeDialogFragment.kt create mode 100644 android/app/src/main/java/app/priceguard/ui/util/ui/NotificationSetting.kt create mode 100644 android/app/src/main/res/anim/from_left_enter.xml create mode 100644 android/app/src/main/res/anim/from_right_enter.xml create mode 100644 android/app/src/main/res/anim/to_left_exit.xml create mode 100644 android/app/src/main/res/anim/to_right_exit.xml create mode 100644 android/app/src/main/res/drawable-anydpi/ic_priceguard_notification.xml create mode 100644 android/app/src/main/res/drawable-hdpi/ic_priceguard_notification.png create mode 100644 android/app/src/main/res/drawable-mdpi/ic_priceguard_notification.png create mode 100644 android/app/src/main/res/drawable-xhdpi/ic_priceguard_notification.png create mode 100644 android/app/src/main/res/drawable-xxhdpi/ic_priceguard_notification.png create mode 100644 android/app/src/main/res/drawable/ic_share.xml create mode 100644 android/app/src/main/res/layout/activity_link_helper_web_view.xml create mode 100644 android/app/src/main/res/layout/fragment_theme_dialog.xml delete mode 100644 android/app/src/main/res/menu/list_top_app_bar_menu.xml delete mode 160000 android/materialchart create mode 100644 backend/src/configs/redis.config.ts create mode 100644 backend/src/cron/cron.service.ts create mode 100644 backend/src/dto/firebase.token.dto.ts create mode 100644 backend/src/dto/product.cache.dto.ts create mode 100644 backend/src/dto/product.rank.cache.dto.ts create mode 100644 backend/src/firebase/firebase.config.ts create mode 100644 backend/src/firebase/firebase.service.ts create mode 100644 backend/src/user/user.service.spec.ts create mode 100644 backend/src/utils/cache.ts diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 70ad252..0000000 --- a/.gitmodules +++ /dev/null @@ -1,4 +0,0 @@ -[submodule "android/materialchart"] - path = android/materialchart - url = https://github.com/Taewan-P/material-android-chart - branch = release diff --git a/android/.gitignore b/android/.gitignore index f01e7c6..0b6b3bb 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -7,6 +7,7 @@ /.idea/workspace.xml /.idea/navEditor.xml /.idea/assetWizardSettings.xml +/.idea/deploymentTargetDropDown.xml .DS_Store /build /captures diff --git a/android/.idea/gradle.xml b/android/.idea/gradle.xml index 266f9cb..0897082 100644 --- a/android/.idea/gradle.xml +++ b/android/.idea/gradle.xml @@ -4,17 +4,15 @@ diff --git a/android/.idea/migrations.xml b/android/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/android/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index edeeff2..3191414 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -19,10 +19,8 @@ android { applicationId = "app.priceguard" minSdk = 29 targetSdk = 34 - versionCode = 2 - versionName = "0.1.1" - versionCode = 3 - versionName = "0.2.0" + versionCode = 4 + versionName = "0.3.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } @@ -64,6 +62,7 @@ dependencies { implementation("com.google.firebase:firebase-analytics") implementation("com.google.firebase:firebase-crashlytics") implementation("com.google.firebase:firebase-perf") + implementation("com.google.firebase:firebase-messaging") // Android implementation("androidx.core:core-ktx:1.12.0") @@ -106,8 +105,13 @@ dependencies { // Pull to Refresh implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") + // Worker + implementation("androidx.work:work-runtime-ktx:2.9.0") + implementation("androidx.hilt:hilt-work:1.1.0") + kapt("androidx.hilt:hilt-compiler:1.1.0") + // Material chart - implementation(project(":materialchart")) + implementation("app.priceguard:materialchart:0.1.2") } kapt { diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index c0c7503..fd71dcf 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -14,6 +14,17 @@ android:supportsRtl="true" android:theme="@style/Theme.PriceGuard" tools:targetApi="34"> + + + + + @@ -37,16 +48,50 @@ android:exported="false" /> + android:exported="true"> + + + + + + + + + android:exported="true" + android:launchMode="singleTask"> + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/java/app/priceguard/data/GraphDataConverter.kt b/android/app/src/main/java/app/priceguard/data/GraphDataConverter.kt new file mode 100644 index 0000000..b7325fa --- /dev/null +++ b/android/app/src/main/java/app/priceguard/data/GraphDataConverter.kt @@ -0,0 +1,97 @@ +package app.priceguard.data + +import app.priceguard.data.dto.PriceDataDTO +import app.priceguard.data.graph.ProductChartData +import app.priceguard.materialchart.data.GraphMode + +class GraphDataConverter { + + fun toDataset(priceData: List?): List { + priceData ?: return listOf() + if (priceData.isEmpty()) { + return listOf() + } + + val dataList = mutableListOf() + priceData.forEach { dto -> + dto.time ?: return@forEach + dto.price ?: return@forEach + dto.isSoldOut ?: return@forEach + dataList.add( + ProductChartData( + x = dto.time / 1000, + y = dto.price.toFloat(), + valid = dto.isSoldOut.not() + ) + ) + } + + return dataList.toList() + } + + fun packWithEdgeData( + list: List, + period: GraphMode = GraphMode.DAY + ): List { + val currentTime = getCurrentTime() + val startTime = getStartTime(period, currentTime) + val sortedList = list.sortedBy { it.x } + val filteredList = sortedList.filter { it.x in startTime..currentTime } + val sievedList = sortedList.filter { it.x !in startTime..currentTime } + val startData = if (sievedList.none()) { + list.first() + } else { + sievedList.last() + } + + return if (filteredList.isEmpty()) { + listOf( + ProductChartData(startTime, startData.y, startData.valid), + ProductChartData(currentTime, list.last().y, list.last().valid) + ) + } else { + listOf( + ProductChartData( + startTime, + startData.y, + startData.valid + ) + ) + filteredList + ProductChartData( + currentTime, + filteredList.last().y, + filteredList.last().valid + ) + } + } + + private fun getStartTime(period: GraphMode, currentTime: Float = getCurrentTime()): Float { + return when (period) { + GraphMode.DAY -> { + currentTime - DAY + } + + GraphMode.WEEK -> { + currentTime - WEEK + } + + GraphMode.MONTH -> { + currentTime - MONTH + } + + GraphMode.QUARTER -> { + currentTime - QUARTER + } + } + } + + private fun getCurrentTime(): Float { + return (System.currentTimeMillis() / 1000).toFloat() + } + + companion object { + const val DAY = 86400 + const val WEEK = DAY * 7 + const val MONTH = DAY * 31 + const val QUARTER = MONTH * 3 + } +} diff --git a/android/app/src/main/java/app/priceguard/data/datastore/ConfigDataSource.kt b/android/app/src/main/java/app/priceguard/data/datastore/ConfigDataSource.kt new file mode 100644 index 0000000..e98ef6c --- /dev/null +++ b/android/app/src/main/java/app/priceguard/data/datastore/ConfigDataSource.kt @@ -0,0 +1,8 @@ +package app.priceguard.data.datastore + +interface ConfigDataSource { + suspend fun saveDynamicMode(mode: Int) + suspend fun saveDarkMode(mode: Int) + suspend fun getDynamicMode(): Int? + suspend fun getDarkMode(): Int? +} diff --git a/android/app/src/main/java/app/priceguard/data/datastore/ConfigDataSourceImpl.kt b/android/app/src/main/java/app/priceguard/data/datastore/ConfigDataSourceImpl.kt new file mode 100644 index 0000000..3de29c4 --- /dev/null +++ b/android/app/src/main/java/app/priceguard/data/datastore/ConfigDataSourceImpl.kt @@ -0,0 +1,42 @@ +package app.priceguard.data.datastore + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey +import app.priceguard.di.ConfigQualifier +import javax.inject.Inject +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map + +class ConfigDataSourceImpl @Inject constructor( + @ConfigQualifier private val dataStore: DataStore +) : ConfigDataSource { + + private val dynamicMode = intPreferencesKey("dynamic_mode") + private val darkMode = intPreferencesKey("dark_mode") + + override suspend fun saveDynamicMode(mode: Int) { + dataStore.edit { preferences -> + preferences[dynamicMode] = mode + } + } + + override suspend fun saveDarkMode(mode: Int) { + dataStore.edit { preferences -> + preferences[darkMode] = mode + } + } + + override suspend fun getDynamicMode(): Int? { + return dataStore.data.map { preferences -> + preferences[dynamicMode] + }.first() + } + + override suspend fun getDarkMode(): Int? { + return dataStore.data.map { preferences -> + preferences[darkMode] + }.first() + } +} diff --git a/android/app/src/main/java/app/priceguard/data/datastore/TokenDataSourceImpl.kt b/android/app/src/main/java/app/priceguard/data/datastore/TokenDataSourceImpl.kt index 6912b3d..5a0932b 100644 --- a/android/app/src/main/java/app/priceguard/data/datastore/TokenDataSourceImpl.kt +++ b/android/app/src/main/java/app/priceguard/data/datastore/TokenDataSourceImpl.kt @@ -4,11 +4,14 @@ import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey +import app.priceguard.di.TokensQualifier import javax.inject.Inject import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map -class TokenDataSourceImpl @Inject constructor(private val dataStore: DataStore) : TokenDataSource { +class TokenDataSourceImpl @Inject constructor( + @TokensQualifier private val dataStore: DataStore +) : TokenDataSource { private val accessTokenKey = stringPreferencesKey("access_token") private val refreshTokenKey = stringPreferencesKey("refresh_token") diff --git a/android/app/src/main/java/app/priceguard/data/dto/LoginResponse.kt b/android/app/src/main/java/app/priceguard/data/dto/LoginResponse.kt deleted file mode 100644 index f4e1026..0000000 --- a/android/app/src/main/java/app/priceguard/data/dto/LoginResponse.kt +++ /dev/null @@ -1,23 +0,0 @@ -package app.priceguard.data.dto - -import kotlinx.serialization.Serializable - -@Serializable -data class LoginResponse( - val statusCode: Int, - val message: String, - val accessToken: String, - val refreshToken: String -) - -data class LoginResult( - val loginState: LoginState, - val accessToken: String?, - val refreshToken: String? -) - -enum class LoginState { - SUCCESS, - INVALID_PARAMETER, - UNDEFINED_ERROR -} diff --git a/android/app/src/main/java/app/priceguard/data/dto/ProductInfo.kt b/android/app/src/main/java/app/priceguard/data/dto/ProductInfo.kt deleted file mode 100644 index 740d789..0000000 --- a/android/app/src/main/java/app/priceguard/data/dto/ProductInfo.kt +++ /dev/null @@ -1,8 +0,0 @@ -package app.priceguard.data.dto - -data class ProductInfo( - val title: String = "", - val brand: String = "", - val logo: Int = 0, - val price: String = "" -) diff --git a/android/app/src/main/java/app/priceguard/data/dto/ProductVerifyDTO.kt b/android/app/src/main/java/app/priceguard/data/dto/ProductVerifyDTO.kt deleted file mode 100644 index 12c02ef..0000000 --- a/android/app/src/main/java/app/priceguard/data/dto/ProductVerifyDTO.kt +++ /dev/null @@ -1,12 +0,0 @@ -package app.priceguard.data.dto - -import kotlinx.serialization.Serializable - -@Serializable -data class ProductVerifyDTO( - val productName: String? = null, - val productCode: String? = null, - val productPrice: Int? = null, - val shop: String? = null, - val imageUrl: String? = null -) diff --git a/android/app/src/main/java/app/priceguard/data/dto/SignupResponse.kt b/android/app/src/main/java/app/priceguard/data/dto/SignupResponse.kt deleted file mode 100644 index 17c51a7..0000000 --- a/android/app/src/main/java/app/priceguard/data/dto/SignupResponse.kt +++ /dev/null @@ -1,24 +0,0 @@ -package app.priceguard.data.dto - -import kotlinx.serialization.Serializable - -@Serializable -data class SignupResponse( - val statusCode: Int, - val message: String, - val accessToken: String, - val refreshToken: String -) - -data class SignupResult( - val signUpState: SignupState, - val accessToken: String?, - val refreshToken: String? -) - -enum class SignupState { - SUCCESS, - INVALID_PARAMETER, - DUPLICATE_EMAIL, - UNDEFINED_ERROR -} diff --git a/android/app/src/main/java/app/priceguard/data/dto/ProductAddRequest.kt b/android/app/src/main/java/app/priceguard/data/dto/add/ProductAddRequest.kt similarity index 80% rename from android/app/src/main/java/app/priceguard/data/dto/ProductAddRequest.kt rename to android/app/src/main/java/app/priceguard/data/dto/add/ProductAddRequest.kt index e9e9dd8..c8d3513 100644 --- a/android/app/src/main/java/app/priceguard/data/dto/ProductAddRequest.kt +++ b/android/app/src/main/java/app/priceguard/data/dto/add/ProductAddRequest.kt @@ -1,4 +1,4 @@ -package app.priceguard.data.dto +package app.priceguard.data.dto.add import kotlinx.serialization.Serializable diff --git a/android/app/src/main/java/app/priceguard/data/dto/ProductAddResponse.kt b/android/app/src/main/java/app/priceguard/data/dto/add/ProductAddResponse.kt similarity index 79% rename from android/app/src/main/java/app/priceguard/data/dto/ProductAddResponse.kt rename to android/app/src/main/java/app/priceguard/data/dto/add/ProductAddResponse.kt index a97b62f..dda1c29 100644 --- a/android/app/src/main/java/app/priceguard/data/dto/ProductAddResponse.kt +++ b/android/app/src/main/java/app/priceguard/data/dto/add/ProductAddResponse.kt @@ -1,4 +1,4 @@ -package app.priceguard.data.dto +package app.priceguard.data.dto.add import kotlinx.serialization.Serializable diff --git a/android/app/src/main/java/app/priceguard/data/dto/alert/AlertUpdateResponse.kt b/android/app/src/main/java/app/priceguard/data/dto/alert/AlertUpdateResponse.kt new file mode 100644 index 0000000..ef273e7 --- /dev/null +++ b/android/app/src/main/java/app/priceguard/data/dto/alert/AlertUpdateResponse.kt @@ -0,0 +1,9 @@ +package app.priceguard.data.dto.alert + +import kotlinx.serialization.Serializable + +@Serializable +data class AlertUpdateResponse( + val statusCode: Int, + val message: String +) diff --git a/android/app/src/main/java/app/priceguard/data/dto/ProductDeleteResponse.kt b/android/app/src/main/java/app/priceguard/data/dto/delete/ProductDeleteResponse.kt similarity index 78% rename from android/app/src/main/java/app/priceguard/data/dto/ProductDeleteResponse.kt rename to android/app/src/main/java/app/priceguard/data/dto/delete/ProductDeleteResponse.kt index 89281b8..33e76c2 100644 --- a/android/app/src/main/java/app/priceguard/data/dto/ProductDeleteResponse.kt +++ b/android/app/src/main/java/app/priceguard/data/dto/delete/ProductDeleteResponse.kt @@ -1,4 +1,4 @@ -package app.priceguard.data.dto +package app.priceguard.data.dto.delete import kotlinx.serialization.Serializable diff --git a/android/app/src/main/java/app/priceguard/data/dto/ProductResponse.kt b/android/app/src/main/java/app/priceguard/data/dto/detail/ProductResponse.kt similarity index 55% rename from android/app/src/main/java/app/priceguard/data/dto/ProductResponse.kt rename to android/app/src/main/java/app/priceguard/data/dto/detail/ProductResponse.kt index f46006d..b657c5e 100644 --- a/android/app/src/main/java/app/priceguard/data/dto/ProductResponse.kt +++ b/android/app/src/main/java/app/priceguard/data/dto/detail/ProductResponse.kt @@ -1,6 +1,6 @@ -package app.priceguard.data.dto +package app.priceguard.data.dto.detail -import app.priceguard.data.graph.ProductChartData +import app.priceguard.data.dto.PriceDataDTO import kotlinx.serialization.Serializable @Serializable @@ -18,16 +18,3 @@ data class ProductResponse( val price: Int? = null, val priceData: List? = null ) - -data class ProductDetailResult( - val productName: String, - val productCode: String, - val shop: String, - val imageUrl: String, - val rank: Int, - val shopUrl: String, - val targetPrice: Int, - val lowestPrice: Int, - val price: Int, - val priceData: List -) diff --git a/android/app/src/main/java/app/priceguard/data/dto/firebase/FirebaseTokenUpdateRequest.kt b/android/app/src/main/java/app/priceguard/data/dto/firebase/FirebaseTokenUpdateRequest.kt new file mode 100644 index 0000000..a10c04c --- /dev/null +++ b/android/app/src/main/java/app/priceguard/data/dto/firebase/FirebaseTokenUpdateRequest.kt @@ -0,0 +1,8 @@ +package app.priceguard.data.dto.firebase + +import kotlinx.serialization.Serializable + +@Serializable +data class FirebaseTokenUpdateRequest( + val token: String +) diff --git a/android/app/src/main/java/app/priceguard/data/dto/firebase/FirebaseTokenUpdateResponse.kt b/android/app/src/main/java/app/priceguard/data/dto/firebase/FirebaseTokenUpdateResponse.kt new file mode 100644 index 0000000..6f397fc --- /dev/null +++ b/android/app/src/main/java/app/priceguard/data/dto/firebase/FirebaseTokenUpdateResponse.kt @@ -0,0 +1,9 @@ +package app.priceguard.data.dto.firebase + +import kotlinx.serialization.Serializable + +@Serializable +data class FirebaseTokenUpdateResponse( + val statusCode: Int, + val message: String +) diff --git a/android/app/src/main/java/app/priceguard/data/dto/ProductDTO.kt b/android/app/src/main/java/app/priceguard/data/dto/list/ProductDTO.kt similarity index 52% rename from android/app/src/main/java/app/priceguard/data/dto/ProductDTO.kt rename to android/app/src/main/java/app/priceguard/data/dto/list/ProductDTO.kt index 3931a8e..d267903 100644 --- a/android/app/src/main/java/app/priceguard/data/dto/ProductDTO.kt +++ b/android/app/src/main/java/app/priceguard/data/dto/list/ProductDTO.kt @@ -1,6 +1,6 @@ -package app.priceguard.data.dto +package app.priceguard.data.dto.list -import app.priceguard.data.graph.ProductChartData +import app.priceguard.data.dto.PriceDataDTO import kotlinx.serialization.Serializable @Serializable @@ -11,15 +11,6 @@ data class ProductDTO( val imageUrl: String? = null, val targetPrice: Int? = null, val price: Int? = null, + val isAlert: Boolean? = null, val priceData: List? = null ) - -data class ProductData( - val productName: String, - val productCode: String, - val shop: String, - val imageUrl: String, - val targetPrice: Int, - val price: Int, - val priceData: List -) diff --git a/android/app/src/main/java/app/priceguard/data/dto/ProductListResponse.kt b/android/app/src/main/java/app/priceguard/data/dto/list/ProductListResponse.kt similarity index 83% rename from android/app/src/main/java/app/priceguard/data/dto/ProductListResponse.kt rename to android/app/src/main/java/app/priceguard/data/dto/list/ProductListResponse.kt index 54be311..7cda3db 100644 --- a/android/app/src/main/java/app/priceguard/data/dto/ProductListResponse.kt +++ b/android/app/src/main/java/app/priceguard/data/dto/list/ProductListResponse.kt @@ -1,4 +1,4 @@ -package app.priceguard.data.dto +package app.priceguard.data.dto.list import kotlinx.serialization.Serializable diff --git a/android/app/src/main/java/app/priceguard/data/dto/LoginRequest.kt b/android/app/src/main/java/app/priceguard/data/dto/login/LoginRequest.kt similarity index 77% rename from android/app/src/main/java/app/priceguard/data/dto/LoginRequest.kt rename to android/app/src/main/java/app/priceguard/data/dto/login/LoginRequest.kt index 2f9cc15..e63ebdd 100644 --- a/android/app/src/main/java/app/priceguard/data/dto/LoginRequest.kt +++ b/android/app/src/main/java/app/priceguard/data/dto/login/LoginRequest.kt @@ -1,4 +1,4 @@ -package app.priceguard.data.dto +package app.priceguard.data.dto.login import kotlinx.serialization.Serializable diff --git a/android/app/src/main/java/app/priceguard/data/dto/login/LoginResponse.kt b/android/app/src/main/java/app/priceguard/data/dto/login/LoginResponse.kt new file mode 100644 index 0000000..c7c7e90 --- /dev/null +++ b/android/app/src/main/java/app/priceguard/data/dto/login/LoginResponse.kt @@ -0,0 +1,11 @@ +package app.priceguard.data.dto.login + +import kotlinx.serialization.Serializable + +@Serializable +data class LoginResponse( + val statusCode: Int, + val message: String, + val accessToken: String? = null, + val refreshToken: String? = null +) diff --git a/android/app/src/main/java/app/priceguard/data/dto/PricePatchRequest.kt b/android/app/src/main/java/app/priceguard/data/dto/patch/PricePatchRequest.kt similarity index 79% rename from android/app/src/main/java/app/priceguard/data/dto/PricePatchRequest.kt rename to android/app/src/main/java/app/priceguard/data/dto/patch/PricePatchRequest.kt index 7765575..0d07b26 100644 --- a/android/app/src/main/java/app/priceguard/data/dto/PricePatchRequest.kt +++ b/android/app/src/main/java/app/priceguard/data/dto/patch/PricePatchRequest.kt @@ -1,4 +1,4 @@ -package app.priceguard.data.dto +package app.priceguard.data.dto.patch import kotlinx.serialization.Serializable diff --git a/android/app/src/main/java/app/priceguard/data/dto/PricePatchResponse.kt b/android/app/src/main/java/app/priceguard/data/dto/patch/PricePatchResponse.kt similarity index 78% rename from android/app/src/main/java/app/priceguard/data/dto/PricePatchResponse.kt rename to android/app/src/main/java/app/priceguard/data/dto/patch/PricePatchResponse.kt index dc162bf..8b95b35 100644 --- a/android/app/src/main/java/app/priceguard/data/dto/PricePatchResponse.kt +++ b/android/app/src/main/java/app/priceguard/data/dto/patch/PricePatchResponse.kt @@ -1,4 +1,4 @@ -package app.priceguard.data.dto +package app.priceguard.data.dto.patch import kotlinx.serialization.Serializable diff --git a/android/app/src/main/java/app/priceguard/data/dto/RecommendProductDTO.kt b/android/app/src/main/java/app/priceguard/data/dto/recommend/RecommendProductDTO.kt similarity index 52% rename from android/app/src/main/java/app/priceguard/data/dto/RecommendProductDTO.kt rename to android/app/src/main/java/app/priceguard/data/dto/recommend/RecommendProductDTO.kt index 9b4ac22..7015641 100644 --- a/android/app/src/main/java/app/priceguard/data/dto/RecommendProductDTO.kt +++ b/android/app/src/main/java/app/priceguard/data/dto/recommend/RecommendProductDTO.kt @@ -1,6 +1,6 @@ -package app.priceguard.data.dto +package app.priceguard.data.dto.recommend -import app.priceguard.data.graph.ProductChartData +import app.priceguard.data.dto.PriceDataDTO import kotlinx.serialization.Serializable @Serializable @@ -13,13 +13,3 @@ data class RecommendProductDTO( val rank: Int? = null, val priceData: List? = null ) - -data class RecommendProductData( - val productName: String, - val productCode: String, - val shop: String, - val imageUrl: String, - val price: Int, - val rank: Int, - val priceData: List -) diff --git a/android/app/src/main/java/app/priceguard/data/dto/RecommendProductResponse.kt b/android/app/src/main/java/app/priceguard/data/dto/recommend/RecommendProductResponse.kt similarity index 82% rename from android/app/src/main/java/app/priceguard/data/dto/RecommendProductResponse.kt rename to android/app/src/main/java/app/priceguard/data/dto/recommend/RecommendProductResponse.kt index 063af2b..716a0f9 100644 --- a/android/app/src/main/java/app/priceguard/data/dto/RecommendProductResponse.kt +++ b/android/app/src/main/java/app/priceguard/data/dto/recommend/RecommendProductResponse.kt @@ -1,4 +1,4 @@ -package app.priceguard.data.dto +package app.priceguard.data.dto.recommend import kotlinx.serialization.Serializable diff --git a/android/app/src/main/java/app/priceguard/data/dto/RenewResponse.kt b/android/app/src/main/java/app/priceguard/data/dto/renew/RenewResponse.kt similarity index 61% rename from android/app/src/main/java/app/priceguard/data/dto/RenewResponse.kt rename to android/app/src/main/java/app/priceguard/data/dto/renew/RenewResponse.kt index a50f0f7..e501dd9 100644 --- a/android/app/src/main/java/app/priceguard/data/dto/RenewResponse.kt +++ b/android/app/src/main/java/app/priceguard/data/dto/renew/RenewResponse.kt @@ -1,4 +1,4 @@ -package app.priceguard.data.dto +package app.priceguard.data.dto.renew import kotlinx.serialization.Serializable @@ -9,10 +9,3 @@ data class RenewResponse( val accessToken: String, val refreshToken: String ) - -enum class RenewResult { - SUCCESS, - EXPIRED, - UNAUTHORIZED, - UNKNOWN_ERROR -} diff --git a/android/app/src/main/java/app/priceguard/data/dto/SignupRequest.kt b/android/app/src/main/java/app/priceguard/data/dto/signup/SignupRequest.kt similarity index 80% rename from android/app/src/main/java/app/priceguard/data/dto/SignupRequest.kt rename to android/app/src/main/java/app/priceguard/data/dto/signup/SignupRequest.kt index 16dfdce..6a28e66 100644 --- a/android/app/src/main/java/app/priceguard/data/dto/SignupRequest.kt +++ b/android/app/src/main/java/app/priceguard/data/dto/signup/SignupRequest.kt @@ -1,4 +1,4 @@ -package app.priceguard.data.dto +package app.priceguard.data.dto.signup import kotlinx.serialization.Serializable diff --git a/android/app/src/main/java/app/priceguard/data/dto/signup/SignupResponse.kt b/android/app/src/main/java/app/priceguard/data/dto/signup/SignupResponse.kt new file mode 100644 index 0000000..106c246 --- /dev/null +++ b/android/app/src/main/java/app/priceguard/data/dto/signup/SignupResponse.kt @@ -0,0 +1,11 @@ +package app.priceguard.data.dto.signup + +import kotlinx.serialization.Serializable + +@Serializable +data class SignupResponse( + val statusCode: Int, + val message: String, + val accessToken: String? = null, + val refreshToken: String? = null +) diff --git a/android/app/src/main/java/app/priceguard/data/dto/ProductVerifyRequest.kt b/android/app/src/main/java/app/priceguard/data/dto/verify/ProductVerifyRequest.kt similarity index 75% rename from android/app/src/main/java/app/priceguard/data/dto/ProductVerifyRequest.kt rename to android/app/src/main/java/app/priceguard/data/dto/verify/ProductVerifyRequest.kt index 63cb97f..832565c 100644 --- a/android/app/src/main/java/app/priceguard/data/dto/ProductVerifyRequest.kt +++ b/android/app/src/main/java/app/priceguard/data/dto/verify/ProductVerifyRequest.kt @@ -1,4 +1,4 @@ -package app.priceguard.data.dto +package app.priceguard.data.dto.verify import kotlinx.serialization.Serializable diff --git a/android/app/src/main/java/app/priceguard/data/dto/ProductVerifyResponse.kt b/android/app/src/main/java/app/priceguard/data/dto/verify/ProductVerifyResponse.kt similarity index 89% rename from android/app/src/main/java/app/priceguard/data/dto/ProductVerifyResponse.kt rename to android/app/src/main/java/app/priceguard/data/dto/verify/ProductVerifyResponse.kt index 5eb5e51..230864a 100644 --- a/android/app/src/main/java/app/priceguard/data/dto/ProductVerifyResponse.kt +++ b/android/app/src/main/java/app/priceguard/data/dto/verify/ProductVerifyResponse.kt @@ -1,4 +1,4 @@ -package app.priceguard.data.dto +package app.priceguard.data.dto.verify import kotlinx.serialization.Serializable diff --git a/android/app/src/main/java/app/priceguard/data/graph/GraphDataConverter.kt b/android/app/src/main/java/app/priceguard/data/graph/GraphDataConverter.kt deleted file mode 100644 index 125c39d..0000000 --- a/android/app/src/main/java/app/priceguard/data/graph/GraphDataConverter.kt +++ /dev/null @@ -1,24 +0,0 @@ -package app.priceguard.data.graph - -import app.priceguard.data.dto.PriceDataDTO - -class GraphDataConverter { - - // TODO: κΈ°κ°„λ³„λ‘œ 데이터 필터링을 톡해 ν•΄λ‹Ή 기간에 λ°œμƒν•œ 가격 λ³€λ™λ§Œ μΆ”μ ν•˜λ„λ‘ ꡬ쑰 λ³€κ²½. - fun toDataset(priceData: List?): List { - priceData ?: return listOf() - if (priceData.isEmpty()) { - return listOf() - } - val dataList = mutableListOf() - priceData.forEach { dto -> - dto.time ?: return@forEach - dto.price ?: return@forEach - dto.isSoldOut ?: return@forEach - dataList.add(ProductChartData(dto.time / 1000, dto.price.toFloat(), dto.isSoldOut.not())) - } - val currentTime = (System.currentTimeMillis() / 1000).toFloat() - dataList.add(ProductChartData(currentTime, dataList.last().y, dataList.last().valid)) - return dataList.toList().sortedBy { it.x }.filter { it.x <= currentTime } - } -} diff --git a/android/app/src/main/java/app/priceguard/data/graph/ProductChartDataset.kt b/android/app/src/main/java/app/priceguard/data/graph/ProductChartDataset.kt index b8411f9..79bf41b 100644 --- a/android/app/src/main/java/app/priceguard/data/graph/ProductChartDataset.kt +++ b/android/app/src/main/java/app/priceguard/data/graph/ProductChartDataset.kt @@ -1,6 +1,5 @@ package app.priceguard.data.graph -import app.priceguard.materialchart.data.ChartData import app.priceguard.materialchart.data.ChartDataset import app.priceguard.materialchart.data.GraphMode import app.priceguard.materialchart.data.GridLine @@ -10,6 +9,8 @@ data class ProductChartDataset( override val showYAxis: Boolean, override val isInteractive: Boolean, override val graphMode: GraphMode, - override val data: List, + override val xLabel: String, + override val yLabel: String, + override val data: List, override val gridLines: List ) : ChartDataset diff --git a/android/app/src/main/java/app/priceguard/data/network/AuthAPI.kt b/android/app/src/main/java/app/priceguard/data/network/AuthAPI.kt index 49518a9..b65be19 100644 --- a/android/app/src/main/java/app/priceguard/data/network/AuthAPI.kt +++ b/android/app/src/main/java/app/priceguard/data/network/AuthAPI.kt @@ -1,6 +1,6 @@ package app.priceguard.data.network -import app.priceguard.data.dto.RenewResponse +import app.priceguard.data.dto.renew.RenewResponse import retrofit2.Response import retrofit2.http.GET import retrofit2.http.Header diff --git a/android/app/src/main/java/app/priceguard/data/network/ProductAPI.kt b/android/app/src/main/java/app/priceguard/data/network/ProductAPI.kt index 31459ef..1f62101 100644 --- a/android/app/src/main/java/app/priceguard/data/network/ProductAPI.kt +++ b/android/app/src/main/java/app/priceguard/data/network/ProductAPI.kt @@ -1,15 +1,16 @@ package app.priceguard.data.network -import app.priceguard.data.dto.PricePatchRequest -import app.priceguard.data.dto.PricePatchResponse -import app.priceguard.data.dto.ProductAddRequest -import app.priceguard.data.dto.ProductAddResponse -import app.priceguard.data.dto.ProductDeleteResponse -import app.priceguard.data.dto.ProductListResponse -import app.priceguard.data.dto.ProductResponse -import app.priceguard.data.dto.ProductVerifyRequest -import app.priceguard.data.dto.ProductVerifyResponse -import app.priceguard.data.dto.RecommendProductResponse +import app.priceguard.data.dto.add.ProductAddRequest +import app.priceguard.data.dto.add.ProductAddResponse +import app.priceguard.data.dto.alert.AlertUpdateResponse +import app.priceguard.data.dto.delete.ProductDeleteResponse +import app.priceguard.data.dto.detail.ProductResponse +import app.priceguard.data.dto.list.ProductListResponse +import app.priceguard.data.dto.patch.PricePatchRequest +import app.priceguard.data.dto.patch.PricePatchResponse +import app.priceguard.data.dto.recommend.RecommendProductResponse +import app.priceguard.data.dto.verify.ProductVerifyRequest +import app.priceguard.data.dto.verify.ProductVerifyResponse import retrofit2.Response import retrofit2.http.Body import retrofit2.http.DELETE @@ -50,4 +51,9 @@ interface ProductAPI { suspend fun updateTargetPrice( @Body pricePatchRequest: PricePatchRequest ): Response + + @PATCH("alert/{productCode}") + suspend fun updateAlert( + @Path("productCode") productCode: String + ): Response } diff --git a/android/app/src/main/java/app/priceguard/data/network/ProductRepositoryResult.kt b/android/app/src/main/java/app/priceguard/data/network/ProductRepositoryResult.kt deleted file mode 100644 index 60710f5..0000000 --- a/android/app/src/main/java/app/priceguard/data/network/ProductRepositoryResult.kt +++ /dev/null @@ -1,10 +0,0 @@ -package app.priceguard.data.network - -import app.priceguard.data.dto.ProductErrorState - -sealed class ProductRepositoryResult { - - data class Success(val data: T) : ProductRepositoryResult() - - data class Error(val productErrorState: ProductErrorState) : ProductRepositoryResult() -} diff --git a/android/app/src/main/java/app/priceguard/data/network/RequestInterceptor.kt b/android/app/src/main/java/app/priceguard/data/network/RequestInterceptor.kt index 443ec0c..0ab031c 100644 --- a/android/app/src/main/java/app/priceguard/data/network/RequestInterceptor.kt +++ b/android/app/src/main/java/app/priceguard/data/network/RequestInterceptor.kt @@ -1,6 +1,6 @@ package app.priceguard.data.network -import app.priceguard.data.repository.TokenRepository +import app.priceguard.data.repository.token.TokenRepository import javax.inject.Inject import kotlinx.coroutines.runBlocking import okhttp3.Interceptor diff --git a/android/app/src/main/java/app/priceguard/data/network/UserAPI.kt b/android/app/src/main/java/app/priceguard/data/network/UserAPI.kt index 706155f..270b0a4 100644 --- a/android/app/src/main/java/app/priceguard/data/network/UserAPI.kt +++ b/android/app/src/main/java/app/priceguard/data/network/UserAPI.kt @@ -1,12 +1,16 @@ package app.priceguard.data.network -import app.priceguard.data.dto.LoginRequest -import app.priceguard.data.dto.LoginResponse -import app.priceguard.data.dto.SignupRequest -import app.priceguard.data.dto.SignupResponse +import app.priceguard.data.dto.firebase.FirebaseTokenUpdateRequest +import app.priceguard.data.dto.firebase.FirebaseTokenUpdateResponse +import app.priceguard.data.dto.login.LoginRequest +import app.priceguard.data.dto.login.LoginResponse +import app.priceguard.data.dto.signup.SignupRequest +import app.priceguard.data.dto.signup.SignupResponse import retrofit2.Response import retrofit2.http.Body +import retrofit2.http.Header import retrofit2.http.POST +import retrofit2.http.PUT interface UserAPI { @@ -19,4 +23,10 @@ interface UserAPI { suspend fun register( @Body request: SignupRequest ): Response + + @PUT("firebase/token") + suspend fun updateFirebaseToken( + @Header("Authorization") authToken: String, + @Body request: FirebaseTokenUpdateRequest + ): Response } diff --git a/android/app/src/main/java/app/priceguard/data/network/APIResult.kt b/android/app/src/main/java/app/priceguard/data/repository/APIResult.kt similarity index 94% rename from android/app/src/main/java/app/priceguard/data/network/APIResult.kt rename to android/app/src/main/java/app/priceguard/data/repository/APIResult.kt index 4055b05..4839379 100644 --- a/android/app/src/main/java/app/priceguard/data/network/APIResult.kt +++ b/android/app/src/main/java/app/priceguard/data/repository/APIResult.kt @@ -1,4 +1,4 @@ -package app.priceguard.data.network +package app.priceguard.data.repository import retrofit2.Response diff --git a/android/app/src/main/java/app/priceguard/data/repository/ProductRepository.kt b/android/app/src/main/java/app/priceguard/data/repository/ProductRepository.kt deleted file mode 100644 index 7078197..0000000 --- a/android/app/src/main/java/app/priceguard/data/repository/ProductRepository.kt +++ /dev/null @@ -1,29 +0,0 @@ -package app.priceguard.data.repository - -import app.priceguard.data.dto.PricePatchRequest -import app.priceguard.data.dto.PricePatchResponse -import app.priceguard.data.dto.ProductAddRequest -import app.priceguard.data.dto.ProductAddResponse -import app.priceguard.data.dto.ProductData -import app.priceguard.data.dto.ProductDetailResult -import app.priceguard.data.dto.ProductVerifyDTO -import app.priceguard.data.dto.ProductVerifyRequest -import app.priceguard.data.dto.RecommendProductData -import app.priceguard.data.network.ProductRepositoryResult - -interface ProductRepository { - - suspend fun verifyLink(productUrl: ProductVerifyRequest, isRenewed: Boolean = false): ProductRepositoryResult - - suspend fun addProduct(productAddRequest: ProductAddRequest, isRenewed: Boolean = false): ProductRepositoryResult - - suspend fun getProductList(isRenewed: Boolean = false): ProductRepositoryResult> - - suspend fun getRecommendedProductList(isRenewed: Boolean = false): ProductRepositoryResult> - - suspend fun getProductDetail(productCode: String, isRenewed: Boolean = false): ProductRepositoryResult - - suspend fun deleteProduct(productCode: String, isRenewed: Boolean = false): ProductRepositoryResult - - suspend fun updateTargetPrice(pricePatchRequest: PricePatchRequest, isRenewed: Boolean = false): ProductRepositoryResult -} diff --git a/android/app/src/main/java/app/priceguard/data/repository/RepositoryResult.kt b/android/app/src/main/java/app/priceguard/data/repository/RepositoryResult.kt new file mode 100644 index 0000000..ed0822d --- /dev/null +++ b/android/app/src/main/java/app/priceguard/data/repository/RepositoryResult.kt @@ -0,0 +1,8 @@ +package app.priceguard.data.repository + +sealed class RepositoryResult { + + data class Success(val data: T) : RepositoryResult() + + data class Error(val errorState: S) : RepositoryResult() +} diff --git a/android/app/src/main/java/app/priceguard/data/repository/TokenRepository.kt b/android/app/src/main/java/app/priceguard/data/repository/TokenRepository.kt deleted file mode 100644 index 4d34ea0..0000000 --- a/android/app/src/main/java/app/priceguard/data/repository/TokenRepository.kt +++ /dev/null @@ -1,13 +0,0 @@ -package app.priceguard.data.repository - -import app.priceguard.data.dto.RenewResult -import app.priceguard.data.dto.UserDataResult - -interface TokenRepository { - suspend fun storeTokens(accessToken: String, refreshToken: String) - suspend fun getAccessToken(): String? - suspend fun getRefreshToken(): String? - suspend fun getUserData(): UserDataResult - suspend fun renewTokens(refreshToken: String): RenewResult - suspend fun clearTokens() -} diff --git a/android/app/src/main/java/app/priceguard/data/repository/TokenRepositoryImpl.kt b/android/app/src/main/java/app/priceguard/data/repository/TokenRepositoryImpl.kt deleted file mode 100644 index 4ed4969..0000000 --- a/android/app/src/main/java/app/priceguard/data/repository/TokenRepositoryImpl.kt +++ /dev/null @@ -1,75 +0,0 @@ -package app.priceguard.data.repository - -import android.util.Log -import app.priceguard.data.datastore.TokenDataSource -import app.priceguard.data.dto.RenewResult -import app.priceguard.data.dto.UserDataDTO -import app.priceguard.data.dto.UserDataResult -import app.priceguard.data.network.APIResult -import app.priceguard.data.network.AuthAPI -import app.priceguard.data.network.getApiResult -import java.util.* -import javax.inject.Inject -import kotlinx.serialization.json.Json - -class TokenRepositoryImpl @Inject constructor( - private val tokenDataSource: TokenDataSource, - private val authAPI: AuthAPI -) : TokenRepository { - - override suspend fun storeTokens(accessToken: String, refreshToken: String) { - tokenDataSource.saveTokens(accessToken, refreshToken) - } - - override suspend fun getAccessToken(): String? { - return tokenDataSource.getAccessToken() - } - - override suspend fun getRefreshToken(): String? { - return tokenDataSource.getRefreshToken() - } - - override suspend fun getUserData(): UserDataResult { - val accessToken = tokenDataSource.getAccessToken() ?: return UserDataResult("", "") - val parts = accessToken.split(".") - return try { - val charset = charset("UTF-8") - val payload = Json.decodeFromString( - String(Base64.getUrlDecoder().decode(parts[1].toByteArray(charset)), charset) - ) - UserDataResult(payload.email, payload.name) - } catch (e: Exception) { - Log.e("Data Not Found", "Error parsing JWT: $e") - UserDataResult("", "") - } - } - - override suspend fun renewTokens(refreshToken: String): RenewResult { - when (val response = getApiResult { authAPI.renewTokens("Bearer $refreshToken") }) { - is APIResult.Success -> { - storeTokens(response.data.accessToken, response.data.refreshToken) - return RenewResult.SUCCESS - } - - is APIResult.Error -> { - return when (response.code) { - 401 -> { - RenewResult.UNAUTHORIZED - } - - 410 -> { - RenewResult.EXPIRED - } - - else -> { - RenewResult.UNKNOWN_ERROR - } - } - } - } - } - - override suspend fun clearTokens() { - tokenDataSource.clearTokens() - } -} diff --git a/android/app/src/main/java/app/priceguard/data/repository/UserRepository.kt b/android/app/src/main/java/app/priceguard/data/repository/UserRepository.kt deleted file mode 100644 index ec19abd..0000000 --- a/android/app/src/main/java/app/priceguard/data/repository/UserRepository.kt +++ /dev/null @@ -1,11 +0,0 @@ -package app.priceguard.data.repository - -import app.priceguard.data.dto.LoginResult -import app.priceguard.data.dto.SignupResult - -interface UserRepository { - - suspend fun signUp(email: String, userName: String, password: String): SignupResult - - suspend fun login(email: String, password: String): LoginResult -} diff --git a/android/app/src/main/java/app/priceguard/data/repository/UserRepositoryImpl.kt b/android/app/src/main/java/app/priceguard/data/repository/UserRepositoryImpl.kt deleted file mode 100644 index 7fd9a41..0000000 --- a/android/app/src/main/java/app/priceguard/data/repository/UserRepositoryImpl.kt +++ /dev/null @@ -1,65 +0,0 @@ -package app.priceguard.data.repository - -import app.priceguard.data.dto.LoginRequest -import app.priceguard.data.dto.LoginResult -import app.priceguard.data.dto.LoginState -import app.priceguard.data.dto.SignupRequest -import app.priceguard.data.dto.SignupResult -import app.priceguard.data.dto.SignupState -import app.priceguard.data.network.APIResult -import app.priceguard.data.network.UserAPI -import app.priceguard.data.network.getApiResult -import javax.inject.Inject - -class UserRepositoryImpl @Inject constructor(private val userAPI: UserAPI) : UserRepository { - - override suspend fun signUp(email: String, userName: String, password: String): SignupResult { - val response = getApiResult { - userAPI.register(SignupRequest(email, userName, password)) - } - when (response) { - is APIResult.Success -> { - return SignupResult(SignupState.SUCCESS, response.data.accessToken, response.data.refreshToken) - } - - is APIResult.Error -> { - return when (response.code) { - 400 -> { - SignupResult(SignupState.INVALID_PARAMETER, null, null) - } - - 409 -> { - SignupResult(SignupState.DUPLICATE_EMAIL, null, null) - } - - else -> { - SignupResult(SignupState.UNDEFINED_ERROR, null, null) - } - } - } - } - } - - override suspend fun login(email: String, password: String): LoginResult { - val response = getApiResult { - userAPI.login(LoginRequest(email, password)) - } - when (response) { - is APIResult.Success -> { - return LoginResult(LoginState.SUCCESS, response.data.accessToken, response.data.refreshToken) - } - - is APIResult.Error -> { - return when (response.code) { - 400 -> { - LoginResult(LoginState.INVALID_PARAMETER, null, null) - } - - else -> { - LoginResult(LoginState.UNDEFINED_ERROR, null, null) - } - } - } - } - } -} diff --git a/android/app/src/main/java/app/priceguard/data/repository/auth/AuthErrorState.kt b/android/app/src/main/java/app/priceguard/data/repository/auth/AuthErrorState.kt new file mode 100644 index 0000000..391b82c --- /dev/null +++ b/android/app/src/main/java/app/priceguard/data/repository/auth/AuthErrorState.kt @@ -0,0 +1,7 @@ +package app.priceguard.data.repository.auth + +enum class AuthErrorState { + INVALID_REQUEST, + DUPLICATED_EMAIL, + UNDEFINED_ERROR +} diff --git a/android/app/src/main/java/app/priceguard/data/repository/auth/AuthRepository.kt b/android/app/src/main/java/app/priceguard/data/repository/auth/AuthRepository.kt new file mode 100644 index 0000000..f75b37b --- /dev/null +++ b/android/app/src/main/java/app/priceguard/data/repository/auth/AuthRepository.kt @@ -0,0 +1,12 @@ +package app.priceguard.data.repository.auth + +import app.priceguard.data.repository.RepositoryResult +import app.priceguard.ui.data.LoginResult +import app.priceguard.ui.data.SignupResult + +interface AuthRepository { + + suspend fun signUp(email: String, userName: String, password: String): RepositoryResult + + suspend fun login(email: String, password: String): RepositoryResult +} diff --git a/android/app/src/main/java/app/priceguard/data/repository/auth/AuthRepositoryImpl.kt b/android/app/src/main/java/app/priceguard/data/repository/auth/AuthRepositoryImpl.kt new file mode 100644 index 0000000..d007435 --- /dev/null +++ b/android/app/src/main/java/app/priceguard/data/repository/auth/AuthRepositoryImpl.kt @@ -0,0 +1,76 @@ +package app.priceguard.data.repository.auth + +import app.priceguard.data.dto.login.LoginRequest +import app.priceguard.data.dto.signup.SignupRequest +import app.priceguard.data.network.UserAPI +import app.priceguard.data.repository.APIResult +import app.priceguard.data.repository.RepositoryResult +import app.priceguard.data.repository.getApiResult +import app.priceguard.ui.data.LoginResult +import app.priceguard.ui.data.SignupResult +import javax.inject.Inject + +class AuthRepositoryImpl @Inject constructor(private val userAPI: UserAPI) : AuthRepository { + + private fun handleError( + code: Int? + ): RepositoryResult { + return when (code) { + 400 -> { + RepositoryResult.Error(AuthErrorState.INVALID_REQUEST) + } + + 409 -> { + RepositoryResult.Error(AuthErrorState.DUPLICATED_EMAIL) + } + + else -> { + RepositoryResult.Error(AuthErrorState.UNDEFINED_ERROR) + } + } + } + + override suspend fun signUp( + email: String, + userName: String, + password: String + ): RepositoryResult { + val response = getApiResult { + userAPI.register(SignupRequest(email, userName, password)) + } + return when (response) { + is APIResult.Success -> { + RepositoryResult.Success( + SignupResult( + response.data.accessToken ?: "", + response.data.refreshToken ?: "" + ) + ) + } + + is APIResult.Error -> { + handleError(response.code) + } + } + } + + override suspend fun login(email: String, password: String): RepositoryResult { + val response = getApiResult { + userAPI.login(LoginRequest(email, password)) + } + return when (response) { + is APIResult.Success -> { + RepositoryResult.Success( + LoginResult( + response.data.accessToken ?: "", + response.data.refreshToken ?: "" + ) + ) + } + + is APIResult.Error -> { + handleError(response.code) + } + } + } +} diff --git a/android/app/src/main/java/app/priceguard/data/dto/ProductErrorState.kt b/android/app/src/main/java/app/priceguard/data/repository/product/ProductErrorState.kt similarity index 72% rename from android/app/src/main/java/app/priceguard/data/dto/ProductErrorState.kt rename to android/app/src/main/java/app/priceguard/data/repository/product/ProductErrorState.kt index 05d582b..8dd2c74 100644 --- a/android/app/src/main/java/app/priceguard/data/dto/ProductErrorState.kt +++ b/android/app/src/main/java/app/priceguard/data/repository/product/ProductErrorState.kt @@ -1,4 +1,4 @@ -package app.priceguard.data.dto +package app.priceguard.data.repository.product enum class ProductErrorState { PERMISSION_DENIED, diff --git a/android/app/src/main/java/app/priceguard/data/repository/product/ProductRepository.kt b/android/app/src/main/java/app/priceguard/data/repository/product/ProductRepository.kt new file mode 100644 index 0000000..b0a933a --- /dev/null +++ b/android/app/src/main/java/app/priceguard/data/repository/product/ProductRepository.kt @@ -0,0 +1,28 @@ +package app.priceguard.data.repository.product + +import app.priceguard.data.repository.RepositoryResult +import app.priceguard.ui.data.PricePatchResult +import app.priceguard.ui.data.ProductAddResult +import app.priceguard.ui.data.ProductData +import app.priceguard.ui.data.ProductDetailResult +import app.priceguard.ui.data.ProductVerifyResult +import app.priceguard.ui.data.RecommendProductData + +interface ProductRepository { + + suspend fun verifyLink(productUrl: String, isRenewed: Boolean = false): RepositoryResult + + suspend fun addProduct(productCode: String, targetPrice: Int, isRenewed: Boolean = false): RepositoryResult + + suspend fun getProductList(isRenewed: Boolean = false): RepositoryResult, ProductErrorState> + + suspend fun getRecommendedProductList(isRenewed: Boolean = false): RepositoryResult, ProductErrorState> + + suspend fun getProductDetail(productCode: String, isRenewed: Boolean = false): RepositoryResult + + suspend fun deleteProduct(productCode: String, isRenewed: Boolean = false): RepositoryResult + + suspend fun updateTargetPrice(productCode: String, targetPrice: Int, isRenewed: Boolean = false): RepositoryResult + + suspend fun switchAlert(productCode: String, isRenewed: Boolean = false): RepositoryResult +} diff --git a/android/app/src/main/java/app/priceguard/data/repository/ProductRepositoryImpl.kt b/android/app/src/main/java/app/priceguard/data/repository/product/ProductRepositoryImpl.kt similarity index 60% rename from android/app/src/main/java/app/priceguard/data/repository/ProductRepositoryImpl.kt rename to android/app/src/main/java/app/priceguard/data/repository/product/ProductRepositoryImpl.kt index 0b269c4..acc3473 100644 --- a/android/app/src/main/java/app/priceguard/data/repository/ProductRepositoryImpl.kt +++ b/android/app/src/main/java/app/priceguard/data/repository/product/ProductRepositoryImpl.kt @@ -1,21 +1,20 @@ -package app.priceguard.data.repository +package app.priceguard.data.repository.product -import app.priceguard.data.dto.PricePatchRequest -import app.priceguard.data.dto.PricePatchResponse -import app.priceguard.data.dto.ProductAddRequest -import app.priceguard.data.dto.ProductAddResponse -import app.priceguard.data.dto.ProductData -import app.priceguard.data.dto.ProductDetailResult -import app.priceguard.data.dto.ProductErrorState -import app.priceguard.data.dto.ProductVerifyDTO -import app.priceguard.data.dto.ProductVerifyRequest -import app.priceguard.data.dto.RecommendProductData -import app.priceguard.data.dto.RenewResult -import app.priceguard.data.graph.GraphDataConverter -import app.priceguard.data.network.APIResult +import app.priceguard.data.GraphDataConverter +import app.priceguard.data.dto.add.ProductAddRequest +import app.priceguard.data.dto.patch.PricePatchRequest +import app.priceguard.data.dto.verify.ProductVerifyRequest import app.priceguard.data.network.ProductAPI -import app.priceguard.data.network.ProductRepositoryResult -import app.priceguard.data.network.getApiResult +import app.priceguard.data.repository.APIResult +import app.priceguard.data.repository.RepositoryResult +import app.priceguard.data.repository.getApiResult +import app.priceguard.data.repository.token.TokenRepository +import app.priceguard.ui.data.PricePatchResult +import app.priceguard.ui.data.ProductAddResult +import app.priceguard.ui.data.ProductData +import app.priceguard.ui.data.ProductDetailResult +import app.priceguard.ui.data.ProductVerifyResult +import app.priceguard.ui.data.RecommendProductData import javax.inject.Inject class ProductRepositoryImpl @Inject constructor( @@ -26,69 +25,73 @@ class ProductRepositoryImpl @Inject constructor( private suspend fun renew(): Boolean { val refreshToken = tokenRepository.getRefreshToken() ?: return false - val renewResult = tokenRepository.renewTokens(refreshToken) - if (renewResult != RenewResult.SUCCESS) { - return false + return when (tokenRepository.renewTokens(refreshToken)) { + is RepositoryResult.Success -> { + true + } + + is RepositoryResult.Error -> { + false + } } - return true } private suspend fun handleError( code: Int?, isRenewed: Boolean, - repoFun: suspend () -> ProductRepositoryResult - ): ProductRepositoryResult { + repoFun: suspend () -> RepositoryResult + ): RepositoryResult { return when (code) { 400 -> { - ProductRepositoryResult.Error(ProductErrorState.INVALID_REQUEST) + RepositoryResult.Error(ProductErrorState.INVALID_REQUEST) } 401 -> { - ProductRepositoryResult.Error(ProductErrorState.PERMISSION_DENIED) + RepositoryResult.Error(ProductErrorState.PERMISSION_DENIED) } 404 -> { - ProductRepositoryResult.Error(ProductErrorState.NOT_FOUND) + RepositoryResult.Error(ProductErrorState.NOT_FOUND) } 409 -> { - ProductRepositoryResult.Error(ProductErrorState.EXIST) + RepositoryResult.Error(ProductErrorState.EXIST) } 410 -> { if (isRenewed) { - ProductRepositoryResult.Error(ProductErrorState.PERMISSION_DENIED) + RepositoryResult.Error(ProductErrorState.PERMISSION_DENIED) } else { if (renew()) { repoFun.invoke() } else { - ProductRepositoryResult.Error(ProductErrorState.PERMISSION_DENIED) + RepositoryResult.Error(ProductErrorState.PERMISSION_DENIED) } } } else -> { - ProductRepositoryResult.Error(ProductErrorState.UNDEFINED_ERROR) + RepositoryResult.Error(ProductErrorState.UNDEFINED_ERROR) } } } override suspend fun verifyLink( - productUrl: ProductVerifyRequest, + productUrl: String, isRenewed: Boolean - ): ProductRepositoryResult { + ): RepositoryResult { val response = getApiResult { - productAPI.verifyLink(productUrl) + productAPI.verifyLink(ProductVerifyRequest(productUrl)) } return when (response) { is APIResult.Success -> { - ProductRepositoryResult.Success( - ProductVerifyDTO( - response.data.productName, - response.data.productCode, - response.data.productPrice, - response.data.shop, - response.data.imageUrl + RepositoryResult.Success( + ProductVerifyResult( + response.data.productName ?: "", + response.data.productCode ?: "", + response.data.productPrice ?: 0, + response.data.shop ?: "", + response.data.imageUrl ?: "" ) ) } @@ -102,16 +105,17 @@ class ProductRepositoryImpl @Inject constructor( } override suspend fun addProduct( - productAddRequest: ProductAddRequest, + productCode: String, + targetPrice: Int, isRenewed: Boolean - ): ProductRepositoryResult { + ): RepositoryResult { val response = getApiResult { - productAPI.addProduct(productAddRequest) + productAPI.addProduct(ProductAddRequest(productCode, targetPrice)) } return when (response) { is APIResult.Success -> { - ProductRepositoryResult.Success( - ProductAddResponse( + RepositoryResult.Success( + ProductAddResult( response.data.statusCode, response.data.message ) @@ -120,19 +124,19 @@ class ProductRepositoryImpl @Inject constructor( is APIResult.Error -> { handleError(response.code, isRenewed) { - addProduct(productAddRequest, true) + addProduct(productCode, targetPrice, true) } } } } - override suspend fun getProductList(isRenewed: Boolean): ProductRepositoryResult> { + override suspend fun getProductList(isRenewed: Boolean): RepositoryResult, ProductErrorState> { val response = getApiResult { productAPI.getProductList() } return when (response) { is APIResult.Success -> { - ProductRepositoryResult.Success( + RepositoryResult.Success( response.data.trackingList?.map { dto -> ProductData( dto.productName ?: "", @@ -141,6 +145,7 @@ class ProductRepositoryImpl @Inject constructor( dto.imageUrl ?: "", dto.targetPrice ?: 0, dto.price ?: 0, + dto.isAlert ?: true, GraphDataConverter().toDataset(dto.priceData) ) } ?: listOf() @@ -155,13 +160,13 @@ class ProductRepositoryImpl @Inject constructor( } } - override suspend fun getRecommendedProductList(isRenewed: Boolean): ProductRepositoryResult> { + override suspend fun getRecommendedProductList(isRenewed: Boolean): RepositoryResult, ProductErrorState> { val response = getApiResult { productAPI.getRecommendedProductList() } return when (response) { is APIResult.Success -> { - ProductRepositoryResult.Success( + RepositoryResult.Success( response.data.recommendList?.map { dto -> RecommendProductData( dto.productName ?: "", @@ -187,13 +192,13 @@ class ProductRepositoryImpl @Inject constructor( override suspend fun getProductDetail( productCode: String, isRenewed: Boolean - ): ProductRepositoryResult { + ): RepositoryResult { val response = getApiResult { productAPI.getProductDetail(productCode) } return when (response) { is APIResult.Success -> { - ProductRepositoryResult.Success( + RepositoryResult.Success( ProductDetailResult( productName = response.data.productName ?: "", productCode = response.data.productCode ?: "", @@ -220,10 +225,10 @@ class ProductRepositoryImpl @Inject constructor( override suspend fun deleteProduct( productCode: String, isRenewed: Boolean - ): ProductRepositoryResult { + ): RepositoryResult { return when (val response = getApiResult { productAPI.deleteProduct(productCode) }) { is APIResult.Success -> { - ProductRepositoryResult.Success(true) + RepositoryResult.Success(true) } is APIResult.Error -> { @@ -235,16 +240,17 @@ class ProductRepositoryImpl @Inject constructor( } override suspend fun updateTargetPrice( - pricePatchRequest: PricePatchRequest, + productCode: String, + targetPrice: Int, isRenewed: Boolean - ): ProductRepositoryResult { + ): RepositoryResult { val response = getApiResult { - productAPI.updateTargetPrice(pricePatchRequest) + productAPI.updateTargetPrice(PricePatchRequest(productCode, targetPrice)) } return when (response) { is APIResult.Success -> { - ProductRepositoryResult.Success( - PricePatchResponse( + RepositoryResult.Success( + PricePatchResult( response.data.statusCode, response.data.message ) @@ -253,7 +259,21 @@ class ProductRepositoryImpl @Inject constructor( is APIResult.Error -> { handleError(response.code, isRenewed) { - updateTargetPrice(pricePatchRequest, true) + updateTargetPrice(productCode, targetPrice, true) + } + } + } + } + + override suspend fun switchAlert(productCode: String, isRenewed: Boolean): RepositoryResult { + return when (val response = getApiResult { productAPI.updateAlert(productCode) }) { + is APIResult.Success -> { + RepositoryResult.Success(true) + } + + is APIResult.Error -> { + handleError(response.code, isRenewed) { + deleteProduct(productCode, true) } } } diff --git a/android/app/src/main/java/app/priceguard/data/repository/token/TokenErrorState.kt b/android/app/src/main/java/app/priceguard/data/repository/token/TokenErrorState.kt new file mode 100644 index 0000000..36506c4 --- /dev/null +++ b/android/app/src/main/java/app/priceguard/data/repository/token/TokenErrorState.kt @@ -0,0 +1,7 @@ +package app.priceguard.data.repository.token + +enum class TokenErrorState { + UNAUTHORIZED, + EXPIRED, + UNDEFINED_ERROR +} diff --git a/android/app/src/main/java/app/priceguard/data/repository/token/TokenRepository.kt b/android/app/src/main/java/app/priceguard/data/repository/token/TokenRepository.kt new file mode 100644 index 0000000..dff5a21 --- /dev/null +++ b/android/app/src/main/java/app/priceguard/data/repository/token/TokenRepository.kt @@ -0,0 +1,15 @@ +package app.priceguard.data.repository.token + +import app.priceguard.data.repository.RepositoryResult +import app.priceguard.ui.data.UserDataResult + +interface TokenRepository { + suspend fun storeTokens(accessToken: String, refreshToken: String) + suspend fun getAccessToken(): String? + suspend fun getRefreshToken(): String? + suspend fun getFirebaseToken(): String? + suspend fun updateFirebaseToken(accessToken: String, firebaseToken: String): RepositoryResult + suspend fun getUserData(): UserDataResult + suspend fun renewTokens(refreshToken: String): RepositoryResult + suspend fun clearTokens() +} diff --git a/android/app/src/main/java/app/priceguard/data/repository/token/TokenRepositoryImpl.kt b/android/app/src/main/java/app/priceguard/data/repository/token/TokenRepositoryImpl.kt new file mode 100644 index 0000000..245322d --- /dev/null +++ b/android/app/src/main/java/app/priceguard/data/repository/token/TokenRepositoryImpl.kt @@ -0,0 +1,111 @@ +package app.priceguard.data.repository.token + +import android.util.Log +import app.priceguard.data.datastore.TokenDataSource +import app.priceguard.data.dto.firebase.FirebaseTokenUpdateRequest +import app.priceguard.data.network.AuthAPI +import app.priceguard.data.network.UserAPI +import app.priceguard.data.repository.APIResult +import app.priceguard.data.repository.RepositoryResult +import app.priceguard.data.repository.getApiResult +import app.priceguard.ui.data.UserDataResult +import com.google.firebase.Firebase +import com.google.firebase.messaging.FirebaseMessaging +import com.google.firebase.messaging.messaging +import java.util.* +import javax.inject.Inject +import kotlinx.coroutines.tasks.await +import kotlinx.serialization.json.Json + +class TokenRepositoryImpl @Inject constructor( + private val tokenDataSource: TokenDataSource, + private val authAPI: AuthAPI, + private val userAPI: UserAPI +) : TokenRepository { + private fun handleError( + code: Int? + ): RepositoryResult { + return when (code) { + 401 -> { + RepositoryResult.Error(TokenErrorState.UNAUTHORIZED) + } + + 410 -> { + RepositoryResult.Error(TokenErrorState.EXPIRED) + } + + else -> { + RepositoryResult.Error(TokenErrorState.UNDEFINED_ERROR) + } + } + } + + override suspend fun storeTokens(accessToken: String, refreshToken: String) { + tokenDataSource.saveTokens(accessToken, refreshToken) + } + + override suspend fun getAccessToken(): String? { + return tokenDataSource.getAccessToken() + } + + override suspend fun getRefreshToken(): String? { + return tokenDataSource.getRefreshToken() + } + + override suspend fun getFirebaseToken(): String? { + return try { + FirebaseMessaging.getInstance().token.await() + } catch (e: Exception) { + Log.e("FCM Token", e.toString()) + null + } + } + + override suspend fun updateFirebaseToken(accessToken: String, firebaseToken: String): RepositoryResult { + return when ( + val response = + getApiResult { userAPI.updateFirebaseToken("Bearer $accessToken", FirebaseTokenUpdateRequest(firebaseToken)) } + ) { + is APIResult.Success -> { + RepositoryResult.Success(true) + } + + is APIResult.Error -> { + handleError(response.code) + } + } + } + + override suspend fun getUserData(): UserDataResult { + val accessToken = tokenDataSource.getAccessToken() ?: return UserDataResult("", "") + val parts = accessToken.split(".") + return try { + val charset = charset("UTF-8") + val payload = Json.decodeFromString( + String(Base64.getUrlDecoder().decode(parts[1].toByteArray(charset)), charset) + ) + UserDataResult(payload.email, payload.name) + } catch (e: Exception) { + Log.e("Data Not Found", "Error parsing JWT: $e") + UserDataResult("", "") + } + } + + override suspend fun renewTokens(refreshToken: String): RepositoryResult { + return when (val response = getApiResult { authAPI.renewTokens("Bearer $refreshToken") }) { + is APIResult.Success -> { + storeTokens(response.data.accessToken, response.data.refreshToken) + RepositoryResult.Success(true) + } + + is APIResult.Error -> { + handleError(response.code) + } + } + } + + override suspend fun clearTokens() { + Firebase.messaging.deleteToken() + tokenDataSource.clearTokens() + } +} diff --git a/android/app/src/main/java/app/priceguard/data/dto/UserDataDTO.kt b/android/app/src/main/java/app/priceguard/data/repository/token/TokenUserData.kt similarity index 55% rename from android/app/src/main/java/app/priceguard/data/dto/UserDataDTO.kt rename to android/app/src/main/java/app/priceguard/data/repository/token/TokenUserData.kt index aae9457..310c9e1 100644 --- a/android/app/src/main/java/app/priceguard/data/dto/UserDataDTO.kt +++ b/android/app/src/main/java/app/priceguard/data/repository/token/TokenUserData.kt @@ -1,17 +1,12 @@ -package app.priceguard.data.dto +package app.priceguard.data.repository.token import kotlinx.serialization.Serializable @Serializable -data class UserDataDTO( +data class TokenUserData( val id: String, val email: String, val name: String, val iat: Int, val exp: Int ) - -data class UserDataResult( - val email: String, - val name: String -) diff --git a/android/app/src/main/java/app/priceguard/di/UserRepositoryModule.kt b/android/app/src/main/java/app/priceguard/di/AuthRepositoryModule.kt similarity index 55% rename from android/app/src/main/java/app/priceguard/di/UserRepositoryModule.kt rename to android/app/src/main/java/app/priceguard/di/AuthRepositoryModule.kt index efc4b7e..ac878fd 100644 --- a/android/app/src/main/java/app/priceguard/di/UserRepositoryModule.kt +++ b/android/app/src/main/java/app/priceguard/di/AuthRepositoryModule.kt @@ -1,8 +1,8 @@ package app.priceguard.di import app.priceguard.data.network.UserAPI -import app.priceguard.data.repository.UserRepository -import app.priceguard.data.repository.UserRepositoryImpl +import app.priceguard.data.repository.auth.AuthRepository +import app.priceguard.data.repository.auth.AuthRepositoryImpl import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -11,9 +11,9 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -object UserRepositoryModule { +object AuthRepositoryModule { @Provides @Singleton - fun provideUserRepository(userAPI: UserAPI): UserRepository = UserRepositoryImpl(userAPI) + fun provideUserRepository(userAPI: UserAPI): AuthRepository = AuthRepositoryImpl(userAPI) } diff --git a/android/app/src/main/java/app/priceguard/di/ConfigDataSourceModule.kt b/android/app/src/main/java/app/priceguard/di/ConfigDataSourceModule.kt new file mode 100644 index 0000000..c008925 --- /dev/null +++ b/android/app/src/main/java/app/priceguard/di/ConfigDataSourceModule.kt @@ -0,0 +1,21 @@ +package app.priceguard.di + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import app.priceguard.data.datastore.ConfigDataSource +import app.priceguard.data.datastore.ConfigDataSourceImpl +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object ConfigDataSourceModule { + + @Provides + @Singleton + fun provideConfigDataSource(@ConfigQualifier dataStore: DataStore): ConfigDataSource = + ConfigDataSourceImpl(dataStore) +} diff --git a/android/app/src/main/java/app/priceguard/di/DataStoreModule.kt b/android/app/src/main/java/app/priceguard/di/DataStoreModule.kt index 841dffe..53a38fb 100644 --- a/android/app/src/main/java/app/priceguard/di/DataStoreModule.kt +++ b/android/app/src/main/java/app/priceguard/di/DataStoreModule.kt @@ -12,17 +12,25 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import javax.inject.Qualifier import javax.inject.Singleton -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob private const val TOKEN_FILE = "tokens" +private const val CONFIG = "config" + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +annotation class ConfigQualifier + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +annotation class TokensQualifier @Module @InstallIn(SingletonComponent::class) object DataStoreModule { + @TokensQualifier @Provides @Singleton fun providePreferencesDataStore(@ApplicationContext appContext: Context): DataStore { @@ -30,8 +38,19 @@ object DataStoreModule { corruptionHandler = ReplaceFileCorruptionHandler( produceNewData = { emptyPreferences() } ), - scope = CoroutineScope(Dispatchers.IO + SupervisorJob()), produceFile = { appContext.preferencesDataStoreFile(TOKEN_FILE) } ) } + + @ConfigQualifier + @Provides + @Singleton + fun provideConfigDataStore(@ApplicationContext appContext: Context): DataStore { + return PreferenceDataStoreFactory.create( + corruptionHandler = ReplaceFileCorruptionHandler( + produceNewData = { emptyPreferences() } + ), + produceFile = { appContext.preferencesDataStoreFile(CONFIG) } + ) + } } diff --git a/android/app/src/main/java/app/priceguard/di/ProductRepositoryModule.kt b/android/app/src/main/java/app/priceguard/di/ProductRepositoryModule.kt index a835a08..41ce3ac 100644 --- a/android/app/src/main/java/app/priceguard/di/ProductRepositoryModule.kt +++ b/android/app/src/main/java/app/priceguard/di/ProductRepositoryModule.kt @@ -1,10 +1,10 @@ package app.priceguard.di -import app.priceguard.data.graph.GraphDataConverter +import app.priceguard.data.GraphDataConverter import app.priceguard.data.network.ProductAPI -import app.priceguard.data.repository.ProductRepository -import app.priceguard.data.repository.ProductRepositoryImpl -import app.priceguard.data.repository.TokenRepository +import app.priceguard.data.repository.product.ProductRepository +import app.priceguard.data.repository.product.ProductRepositoryImpl +import app.priceguard.data.repository.token.TokenRepository import dagger.Module import dagger.Provides import dagger.hilt.InstallIn diff --git a/android/app/src/main/java/app/priceguard/di/TokenDataSourceModule.kt b/android/app/src/main/java/app/priceguard/di/TokenDataSourceModule.kt index fc33fae..253f30c 100644 --- a/android/app/src/main/java/app/priceguard/di/TokenDataSourceModule.kt +++ b/android/app/src/main/java/app/priceguard/di/TokenDataSourceModule.kt @@ -16,6 +16,6 @@ object TokenDataSourceModule { @Provides @Singleton - fun provideTokenDataSource(dataStore: DataStore): TokenDataSource = + fun provideTokenDataSource(@TokensQualifier dataStore: DataStore): TokenDataSource = TokenDataSourceImpl(dataStore) } diff --git a/android/app/src/main/java/app/priceguard/di/TokenRepositoryModule.kt b/android/app/src/main/java/app/priceguard/di/TokenRepositoryModule.kt index 2169cbe..845aa6f 100644 --- a/android/app/src/main/java/app/priceguard/di/TokenRepositoryModule.kt +++ b/android/app/src/main/java/app/priceguard/di/TokenRepositoryModule.kt @@ -2,8 +2,9 @@ package app.priceguard.di import app.priceguard.data.datastore.TokenDataSource import app.priceguard.data.network.AuthAPI -import app.priceguard.data.repository.TokenRepository -import app.priceguard.data.repository.TokenRepositoryImpl +import app.priceguard.data.network.UserAPI +import app.priceguard.data.repository.token.TokenRepository +import app.priceguard.data.repository.token.TokenRepositoryImpl import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -18,7 +19,8 @@ object TokenRepositoryModule { @Singleton fun provideTokenRepository( tokenDataSource: TokenDataSource, - authAPI: AuthAPI + authAPI: AuthAPI, + userAPI: UserAPI ): TokenRepository = - TokenRepositoryImpl(tokenDataSource, authAPI) + TokenRepositoryImpl(tokenDataSource, authAPI, userAPI) } diff --git a/android/app/src/main/java/app/priceguard/service/PriceGuardFirebaseMessagingService.kt b/android/app/src/main/java/app/priceguard/service/PriceGuardFirebaseMessagingService.kt new file mode 100644 index 0000000..e46e26a --- /dev/null +++ b/android/app/src/main/java/app/priceguard/service/PriceGuardFirebaseMessagingService.kt @@ -0,0 +1,11 @@ +package app.priceguard.service + +import android.util.Log +import com.google.firebase.messaging.FirebaseMessagingService + +class PriceGuardFirebaseMessagingService : FirebaseMessagingService() { + // Init μ‹œμ—λ„ 호좜됨 + override fun onNewToken(token: String) { + Log.d("PriceGuardFirebaseMessagingService", "Refreshed token: $token") + } +} diff --git a/android/app/src/main/java/app/priceguard/service/UpdateAlarmWorker.kt b/android/app/src/main/java/app/priceguard/service/UpdateAlarmWorker.kt new file mode 100644 index 0000000..f463b71 --- /dev/null +++ b/android/app/src/main/java/app/priceguard/service/UpdateAlarmWorker.kt @@ -0,0 +1,58 @@ +package app.priceguard.service + +import android.content.Context +import android.util.Log +import androidx.hilt.work.HiltWorker +import androidx.work.Constraints +import androidx.work.CoroutineWorker +import androidx.work.Data +import androidx.work.OneTimeWorkRequest +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkerParameters +import app.priceguard.data.repository.RepositoryResult +import app.priceguard.data.repository.product.ProductRepository +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject + +@HiltWorker +class UpdateAlarmWorker @AssistedInject constructor( + @Assisted appContext: Context, + @Assisted workerParams: WorkerParameters, + private val productRepository: ProductRepository +) : CoroutineWorker(appContext, workerParams) { + + override suspend fun doWork(): Result { + val inputData = inputData.getString(ARGUMENT_KEY) ?: return Result.failure() + + return updateAlarm(inputData) + } + + private suspend fun updateAlarm(productCode: String): Result { + return try { + when (productRepository.switchAlert(productCode)) { + is RepositoryResult.Error -> { + Result.failure() + } + + is RepositoryResult.Success -> { + Result.success() + } + } + } catch (e: Exception) { + Log.e("Update Alarm Error", e.message.toString()) + Result.failure() + } + } + + companion object { + const val ARGUMENT_KEY = "productCode" + fun createWorkRequest(inputString: String): OneTimeWorkRequest { + val inputData = Data.Builder().putString(ARGUMENT_KEY, inputString).build() + val constraints = Constraints.Builder().build() + return OneTimeWorkRequestBuilder() + .setInputData(inputData) + .setConstraints(constraints) + .build() + } + } +} diff --git a/android/app/src/main/java/app/priceguard/service/UpdateTokenWorker.kt b/android/app/src/main/java/app/priceguard/service/UpdateTokenWorker.kt new file mode 100644 index 0000000..0f4fa3e --- /dev/null +++ b/android/app/src/main/java/app/priceguard/service/UpdateTokenWorker.kt @@ -0,0 +1,46 @@ +package app.priceguard.service + +import android.content.Context +import android.util.Log +import androidx.hilt.work.HiltWorker +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import app.priceguard.data.repository.RepositoryResult +import app.priceguard.data.repository.token.TokenRepository +import com.google.firebase.ktx.Firebase +import com.google.firebase.messaging.ktx.messaging +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import kotlinx.coroutines.tasks.await + +@HiltWorker +class UpdateTokenWorker @AssistedInject constructor( + @Assisted appContext: Context, + @Assisted workerParams: WorkerParameters, + private val tokenRepository: TokenRepository +) : + CoroutineWorker(appContext, workerParams) { + + override suspend fun doWork(): Result { + val token = Firebase.messaging.token.await() + return storeToken(token) + } + + private suspend fun storeToken(token: String): Result { + val accessToken = tokenRepository.getAccessToken() ?: return Result.failure() + return try { + when (tokenRepository.updateFirebaseToken(accessToken, token)) { + is RepositoryResult.Error -> { + Result.failure() + } + + is RepositoryResult.Success -> { + Result.success() + } + } + } catch (e: Exception) { + Log.e("Update Token Error", e.message.toString()) + Result.failure() + } + } +} diff --git a/android/app/src/main/java/app/priceguard/ui/PriceGuardApp.kt b/android/app/src/main/java/app/priceguard/ui/PriceGuardApp.kt index 7743a83..829419b 100644 --- a/android/app/src/main/java/app/priceguard/ui/PriceGuardApp.kt +++ b/android/app/src/main/java/app/priceguard/ui/PriceGuardApp.kt @@ -1,7 +1,80 @@ package app.priceguard.ui import android.app.Application +import android.app.UiModeManager +import android.content.Context +import androidx.appcompat.app.AppCompatDelegate +import androidx.hilt.work.HiltWorkerFactory +import androidx.work.Configuration +import app.priceguard.data.datastore.ConfigDataSource +import com.google.android.material.color.DynamicColors import dagger.hilt.android.HiltAndroidApp +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch @HiltAndroidApp -class PriceGuardApp : Application() +class PriceGuardApp : Application(), Configuration.Provider { + + @Inject + lateinit var configDataSource: ConfigDataSource + + @Inject + lateinit var workerFactory: HiltWorkerFactory + + override val workManagerConfiguration: Configuration + get() = Configuration.Builder() + .setWorkerFactory(workerFactory) + .build() + + override fun onCreate() { + super.onCreate() + + initAppTheme() + } + + private fun initAppTheme() { + CoroutineScope(Dispatchers.IO).launch { + val dynamicColorMode = configDataSource.getDynamicMode() + val darkMode = configDataSource.getDarkMode() + + when (dynamicColorMode) { + MODE_DYNAMIC -> { + DynamicColors.applyToActivitiesIfAvailable(this@PriceGuardApp) + } + } + + when (darkMode) { + MODE_LIGHT -> { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { + val uiModeManager = + getSystemService(Context.UI_MODE_SERVICE) as UiModeManager + uiModeManager.setApplicationNightMode(UiModeManager.MODE_NIGHT_NO) + } else { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) + } + } + + MODE_DARK -> { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { + val uiModeManager = + getSystemService(Context.UI_MODE_SERVICE) as UiModeManager + uiModeManager.setApplicationNightMode(UiModeManager.MODE_NIGHT_YES) + } else { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) + } + } + } + } + } + + companion object { + const val MODE_SYSTEM = 0 + const val MODE_LIGHT = 1 + const val MODE_DARK = 2 + + const val MODE_DYNAMIC_NO = 0 + const val MODE_DYNAMIC = 1 + } +} diff --git a/android/app/src/main/java/app/priceguard/ui/additem/AddItemActivity.kt b/android/app/src/main/java/app/priceguard/ui/additem/AddItemActivity.kt index fdfba1a..132accb 100644 --- a/android/app/src/main/java/app/priceguard/ui/additem/AddItemActivity.kt +++ b/android/app/src/main/java/app/priceguard/ui/additem/AddItemActivity.kt @@ -1,8 +1,12 @@ package app.priceguard.ui.additem +import android.content.Intent import android.os.Bundle import androidx.appcompat.app.AppCompatActivity +import androidx.navigation.NavController +import androidx.navigation.NavDirections import androidx.navigation.fragment.NavHostFragment +import app.priceguard.R import app.priceguard.databinding.ActivityAddItemBinding import app.priceguard.ui.additem.link.RegisterItemLinkFragmentDirections import dagger.hilt.android.AndroidEntryPoint @@ -21,12 +25,19 @@ class AddItemActivity : AppCompatActivity() { } private fun setStartDestination() { - if (intent.hasExtra("productCode") && + val navController = binding.fcvAddItem.getFragment().navController + + if (intent?.action == Intent.ACTION_SEND && intent.type == "text/plain") { + intent.getStringExtra(Intent.EXTRA_TEXT)?.let { data -> + val bundle = Bundle() + bundle.putString("link", data) + navController.navigate(R.id.registerItemLinkFragment, bundle) + } + } else if (intent.hasExtra("productCode") && intent.hasExtra("productTitle") && intent.hasExtra("productPrice") && intent.hasExtra("isAdding") ) { - val navController = binding.fcvAddItem.getFragment().navController val action = RegisterItemLinkFragmentDirections.actionRegisterItemLinkFragmentToSetTargetPriceFragment( intent.getStringExtra("productCode") ?: "", @@ -34,7 +45,11 @@ class AddItemActivity : AppCompatActivity() { intent.getIntExtra("productPrice", 0), intent.getBooleanExtra("isAdding", true) ) - navController.navigate(action) + navController.safeNavigate(action) } } + + private fun NavController.safeNavigate(direction: NavDirections) { + currentDestination?.getAction(direction.actionId)?.run { navigate(direction) } + } } diff --git a/android/app/src/main/java/app/priceguard/ui/additem/confirm/ConfirmItemLinkFragment.kt b/android/app/src/main/java/app/priceguard/ui/additem/confirm/ConfirmItemLinkFragment.kt index 8af63f0..f8f434a 100644 --- a/android/app/src/main/java/app/priceguard/ui/additem/confirm/ConfirmItemLinkFragment.kt +++ b/android/app/src/main/java/app/priceguard/ui/additem/confirm/ConfirmItemLinkFragment.kt @@ -4,20 +4,17 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.activity.viewModels import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController -import app.priceguard.R -import app.priceguard.data.dto.ProductVerifyDTO import app.priceguard.databinding.FragmentConfirmItemLinkBinding -import java.text.NumberFormat -import kotlinx.serialization.json.Json class ConfirmItemLinkFragment : Fragment() { private var _binding: FragmentConfirmItemLinkBinding? = null private val binding get() = _binding!! - - private lateinit var productInfo: ProductVerifyDTO + private val confirmItemLinkViewModel: ConfirmItemLinkViewModel by viewModels() override fun onCreateView( inflater: LayoutInflater, @@ -30,23 +27,20 @@ class ConfirmItemLinkFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - + binding.viewModel = confirmItemLinkViewModel + binding.lifecycleOwner = viewLifecycleOwner binding.initListener() - binding.initView() + initView() } - private fun FragmentConfirmItemLinkBinding.initView() { - val productJson = requireArguments().getString("product") ?: return - productInfo = Json.decodeFromString(productJson) - - tvConfirmItemPrice.text = - String.format( - resources.getString(R.string.won), - NumberFormat.getNumberInstance().format(productInfo.productPrice) - ) - tvConfirmItemBrand.text = productInfo.shop - tvConfirmItemItemTitle.text = productInfo.productName - imageUrl = productInfo.imageUrl + private fun initView() { + val arguments = requireArguments() + confirmItemLinkViewModel.setUIState( + price = arguments.getInt("productPrice"), + brand = arguments.getString("shop") ?: return, + name = arguments.getString("productName") ?: return, + imageUrl = arguments.getString("imageUrl") ?: return + ) } override fun onDestroyView() { @@ -55,12 +49,14 @@ class ConfirmItemLinkFragment : Fragment() { } private fun FragmentConfirmItemLinkBinding.initListener() { + val arguments = requireArguments() + btnConfirmItemNext.setOnClickListener { val action = ConfirmItemLinkFragmentDirections.actionConfirmItemLinkFragmentToSetTargetPriceFragment( - productInfo.productCode ?: "", - productInfo.productName ?: "", - productInfo.productPrice ?: 0, + arguments.getString("productCode") ?: "", + arguments.getString("productName") ?: "", + arguments.getInt("productPrice"), true ) findNavController().navigate(action) diff --git a/android/app/src/main/java/app/priceguard/ui/additem/confirm/ConfirmItemLinkViewModel.kt b/android/app/src/main/java/app/priceguard/ui/additem/confirm/ConfirmItemLinkViewModel.kt index 68503f6..cd9db34 100644 --- a/android/app/src/main/java/app/priceguard/ui/additem/confirm/ConfirmItemLinkViewModel.kt +++ b/android/app/src/main/java/app/priceguard/ui/additem/confirm/ConfirmItemLinkViewModel.kt @@ -1,16 +1,27 @@ package app.priceguard.ui.additem.confirm import androidx.lifecycle.ViewModel -import app.priceguard.data.dto.ProductVerifyDTO import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update class ConfirmItemLinkViewModel : ViewModel() { - private val _flow = MutableStateFlow(ProductVerifyDTO("", "", 0, "", "")) - val flow = _flow.asStateFlow() + data class ConfirmItemLinkUIState( + val price: Int? = null, + val brand: String? = null, + val name: String? = null, + val imageUrl: String? = null + ) - fun setProductInfo(productInfo: ProductVerifyDTO) { - _flow.value = productInfo + private var _state: MutableStateFlow = + MutableStateFlow(ConfirmItemLinkUIState()) + val state: StateFlow = _state.asStateFlow() + + fun setUIState(price: Int, brand: String, name: String, imageUrl: String) { + _state.update { + it.copy(price = price, brand = brand, name = name, imageUrl = imageUrl) + } } } diff --git a/android/app/src/main/java/app/priceguard/ui/additem/link/LinkHelperWebViewActivity.kt b/android/app/src/main/java/app/priceguard/ui/additem/link/LinkHelperWebViewActivity.kt new file mode 100644 index 0000000..23727c6 --- /dev/null +++ b/android/app/src/main/java/app/priceguard/ui/additem/link/LinkHelperWebViewActivity.kt @@ -0,0 +1,19 @@ +package app.priceguard.ui.additem.link + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import app.priceguard.databinding.ActivityLinkHelperWebViewBinding + +class LinkHelperWebViewActivity : AppCompatActivity() { + + private lateinit var binding: ActivityLinkHelperWebViewBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = ActivityLinkHelperWebViewBinding.inflate(layoutInflater) + setContentView(binding.root) + + binding.wbLinkHelper.loadUrl("https://info-kr.priceguard.app/") + } +} diff --git a/android/app/src/main/java/app/priceguard/ui/additem/link/RegisterItemLinkFragment.kt b/android/app/src/main/java/app/priceguard/ui/additem/link/RegisterItemLinkFragment.kt index e9024e8..e5d3c2f 100644 --- a/android/app/src/main/java/app/priceguard/ui/additem/link/RegisterItemLinkFragment.kt +++ b/android/app/src/main/java/app/priceguard/ui/additem/link/RegisterItemLinkFragment.kt @@ -1,24 +1,25 @@ package app.priceguard.ui.additem.link +import android.content.Intent import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.activity.addCallback import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.navigation.NavController import androidx.navigation.NavDirections import androidx.navigation.fragment.findNavController import app.priceguard.R -import app.priceguard.data.dto.ProductErrorState -import app.priceguard.data.repository.TokenRepository +import app.priceguard.data.repository.product.ProductErrorState +import app.priceguard.data.repository.token.TokenRepository import app.priceguard.databinding.FragmentRegisterItemLinkBinding +import app.priceguard.ui.home.HomeActivity import app.priceguard.ui.util.lifecycle.repeatOnStarted import app.priceguard.ui.util.ui.showPermissionDeniedDialog import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json @AndroidEntryPoint class RegisterItemLinkFragment : Fragment() { @@ -28,7 +29,7 @@ class RegisterItemLinkFragment : Fragment() { private var _binding: FragmentRegisterItemLinkBinding? = null private val binding get() = _binding!! - private val viewModel: RegisterItemLinkViewModel by viewModels() + private val registerItemLinkViewModel: RegisterItemLinkViewModel by viewModels() override fun onCreateView( inflater: LayoutInflater, @@ -42,14 +43,46 @@ class RegisterItemLinkFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.lifecycleOwner = viewLifecycleOwner - binding.viewModel = viewModel + binding.viewModel = registerItemLinkViewModel + + setBackPressedCallback() + setLinkText() + initCollector() initEvent() + + binding.tvRegisterItemHelper.setOnClickListener { + val intent = Intent(requireActivity(), LinkHelperWebViewActivity::class.java) + startActivity(intent) + } + } + + private fun setLinkText() { + arguments?.getString("link")?.let { linkText -> + binding.etRegisterItemLink.setText(linkText) + registerItemLinkViewModel.updateLink(linkText) + } + } + + private fun setBackPressedCallback() { + requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) { + goToHomeActivity() + } + } + + private fun goToHomeActivity() { + val activityIntent = requireActivity().intent + if (activityIntent?.action == Intent.ACTION_SEND) { + val intent = Intent(requireActivity(), HomeActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + startActivity(intent) + } + requireActivity().finish() } private fun initCollector() { repeatOnStarted { - viewModel.state.collect { state -> + registerItemLinkViewModel.state.collect { state -> if (state.isLinkError) { updateLinkError(getString(R.string.not_link)) } else { @@ -61,12 +94,16 @@ class RegisterItemLinkFragment : Fragment() { private fun initEvent() { repeatOnStarted { - viewModel.event.collect { event -> + registerItemLinkViewModel.event.collect { event -> when (event) { is RegisterItemLinkViewModel.RegisterLinkEvent.SuccessVerification -> { val action = RegisterItemLinkFragmentDirections.actionRegisterItemLinkFragmentToConfirmItemLinkFragment( - Json.encodeToString(event.product) + event.product.productCode, + event.product.productPrice, + event.product.shop, + event.product.productName, + event.product.imageUrl ) findNavController().safeNavigate(action) } diff --git a/android/app/src/main/java/app/priceguard/ui/additem/link/RegisterItemLinkViewModel.kt b/android/app/src/main/java/app/priceguard/ui/additem/link/RegisterItemLinkViewModel.kt index e933a86..15c4659 100644 --- a/android/app/src/main/java/app/priceguard/ui/additem/link/RegisterItemLinkViewModel.kt +++ b/android/app/src/main/java/app/priceguard/ui/additem/link/RegisterItemLinkViewModel.kt @@ -2,11 +2,10 @@ package app.priceguard.ui.additem.link import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import app.priceguard.data.dto.ProductErrorState -import app.priceguard.data.dto.ProductVerifyDTO -import app.priceguard.data.dto.ProductVerifyRequest -import app.priceguard.data.network.ProductRepositoryResult -import app.priceguard.data.repository.ProductRepository +import app.priceguard.data.repository.RepositoryResult +import app.priceguard.data.repository.product.ProductErrorState +import app.priceguard.data.repository.product.ProductRepository +import app.priceguard.ui.data.ProductVerifyResult import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.MutableSharedFlow @@ -21,13 +20,13 @@ class RegisterItemLinkViewModel data class RegisterLinkUIState( val link: String = "", - val product: ProductVerifyDTO? = null, - val isNextReady: Boolean = true, + val product: ProductVerifyResult? = null, + val isNextReady: Boolean = false, val isLinkError: Boolean = false ) sealed class RegisterLinkEvent { - data class SuccessVerification(val product: ProductVerifyDTO) : RegisterLinkEvent() + data class SuccessVerification(val product: ProductVerifyResult) : RegisterLinkEvent() data class FailureVerification(val errorType: ProductErrorState) : RegisterLinkEvent() } @@ -55,16 +54,15 @@ class RegisterItemLinkViewModel ) viewModelScope.launch { - val response = productRepository.verifyLink(ProductVerifyRequest(state.value.link)) - when (response) { - is ProductRepositoryResult.Success -> { + when (val response = productRepository.verifyLink(state.value.link)) { + is RepositoryResult.Success -> { _state.value = state.value.copy(isNextReady = true, product = response.data) _event.emit(RegisterLinkEvent.SuccessVerification(response.data)) } - is ProductRepositoryResult.Error -> { + is RepositoryResult.Error -> { _state.value = state.value.copy(isLinkError = true, isNextReady = false) - _event.emit(RegisterLinkEvent.FailureVerification(response.productErrorState)) + _event.emit(RegisterLinkEvent.FailureVerification(response.errorState)) } } } diff --git a/android/app/src/main/java/app/priceguard/ui/additem/setprice/SetTargetPriceFragment.kt b/android/app/src/main/java/app/priceguard/ui/additem/setprice/SetTargetPriceFragment.kt index 51167ba..209d6fd 100644 --- a/android/app/src/main/java/app/priceguard/ui/additem/setprice/SetTargetPriceFragment.kt +++ b/android/app/src/main/java/app/priceguard/ui/additem/setprice/SetTargetPriceFragment.kt @@ -1,5 +1,6 @@ package app.priceguard.ui.additem.setprice +import android.content.Intent import android.os.Bundle import android.text.Editable import android.view.LayoutInflater @@ -11,10 +12,11 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import app.priceguard.R -import app.priceguard.data.dto.ProductErrorState -import app.priceguard.data.repository.TokenRepository +import app.priceguard.data.repository.product.ProductErrorState +import app.priceguard.data.repository.token.TokenRepository import app.priceguard.databinding.FragmentSetTargetPriceBinding import app.priceguard.ui.additem.setprice.SetTargetPriceViewModel.SetTargetPriceEvent +import app.priceguard.ui.home.HomeActivity import app.priceguard.ui.util.lifecycle.repeatOnStarted import app.priceguard.ui.util.ui.showPermissionDeniedDialog import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -32,7 +34,7 @@ class SetTargetPriceFragment : Fragment() { private var _binding: FragmentSetTargetPriceBinding? = null private val binding get() = _binding!! - private val viewModel: SetTargetPriceViewModel by viewModels() + private val setTargetPriceViewModel: SetTargetPriceViewModel by viewModels() override fun onCreateView( inflater: LayoutInflater, @@ -46,34 +48,44 @@ class SetTargetPriceFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - binding.vm = viewModel + binding.viewModel = setTargetPriceViewModel binding.lifecycleOwner = viewLifecycleOwner - val callback = requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) { + setBackPressedCallback() + binding.initView() + binding.initListener() + handleEvent() + } + + private fun setBackPressedCallback() { + requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) { if (requireActivity().intent.hasExtra("isAdding")) { requireActivity().finish() } else { findNavController().navigateUp() } } + } + + private fun FragmentSetTargetPriceBinding.initView() { + val arguments = requireArguments() - val productCode = requireArguments().getString("productCode") ?: "" - val title = requireArguments().getString("productTitle") ?: "" - val price = requireArguments().getInt("productPrice") + val productCode = arguments.getString("productCode") ?: "" + val title = arguments.getString("productTitle") ?: "" + val price = arguments.getInt("productPrice") - viewModel.updateTargetPrice((price * 0.8).toInt()) + setTargetPriceViewModel.updateTargetPrice((price * 0.8).toInt()) - binding.tvSetPriceCurrentPrice.text = + tvSetPriceCurrentPrice.text = String.format( resources.getString(R.string.won), NumberFormat.getNumberInstance().format(price) ) + tvSetPriceCurrentPrice.contentDescription = + getString(R.string.current_price_info, tvSetPriceCurrentPrice.text) - viewModel.setProductInfo(productCode, title, price) - binding.etTargetPrice.setText((price * 0.8).toInt().toString()) - - binding.initListener() - handleEvent() + setTargetPriceViewModel.setProductInfo(productCode, title, price) + etTargetPrice.setText((price * 0.8).toInt().toString()) } private fun FragmentSetTargetPriceBinding.initListener() { @@ -86,7 +98,7 @@ class SetTargetPriceFragment : Fragment() { } btnConfirmItemNext.setOnClickListener { val isAdding = requireArguments().getBoolean("isAdding") - if (isAdding) viewModel.addProduct() else viewModel.patchProduct() + if (isAdding) setTargetPriceViewModel.addProduct() else setTargetPriceViewModel.patchProduct() } slTargetPrice.addOnChangeListener { _, value, _ -> if (!etTargetPrice.isFocused) { @@ -115,14 +127,20 @@ class SetTargetPriceFragment : Fragment() { 0F } - viewModel.updateTargetPrice(targetPrice.toInt()) + setTargetPriceViewModel.updateTargetPrice(targetPrice.toInt()) val percent = - ((targetPrice / viewModel.state.value.productPrice) * MAX_PERCENT).toInt() + ((targetPrice / setTargetPriceViewModel.state.value.productPrice) * MAX_PERCENT).toInt() binding.tvTargetPricePercent.text = String.format(getString(R.string.current_price_percent), percent) + binding.tvTargetPricePercent.contentDescription = getString( + R.string.target_price_percent_and_price, + binding.tvTargetPricePercent.text, + binding.tvSetPriceCurrentPrice.text + ) + binding.updateSlideValueWithPrice(targetPrice, percent.roundAtFirstDigit()) } } @@ -132,18 +150,18 @@ class SetTargetPriceFragment : Fragment() { } private fun FragmentSetTargetPriceBinding.setTargetPriceAndPercent(value: Float) { - val targetPrice = ((viewModel.state.value.productPrice) * value.toInt() / 100) + val targetPrice = ((setTargetPriceViewModel.state.value.productPrice) * value.toInt() / 100) tvTargetPricePercent.text = String.format(getString(R.string.current_price_percent), value.toInt()) etTargetPrice.setText( targetPrice.toString() ) - viewModel.updateTargetPrice(targetPrice) + setTargetPriceViewModel.updateTargetPrice(targetPrice) } private fun handleEvent() { repeatOnStarted { - viewModel.event.collect { event -> + setTargetPriceViewModel.event.collect { event -> when (event) { is SetTargetPriceEvent.SuccessProductAdd -> { showActivityFinishDialog( @@ -204,18 +222,28 @@ class SetTargetPriceFragment : Fragment() { MaterialAlertDialogBuilder(requireActivity(), R.style.ThemeOverlay_App_MaterialAlertDialog) .setTitle(title) .setMessage(message) - .setPositiveButton(R.string.confirm) { _, _ -> requireActivity().finish() } - .setOnDismissListener { requireActivity().finish() } + .setPositiveButton(R.string.confirm) { _, _ -> goToHomeActivity() } + .setOnDismissListener { goToHomeActivity() } .create() .show() } + private fun goToHomeActivity() { + val activityIntent = requireActivity().intent + if (activityIntent?.action == Intent.ACTION_SEND) { + val intent = Intent(requireActivity(), HomeActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + startActivity(intent) + } + requireActivity().finish() + } + private fun FragmentSetTargetPriceBinding.updateSlideValueWithPrice( targetPrice: Float, percent: Int ) { val pricePercent = percent.coerceIn(MIN_PERCENT, MAX_PERCENT) - if (targetPrice > viewModel.state.value.productPrice) { + if (targetPrice > setTargetPriceViewModel.state.value.productPrice) { tvTargetPricePercent.text = getString(R.string.over_current_price) } slTargetPrice.value = pricePercent.toFloat() diff --git a/android/app/src/main/java/app/priceguard/ui/additem/setprice/SetTargetPriceViewModel.kt b/android/app/src/main/java/app/priceguard/ui/additem/setprice/SetTargetPriceViewModel.kt index 7a95341..5120522 100644 --- a/android/app/src/main/java/app/priceguard/ui/additem/setprice/SetTargetPriceViewModel.kt +++ b/android/app/src/main/java/app/priceguard/ui/additem/setprice/SetTargetPriceViewModel.kt @@ -2,11 +2,9 @@ package app.priceguard.ui.additem.setprice import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import app.priceguard.data.dto.PricePatchRequest -import app.priceguard.data.dto.ProductAddRequest -import app.priceguard.data.dto.ProductErrorState -import app.priceguard.data.network.ProductRepositoryResult -import app.priceguard.data.repository.ProductRepository +import app.priceguard.data.repository.RepositoryResult +import app.priceguard.data.repository.product.ProductErrorState +import app.priceguard.data.repository.product.ProductRepository import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.MutableSharedFlow @@ -42,18 +40,16 @@ class SetTargetPriceViewModel @Inject constructor(private val productRepository: fun addProduct() { viewModelScope.launch { val response = productRepository.addProduct( - ProductAddRequest( - _state.value.productCode, - _state.value.targetPrice - ) + _state.value.productCode, + _state.value.targetPrice ) when (response) { - is ProductRepositoryResult.Success -> { + is RepositoryResult.Success -> { _event.emit(SetTargetPriceEvent.SuccessProductAdd) } - is ProductRepositoryResult.Error -> { - _event.emit(SetTargetPriceEvent.FailurePriceAdd(response.productErrorState)) + is RepositoryResult.Error -> { + _event.emit(SetTargetPriceEvent.FailurePriceAdd(response.errorState)) } } } @@ -62,18 +58,16 @@ class SetTargetPriceViewModel @Inject constructor(private val productRepository: fun patchProduct() { viewModelScope.launch { val response = productRepository.updateTargetPrice( - PricePatchRequest( - _state.value.productCode, - _state.value.targetPrice - ) + _state.value.productCode, + _state.value.targetPrice ) when (response) { - is ProductRepositoryResult.Success -> { + is RepositoryResult.Success -> { _event.emit(SetTargetPriceEvent.SuccessPriceUpdate) } - is ProductRepositoryResult.Error -> { - _event.emit(SetTargetPriceEvent.FailurePriceUpdate(response.productErrorState)) + is RepositoryResult.Error -> { + _event.emit(SetTargetPriceEvent.FailurePriceUpdate(response.errorState)) } } } diff --git a/android/app/src/main/java/app/priceguard/ui/data/LoginResult.kt b/android/app/src/main/java/app/priceguard/ui/data/LoginResult.kt new file mode 100644 index 0000000..e8924ce --- /dev/null +++ b/android/app/src/main/java/app/priceguard/ui/data/LoginResult.kt @@ -0,0 +1,6 @@ +package app.priceguard.ui.data + +data class LoginResult( + val accessToken: String, + val refreshToken: String +) diff --git a/android/app/src/main/java/app/priceguard/ui/data/PricePatchResult.kt b/android/app/src/main/java/app/priceguard/ui/data/PricePatchResult.kt new file mode 100644 index 0000000..8f87999 --- /dev/null +++ b/android/app/src/main/java/app/priceguard/ui/data/PricePatchResult.kt @@ -0,0 +1,6 @@ +package app.priceguard.ui.data + +data class PricePatchResult( + val statusCode: Int, + val message: String +) diff --git a/android/app/src/main/java/app/priceguard/ui/data/ProductAddResult.kt b/android/app/src/main/java/app/priceguard/ui/data/ProductAddResult.kt new file mode 100644 index 0000000..f329760 --- /dev/null +++ b/android/app/src/main/java/app/priceguard/ui/data/ProductAddResult.kt @@ -0,0 +1,6 @@ +package app.priceguard.ui.data + +data class ProductAddResult( + val statusCode: Int, + val message: String +) diff --git a/android/app/src/main/java/app/priceguard/ui/data/ProductData.kt b/android/app/src/main/java/app/priceguard/ui/data/ProductData.kt new file mode 100644 index 0000000..4313894 --- /dev/null +++ b/android/app/src/main/java/app/priceguard/ui/data/ProductData.kt @@ -0,0 +1,14 @@ +package app.priceguard.ui.data + +import app.priceguard.data.graph.ProductChartData + +data class ProductData( + val productName: String, + val productCode: String, + val shop: String, + val imageUrl: String, + val targetPrice: Int, + val price: Int, + val isAlert: Boolean, + val priceData: List +) diff --git a/android/app/src/main/java/app/priceguard/ui/data/ProductDetailResult.kt b/android/app/src/main/java/app/priceguard/ui/data/ProductDetailResult.kt new file mode 100644 index 0000000..5d7df31 --- /dev/null +++ b/android/app/src/main/java/app/priceguard/ui/data/ProductDetailResult.kt @@ -0,0 +1,16 @@ +package app.priceguard.ui.data + +import app.priceguard.data.graph.ProductChartData + +data class ProductDetailResult( + val productName: String, + val productCode: String, + val shop: String, + val imageUrl: String, + val rank: Int, + val shopUrl: String, + val targetPrice: Int, + val lowestPrice: Int, + val price: Int, + val priceData: List +) diff --git a/android/app/src/main/java/app/priceguard/ui/data/ProductVerifyResult.kt b/android/app/src/main/java/app/priceguard/ui/data/ProductVerifyResult.kt new file mode 100644 index 0000000..1e2bd89 --- /dev/null +++ b/android/app/src/main/java/app/priceguard/ui/data/ProductVerifyResult.kt @@ -0,0 +1,9 @@ +package app.priceguard.ui.data + +data class ProductVerifyResult( + val productName: String, + val productCode: String, + val productPrice: Int, + val shop: String, + val imageUrl: String +) diff --git a/android/app/src/main/java/app/priceguard/ui/data/RecommendProductData.kt b/android/app/src/main/java/app/priceguard/ui/data/RecommendProductData.kt new file mode 100644 index 0000000..85e5ce2 --- /dev/null +++ b/android/app/src/main/java/app/priceguard/ui/data/RecommendProductData.kt @@ -0,0 +1,13 @@ +package app.priceguard.ui.data + +import app.priceguard.data.graph.ProductChartData + +data class RecommendProductData( + val productName: String, + val productCode: String, + val shop: String, + val imageUrl: String, + val price: Int, + val rank: Int, + val priceData: List +) diff --git a/android/app/src/main/java/app/priceguard/ui/data/SignupResult.kt b/android/app/src/main/java/app/priceguard/ui/data/SignupResult.kt new file mode 100644 index 0000000..b4697a8 --- /dev/null +++ b/android/app/src/main/java/app/priceguard/ui/data/SignupResult.kt @@ -0,0 +1,6 @@ +package app.priceguard.ui.data + +data class SignupResult( + val accessToken: String, + val refreshToken: String +) diff --git a/android/app/src/main/java/app/priceguard/ui/data/UserDataResult.kt b/android/app/src/main/java/app/priceguard/ui/data/UserDataResult.kt new file mode 100644 index 0000000..37290ce --- /dev/null +++ b/android/app/src/main/java/app/priceguard/ui/data/UserDataResult.kt @@ -0,0 +1,6 @@ +package app.priceguard.ui.data + +data class UserDataResult( + val email: String, + val name: String +) diff --git a/android/app/src/main/java/app/priceguard/ui/detail/DetailActivity.kt b/android/app/src/main/java/app/priceguard/ui/detail/DetailActivity.kt index 189c358..e34804e 100644 --- a/android/app/src/main/java/app/priceguard/ui/detail/DetailActivity.kt +++ b/android/app/src/main/java/app/priceguard/ui/detail/DetailActivity.kt @@ -5,15 +5,18 @@ import android.content.Intent import android.net.Uri import android.os.Bundle import android.widget.Toast +import androidx.activity.addCallback import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import app.priceguard.R -import app.priceguard.data.dto.ProductErrorState +import app.priceguard.data.graph.ProductChartDataset import app.priceguard.data.graph.ProductChartGridLine -import app.priceguard.data.repository.TokenRepository +import app.priceguard.data.repository.product.ProductErrorState +import app.priceguard.data.repository.token.TokenRepository import app.priceguard.databinding.ActivityDetailBinding import app.priceguard.materialchart.data.GraphMode import app.priceguard.ui.additem.AddItemActivity +import app.priceguard.ui.home.HomeActivity import app.priceguard.ui.util.lifecycle.repeatOnStarted import app.priceguard.ui.util.ui.showConfirmationDialog import app.priceguard.ui.util.ui.showPermissionDeniedDialog @@ -36,13 +39,19 @@ class DetailActivity : AppCompatActivity() { binding.viewModel = productDetailViewModel setContentView(binding.root) + setBackPressedCallback() initListener() setNavigationButton() - setGraph() - checkProductCode() + checkProductCode(intent) observeEvent() } + private fun setBackPressedCallback() { + onBackPressedDispatcher.addCallback(this) { + goToHomeActivityIfDeepLinked() + } + } + override fun onStart() { super.onStart() if (productDetailViewModel.state.value.isReady) { @@ -90,15 +99,48 @@ class DetailActivity : AppCompatActivity() { } } } + + binding.btnDetailShare.setOnClickListener { + val shareLink = + getString(R.string.share_link_template, productDetailViewModel.productCode) + + val sendIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_TITLE, getString(R.string.share_product)) + putExtra(Intent.EXTRA_TEXT, getString(R.string.share_message_template, shareLink)) + type = "text/plain" + } + + val shareIntent = Intent.createChooser(sendIntent, null) + startActivity(shareIntent) + } + } + + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + if (intent != null) { + checkProductCode(intent) + } } - private fun checkProductCode() { + private fun checkProductCode(intent: Intent) { val productCode = intent.getStringExtra("productCode") - if (productCode == null) { - // Invalid access + val deepLink = intent.data + val productCodeFromDeepLink = deepLink?.getQueryParameter("code") + + if (productCode == null && productCodeFromDeepLink == null) { showDialogAndExit(getString(R.string.error), getString(R.string.invalid_access)) - } else { - productDetailViewModel.productCode = productCode + return + } + + productCode?.let { code -> + productDetailViewModel.productCode = code + productDetailViewModel.getDetails(false) + return + } + + productCodeFromDeepLink?.let { code -> + productDetailViewModel.productCode = code productDetailViewModel.getDetails(false) } } @@ -106,17 +148,16 @@ class DetailActivity : AppCompatActivity() { private fun observeEvent() { repeatOnStarted { productDetailViewModel.state.collect { state -> - binding.chGraphDetail.dataset = state.chartData?.copy( - gridLines = listOf( - ProductChartGridLine( - resources.getString(R.string.target_price), - state.targetPrice?.toFloat() ?: 0F - ), - ProductChartGridLine( - resources.getString(R.string.lowest_price), - state.lowestPrice?.toFloat() ?: 0F - ) - ) + state.targetPrice ?: return@collect + binding.chGraphDetail.dataset = ProductChartDataset( + showXAxis = true, + showYAxis = true, + isInteractive = true, + graphMode = state.graphMode, + xLabel = getString(R.string.date_text), + yLabel = getString(R.string.price_text), + data = state.chartData, + gridLines = getGridLines(state.targetPrice.toFloat()) ) } } @@ -193,14 +234,32 @@ class DetailActivity : AppCompatActivity() { } } + private fun getGridLines(targetPrice: Float): List { + return if (targetPrice < 0) { + listOf() + } else { + listOf( + ProductChartGridLine( + resources.getString(R.string.target_price), + targetPrice + ) + ) + } + } + private fun setNavigationButton() { binding.mtDetailTopbar.setNavigationOnClickListener { - finish() + goToHomeActivityIfDeepLinked() } } - private fun setGraph() { - binding.chGraphDetail.setXAxisMargin(48F) + private fun goToHomeActivityIfDeepLinked() { + if (intent.data?.getQueryParameter("code") != null || intent.getBooleanExtra("directed", false)) { + val intent = Intent(this@DetailActivity, HomeActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + startActivity(intent) + } + finish() } private fun showConfirmationDialog( diff --git a/android/app/src/main/java/app/priceguard/ui/detail/ProductDetailViewModel.kt b/android/app/src/main/java/app/priceguard/ui/detail/ProductDetailViewModel.kt index d420144..a51de14 100644 --- a/android/app/src/main/java/app/priceguard/ui/detail/ProductDetailViewModel.kt +++ b/android/app/src/main/java/app/priceguard/ui/detail/ProductDetailViewModel.kt @@ -2,10 +2,11 @@ package app.priceguard.ui.detail import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import app.priceguard.data.dto.ProductErrorState -import app.priceguard.data.graph.ProductChartDataset -import app.priceguard.data.network.ProductRepositoryResult -import app.priceguard.data.repository.ProductRepository +import app.priceguard.data.GraphDataConverter +import app.priceguard.data.graph.ProductChartData +import app.priceguard.data.repository.RepositoryResult +import app.priceguard.data.repository.product.ProductErrorState +import app.priceguard.data.repository.product.ProductRepository import app.priceguard.materialchart.data.GraphMode import dagger.hilt.android.lifecycle.HiltViewModel import java.text.NumberFormat @@ -20,8 +21,10 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @HiltViewModel -class ProductDetailViewModel @Inject constructor(val productRepository: ProductRepository) : - ViewModel() { +class ProductDetailViewModel @Inject constructor( + private val productRepository: ProductRepository, + private val graphDataConverter: GraphDataConverter +) : ViewModel() { data class ProductDetailUIState( val isTracking: Boolean = false, @@ -38,8 +41,8 @@ class ProductDetailViewModel @Inject constructor(val productRepository: ProductR val formattedPrice: String = "", val formattedTargetPrice: String = "", val formattedLowestPrice: String = "", - val chartPeriod: GraphMode = GraphMode.DAY, - val chartData: ProductChartDataset? = null + val graphMode: GraphMode = GraphMode.DAY, + val chartData: List = listOf() ) sealed class ProductDetailEvent { @@ -53,6 +56,7 @@ class ProductDetailViewModel @Inject constructor(val productRepository: ProductR } lateinit var productCode: String + private var productGraphData: List = listOf() private var _event: MutableSharedFlow = MutableSharedFlow() val event: SharedFlow = _event.asSharedFlow() @@ -64,12 +68,12 @@ class ProductDetailViewModel @Inject constructor(val productRepository: ProductR fun deleteProductTracking() { viewModelScope.launch { when (val result = productRepository.deleteProduct(productCode)) { - is ProductRepositoryResult.Success -> { + is RepositoryResult.Success -> { _event.emit(ProductDetailEvent.DeleteSuccess) } - is ProductRepositoryResult.Error -> { - _event.emit(ProductDetailEvent.DeleteFailed(result.productErrorState)) + is RepositoryResult.Error -> { + _event.emit(ProductDetailEvent.DeleteFailed(result.errorState)) } } } @@ -90,7 +94,8 @@ class ProductDetailViewModel @Inject constructor(val productRepository: ProductR _state.value = _state.value.copy(isRefreshing = false) when (result) { - is ProductRepositoryResult.Success -> { + is RepositoryResult.Success -> { + productGraphData = result.data.priceData _state.update { it.copy( isReady = true, @@ -112,21 +117,16 @@ class ProductDetailViewModel @Inject constructor(val productRepository: ProductR ) }, formattedLowestPrice = formatPrice(result.data.lowestPrice), - chartPeriod = GraphMode.DAY, - chartData = ProductChartDataset( - showXAxis = true, - showYAxis = true, - isInteractive = true, - graphMode = GraphMode.DAY, - data = result.data.priceData, - gridLines = listOf() + chartData = graphDataConverter.packWithEdgeData( + result.data.priceData, + state.value.graphMode ) ) } } - is ProductRepositoryResult.Error -> { - when (result.productErrorState) { + is RepositoryResult.Error -> { + when (result.errorState) { ProductErrorState.PERMISSION_DENIED -> { _event.emit(ProductDetailEvent.Logout) } @@ -145,17 +145,14 @@ class ProductDetailViewModel @Inject constructor(val productRepository: ProductR } fun changePeriod(period: GraphMode) { + if (productGraphData.isEmpty()) { + return + } + _state.update { it.copy( - chartPeriod = period, - chartData = ProductChartDataset( - showXAxis = true, - showYAxis = true, - isInteractive = true, - graphMode = period, - data = it.chartData?.data ?: listOf(), - gridLines = listOf() - ) + graphMode = period, + chartData = graphDataConverter.packWithEdgeData(productGraphData, period) ) } } diff --git a/android/app/src/main/java/app/priceguard/ui/home/HomeActivity.kt b/android/app/src/main/java/app/priceguard/ui/home/HomeActivity.kt index 7ffeec5..e3c789f 100644 --- a/android/app/src/main/java/app/priceguard/ui/home/HomeActivity.kt +++ b/android/app/src/main/java/app/priceguard/ui/home/HomeActivity.kt @@ -1,26 +1,138 @@ package app.priceguard.ui.home +import android.Manifest +import android.content.pm.PackageManager +import android.os.Build import android.os.Bundle +import android.util.Log +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.setupWithNavController +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager +import app.priceguard.R import app.priceguard.databinding.ActivityHomeBinding +import app.priceguard.service.UpdateTokenWorker +import app.priceguard.ui.util.ui.openNotificationSettings +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.GoogleApiAvailability +import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint +import java.util.concurrent.TimeUnit @AndroidEntryPoint class HomeActivity : AppCompatActivity() { private lateinit var binding: ActivityHomeBinding + private lateinit var snackbar: Snackbar override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityHomeBinding.inflate(layoutInflater) setContentView(binding.root) + + enqueueWorker() + initSnackBar() + checkForGooglePlayServices() setBottomNavigationBar() + askNotificationPermission() + } + + override fun onResume() { + super.onResume() + checkForGooglePlayServices() + + if (ContextCompat.checkSelfPermission( + this, + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + ) { + dismissSnackbar() + } else { + showNotificationOffSnackbar() + } + } + + private fun enqueueWorker() { + val saveRequest = + PeriodicWorkRequestBuilder(730, TimeUnit.HOURS) + .build() + + WorkManager.getInstance(this).enqueueUniquePeriodicWork( + "saveRequest", + ExistingPeriodicWorkPolicy.UPDATE, + saveRequest + ) + } + + private fun checkForGooglePlayServices() { + val availability = GoogleApiAvailability().isGooglePlayServicesAvailable(this) + + if (availability != ConnectionResult.SUCCESS) { + GoogleApiAvailability().makeGooglePlayServicesAvailable(this) + } + } + + private fun initSnackBar() { + snackbar = Snackbar.make( + binding.root, + getString(R.string.currently_notification_disabled), + Snackbar.LENGTH_INDEFINITE + ).setAction(getString(R.string.setting)) { + openNotificationSettings() + }.setAnchorView(binding.bottomNavigation) } private fun setBottomNavigationBar() { val navController = binding.navHostHome.getFragment().navController binding.bottomNavigation.setupWithNavController(navController) } + + private val requestPermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted: Boolean -> + if (isGranted) { + Log.d("NOTIFICATION", "PERMISSION GRANTED") + } else { + showNotificationOffSnackbar() + } + } + + private fun askNotificationPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + when { + ContextCompat.checkSelfPermission( + this, + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED -> { + // Allowed + Log.d("NOTIFICATION", "PERMISSION GRANTED") + } + + shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS) -> { + // Explicitly denied + showNotificationOffSnackbar() + } + + else -> { + // Initial cases + requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + } + } + } + } + + private fun showNotificationOffSnackbar() { + if (snackbar.isShown) return + snackbar.show() + } + + private fun dismissSnackbar() { + if (snackbar.isShown) { + snackbar.dismiss() + } + } } diff --git a/android/app/src/main/java/app/priceguard/ui/home/ProductSummaryAdapter.kt b/android/app/src/main/java/app/priceguard/ui/home/ProductSummaryAdapter.kt index ac02f9d..3590c0a 100644 --- a/android/app/src/main/java/app/priceguard/ui/home/ProductSummaryAdapter.kt +++ b/android/app/src/main/java/app/priceguard/ui/home/ProductSummaryAdapter.kt @@ -1,6 +1,5 @@ package app.priceguard.ui.home -import android.content.Intent import android.util.TypedValue import android.view.LayoutInflater import android.view.View @@ -13,15 +12,22 @@ import app.priceguard.data.graph.ProductChartData import app.priceguard.data.graph.ProductChartDataset import app.priceguard.databinding.ItemProductSummaryBinding import app.priceguard.materialchart.data.GraphMode -import app.priceguard.ui.detail.DetailActivity -class ProductSummaryAdapter : +class ProductSummaryAdapter(private val productSummaryClickListener: ProductSummaryClickListener) : ListAdapter(diffUtil) { + init { + setHasStableIds(true) + } + + override fun getItemId(position: Int): Long { + return getItem(position).productCode.toLong() + } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val binding = ItemProductSummaryBinding.inflate(LayoutInflater.from(parent.context), parent, false) - return ViewHolder(binding) + return ViewHolder(binding, productSummaryClickListener) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { @@ -29,7 +35,10 @@ class ProductSummaryAdapter : holder.bind(item) } - class ViewHolder(private val binding: ItemProductSummaryBinding) : + class ViewHolder( + private val binding: ItemProductSummaryBinding, + private val productSummaryClickListener: ProductSummaryClickListener + ) : RecyclerView.ViewHolder(binding.root) { fun bind(item: ProductSummary) { @@ -53,29 +62,30 @@ class ProductSummaryAdapter : is ProductSummary.UserProductSummary -> { tvProductRecommendRank.visibility = View.GONE msProduct.visibility = View.VISIBLE + msProduct.isChecked = item.isAlarmOn tvProductDiscountPercent.visibility = View.VISIBLE - setDisCount(item.discountPercent) - setSwitchListener() + setDiscount(item.discountPercent) + setSwitchListener(item) } } } - private fun ItemProductSummaryBinding.setSwitchListener() { + private fun ItemProductSummaryBinding.setSwitchListener(item: ProductSummary) { if (msProduct.isChecked.not()) { msProduct.setThumbIconResource(R.drawable.ic_notifications_off) } msProduct.setOnCheckedChangeListener { _, isChecked -> + productSummaryClickListener.onToggle(item.productCode, msProduct.isChecked) if (isChecked) { - // TODO: 푸쉬 μ•ŒλžŒ μ„€μ • μΆ”κ°€ msProduct.setThumbIconResource(R.drawable.ic_notifications_active) } else { - // TODO: 푸쉬 μ•ŒλžŒ μ„€μ • 제거 msProduct.setThumbIconResource(R.drawable.ic_notifications_off) } } + msProduct.contentDescription = msProduct.context.getString(R.string.single_product_notification_toggle, item.title) } - private fun ItemProductSummaryBinding.setDisCount(discount: Float) { + private fun ItemProductSummaryBinding.setDiscount(discount: Float) { tvProductDiscountPercent.text = if (discount > 0) { tvProductDiscountPercent.context.getString( @@ -95,19 +105,19 @@ class ProductSummaryAdapter : true ) tvProductDiscountPercent.setTextColor(value.data) + tvProductDiscountPercent.contentDescription = tvProductDiscountPercent.context.getString(R.string.target_price_delta, tvProductDiscountPercent.text) } private fun ItemProductSummaryBinding.setRecommendRank(item: ProductSummary.RecommendedProductSummary) { tvProductRecommendRank.text = tvProductRecommendRank.context.getString( R.string.recommand_rank, item.recommendRank ) + tvProductRecommendRank.contentDescription = tvProductRecommendRank.context.getString(R.string.current_rank_info, item.recommendRank) } private fun ItemProductSummaryBinding.setClickListener(code: String) { cvProduct.setOnClickListener { - val intent = Intent(binding.root.context, DetailActivity::class.java) - intent.putExtra("productCode", code) - binding.root.context.startActivity(intent) + productSummaryClickListener.onClick(code) } } @@ -116,7 +126,9 @@ class ProductSummaryAdapter : showXAxis = false, showYAxis = false, isInteractive = false, - graphMode = GraphMode.DAY, + graphMode = GraphMode.WEEK, + xLabel = chGraph.context.getString(R.string.date_text), + yLabel = chGraph.context.getString(R.string.price_text), data = data, gridLines = listOf() ) @@ -129,7 +141,7 @@ class ProductSummaryAdapter : oldItem == newItem override fun areItemsTheSame(oldItem: ProductSummary, newItem: ProductSummary) = - oldItem.hashCode() == newItem.hashCode() + oldItem.productCode == newItem.productCode } } } diff --git a/android/app/src/main/java/app/priceguard/ui/home/ProductSummaryClickListener.kt b/android/app/src/main/java/app/priceguard/ui/home/ProductSummaryClickListener.kt new file mode 100644 index 0000000..871d2a5 --- /dev/null +++ b/android/app/src/main/java/app/priceguard/ui/home/ProductSummaryClickListener.kt @@ -0,0 +1,7 @@ +package app.priceguard.ui.home + +interface ProductSummaryClickListener { + fun onClick(productCode: String) + + fun onToggle(productCode: String, checked: Boolean) +} diff --git a/android/app/src/main/java/app/priceguard/ui/home/list/ProductListFragment.kt b/android/app/src/main/java/app/priceguard/ui/home/list/ProductListFragment.kt index f1e0abb..d10b195 100644 --- a/android/app/src/main/java/app/priceguard/ui/home/list/ProductListFragment.kt +++ b/android/app/src/main/java/app/priceguard/ui/home/list/ProductListFragment.kt @@ -8,12 +8,17 @@ import android.view.ViewGroup import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.SimpleItemAnimator +import androidx.work.WorkManager import app.priceguard.R -import app.priceguard.data.dto.ProductErrorState -import app.priceguard.data.repository.TokenRepository +import app.priceguard.data.repository.product.ProductErrorState +import app.priceguard.data.repository.token.TokenRepository import app.priceguard.databinding.FragmentProductListBinding +import app.priceguard.service.UpdateAlarmWorker import app.priceguard.ui.additem.AddItemActivity +import app.priceguard.ui.detail.DetailActivity import app.priceguard.ui.home.ProductSummaryAdapter +import app.priceguard.ui.home.ProductSummaryClickListener import app.priceguard.ui.util.lifecycle.repeatOnStarted import app.priceguard.ui.util.ui.disableAppBarRecyclerView import app.priceguard.ui.util.ui.showConfirmationDialog @@ -31,6 +36,8 @@ class ProductListFragment : Fragment() { private val binding get() = _binding!! private val productListViewModel: ProductListViewModel by viewModels() + private var workRequestSet: MutableSet = mutableSetOf() + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -59,7 +66,29 @@ class ProductListFragment : Fragment() { } private fun FragmentProductListBinding.initSettingAdapter() { - val adapter = ProductSummaryAdapter() + val animator = rvProductList.itemAnimator + if (animator is SimpleItemAnimator) { + animator.supportsChangeAnimations = false + } + + val listener = object : ProductSummaryClickListener { + override fun onClick(productCode: String) { + val intent = Intent(context, DetailActivity::class.java) + intent.putExtra("productCode", productCode) + startActivity(intent) + } + + override fun onToggle(productCode: String, checked: Boolean) { + productListViewModel.updateProductAlarmToggle(productCode, checked) + if (workRequestSet.contains(productCode)) { + workRequestSet.remove(productCode) + } else { + workRequestSet.add(productCode) + } + } + } + + val adapter = ProductSummaryAdapter(listener) rvProductList.adapter = adapter this@ProductListFragment.repeatOnStarted { productListViewModel.productList.collect { list -> @@ -84,7 +113,7 @@ class ProductListFragment : Fragment() { } private fun collectEvent() { - repeatOnStarted { + viewLifecycleOwner.repeatOnStarted { productListViewModel.events.collect { event -> when (event) { ProductErrorState.PERMISSION_DENIED -> { @@ -116,6 +145,15 @@ class ProductListFragment : Fragment() { } } + override fun onStop() { + super.onStop() + workRequestSet.forEach { productCode -> + WorkManager.getInstance(requireContext()) + .enqueue(UpdateAlarmWorker.createWorkRequest(productCode)) + } + workRequestSet.clear() + } + override fun onDestroyView() { super.onDestroyView() _binding = null diff --git a/android/app/src/main/java/app/priceguard/ui/home/list/ProductListViewModel.kt b/android/app/src/main/java/app/priceguard/ui/home/list/ProductListViewModel.kt index 194e045..5da268c 100644 --- a/android/app/src/main/java/app/priceguard/ui/home/list/ProductListViewModel.kt +++ b/android/app/src/main/java/app/priceguard/ui/home/list/ProductListViewModel.kt @@ -2,9 +2,11 @@ package app.priceguard.ui.home.list import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import app.priceguard.data.dto.ProductErrorState -import app.priceguard.data.network.ProductRepositoryResult -import app.priceguard.data.repository.ProductRepository +import app.priceguard.data.GraphDataConverter +import app.priceguard.data.repository.RepositoryResult +import app.priceguard.data.repository.product.ProductErrorState +import app.priceguard.data.repository.product.ProductRepository +import app.priceguard.materialchart.data.GraphMode import app.priceguard.ui.home.ProductSummary.UserProductSummary import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @@ -19,7 +21,8 @@ import kotlinx.coroutines.launch @HiltViewModel class ProductListViewModel @Inject constructor( - private val productRepository: ProductRepository + private val productRepository: ProductRepository, + private val graphDataConverter: GraphDataConverter ) : ViewModel() { private var _isRefreshing: MutableStateFlow = MutableStateFlow(false) @@ -42,28 +45,38 @@ class ProductListViewModel @Inject constructor( _isRefreshing.value = false when (result) { - is ProductRepositoryResult.Success -> { + is RepositoryResult.Success -> { _productList.value = result.data.map { data -> UserProductSummary( data.shop, data.productName, data.price, data.productCode, - data.priceData, + graphDataConverter.packWithEdgeData(data.priceData, GraphMode.WEEK), calculateDiscountRate(data.targetPrice, data.price), - true + data.isAlert ) } } - is ProductRepositoryResult.Error -> { - _events.emit(result.productErrorState) + is RepositoryResult.Error -> { + _events.emit(result.errorState) } } } } + fun updateProductAlarmToggle(productCode: String, checked: Boolean) { + _productList.value = productList.value.mapIndexed { _, product -> + if (product.productCode == productCode) { + product.copy(isAlarmOn = checked) + } else { + product + } + } + } + private fun calculateDiscountRate(targetPrice: Int, price: Int): Float { - return round((price - targetPrice).toFloat() / (if (targetPrice == 0) 1 else targetPrice) * 1000) / 10 + return round((price - targetPrice).toFloat() / (if (price == 0) 1 else price) * 1000) / 10 } } diff --git a/android/app/src/main/java/app/priceguard/ui/home/mypage/MyPageFragment.kt b/android/app/src/main/java/app/priceguard/ui/home/mypage/MyPageFragment.kt index 2392312..7ee3e6d 100644 --- a/android/app/src/main/java/app/priceguard/ui/home/mypage/MyPageFragment.kt +++ b/android/app/src/main/java/app/priceguard/ui/home/mypage/MyPageFragment.kt @@ -8,12 +8,14 @@ import android.view.ViewGroup import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController import app.priceguard.R -import app.priceguard.data.repository.TokenRepository +import app.priceguard.data.repository.token.TokenRepository import app.priceguard.databinding.FragmentMyPageBinding import app.priceguard.ui.home.mypage.MyPageViewModel.MyPageEvent import app.priceguard.ui.intro.IntroActivity import app.priceguard.ui.util.lifecycle.repeatOnStarted +import app.priceguard.ui.util.ui.openNotificationSettings import com.google.android.gms.oss.licenses.OssLicensesMenuActivity import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint @@ -62,11 +64,11 @@ class MyPageFragment : Fragment() { override fun onClick(setting: Setting) { when (setting) { Setting.NOTIFICATION -> { - // TODO: μ•ŒλžŒ μ„€μ • + requireContext().openNotificationSettings() } Setting.THEME -> { - // TODO: ν…Œλ§ˆ μ„€μ • + findNavController().navigate(R.id.action_myPageFragment_to_themeDialogFragment) } Setting.LICENSE -> { diff --git a/android/app/src/main/java/app/priceguard/ui/home/mypage/MyPageViewModel.kt b/android/app/src/main/java/app/priceguard/ui/home/mypage/MyPageViewModel.kt index c553dd2..3ec3342 100644 --- a/android/app/src/main/java/app/priceguard/ui/home/mypage/MyPageViewModel.kt +++ b/android/app/src/main/java/app/priceguard/ui/home/mypage/MyPageViewModel.kt @@ -2,7 +2,7 @@ package app.priceguard.ui.home.mypage import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import app.priceguard.data.repository.TokenRepository +import app.priceguard.data.repository.token.TokenRepository import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.MutableSharedFlow diff --git a/android/app/src/main/java/app/priceguard/ui/home/recommend/RecommendedProductFragment.kt b/android/app/src/main/java/app/priceguard/ui/home/recommend/RecommendedProductFragment.kt index fbbc963..bee82f2 100644 --- a/android/app/src/main/java/app/priceguard/ui/home/recommend/RecommendedProductFragment.kt +++ b/android/app/src/main/java/app/priceguard/ui/home/recommend/RecommendedProductFragment.kt @@ -1,5 +1,6 @@ package app.priceguard.ui.home.recommend +import android.content.Intent import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -8,10 +9,12 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import app.priceguard.R -import app.priceguard.data.dto.ProductErrorState -import app.priceguard.data.repository.TokenRepository +import app.priceguard.data.repository.product.ProductErrorState +import app.priceguard.data.repository.token.TokenRepository import app.priceguard.databinding.FragmentRecommendedProductBinding +import app.priceguard.ui.detail.DetailActivity import app.priceguard.ui.home.ProductSummaryAdapter +import app.priceguard.ui.home.ProductSummaryClickListener import app.priceguard.ui.util.lifecycle.repeatOnStarted import app.priceguard.ui.util.ui.disableAppBarRecyclerView import app.priceguard.ui.util.ui.showConfirmationDialog @@ -57,7 +60,19 @@ class RecommendedProductFragment : Fragment() { } private fun FragmentRecommendedProductBinding.initSettingAdapter() { - val adapter = ProductSummaryAdapter() + val listener = object : ProductSummaryClickListener { + override fun onClick(productCode: String) { + val intent = Intent(context, DetailActivity::class.java) + intent.putExtra("productCode", productCode) + startActivity(intent) + } + + override fun onToggle(productCode: String, checked: Boolean) { + return + } + } + + val adapter = ProductSummaryAdapter(listener) rvRecommendedProduct.adapter = adapter this@RecommendedProductFragment.repeatOnStarted { recommendedProductViewModel.recommendedProductList.collect { list -> diff --git a/android/app/src/main/java/app/priceguard/ui/home/recommend/RecommendedProductViewModel.kt b/android/app/src/main/java/app/priceguard/ui/home/recommend/RecommendedProductViewModel.kt index 49d81b5..a3a9d34 100644 --- a/android/app/src/main/java/app/priceguard/ui/home/recommend/RecommendedProductViewModel.kt +++ b/android/app/src/main/java/app/priceguard/ui/home/recommend/RecommendedProductViewModel.kt @@ -2,9 +2,11 @@ package app.priceguard.ui.home.recommend import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import app.priceguard.data.dto.ProductErrorState -import app.priceguard.data.network.ProductRepositoryResult -import app.priceguard.data.repository.ProductRepository +import app.priceguard.data.GraphDataConverter +import app.priceguard.data.repository.RepositoryResult +import app.priceguard.data.repository.product.ProductErrorState +import app.priceguard.data.repository.product.ProductRepository +import app.priceguard.materialchart.data.GraphMode import app.priceguard.ui.home.ProductSummary.RecommendedProductSummary import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @@ -18,13 +20,10 @@ import kotlinx.coroutines.launch @HiltViewModel class RecommendedProductViewModel @Inject constructor( - private val productRepository: ProductRepository + private val productRepository: ProductRepository, + private val graphDataConverter: GraphDataConverter ) : ViewModel() { - sealed class RecommendedProductEvent { - data object PermissionDenied : RecommendedProductEvent() - } - private var _isRefreshing = MutableStateFlow(false) val isRefreshing: StateFlow = _isRefreshing.asStateFlow() @@ -46,21 +45,21 @@ class RecommendedProductViewModel @Inject constructor( _isRefreshing.value = false when (result) { - is ProductRepositoryResult.Success -> { + is RepositoryResult.Success -> { _recommendedProductList.value = result.data.map { data -> RecommendedProductSummary( data.shop, data.productName, data.price, data.productCode, - data.priceData, + graphDataConverter.packWithEdgeData(data.priceData, GraphMode.WEEK), data.rank ) } } - is ProductRepositoryResult.Error -> { - _events.emit(result.productErrorState) + is RepositoryResult.Error -> { + _events.emit(result.errorState) } } } diff --git a/android/app/src/main/java/app/priceguard/ui/home/theme/ThemeDialogFragment.kt b/android/app/src/main/java/app/priceguard/ui/home/theme/ThemeDialogFragment.kt new file mode 100644 index 0000000..58e8160 --- /dev/null +++ b/android/app/src/main/java/app/priceguard/ui/home/theme/ThemeDialogFragment.kt @@ -0,0 +1,124 @@ +package app.priceguard.ui.home.theme + +import android.app.Dialog +import android.os.Bundle +import androidx.appcompat.app.AppCompatDelegate +import androidx.fragment.app.DialogFragment +import androidx.lifecycle.lifecycleScope +import app.priceguard.R +import app.priceguard.data.datastore.ConfigDataSource +import app.priceguard.databinding.FragmentThemeDialogBinding +import app.priceguard.ui.PriceGuardApp +import com.google.android.material.color.DynamicColors +import com.google.android.material.color.DynamicColorsOptions +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class ThemeDialogFragment : DialogFragment() { + + @Inject + lateinit var configDataSource: ConfigDataSource + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val binding: FragmentThemeDialogBinding = + FragmentThemeDialogBinding.inflate(requireActivity().layoutInflater) + val view = binding.root + + setCheckedButton(binding) + + return MaterialAlertDialogBuilder( + requireActivity(), + R.style.ThemeOverlay_App_MaterialAlertDialog + ).apply { + setView(view) + setPositiveButton(R.string.confirm) { _, _ -> + val dynamicMode = when (binding.rgDynamicColor.checkedRadioButtonId) { + R.id.rb_yes -> { + DynamicColors.applyToActivitiesIfAvailable(requireActivity().application) + requireActivity().recreate() + PriceGuardApp.MODE_DYNAMIC + } + + else -> { + DynamicColors.applyToActivitiesIfAvailable( + requireActivity().application, + DynamicColorsOptions.Builder() + .setThemeOverlay(R.style.Theme_PriceGuard).build() + ) + requireActivity().recreate() + PriceGuardApp.MODE_DYNAMIC_NO + } + } + + val darkMode = when (binding.rgDarkMode.checkedRadioButtonId) { + R.id.rb_system -> { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) + PriceGuardApp.MODE_SYSTEM + } + + R.id.rb_light -> { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) + PriceGuardApp.MODE_LIGHT + } + + R.id.rb_dark -> { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) + PriceGuardApp.MODE_DARK + } + + else -> { + PriceGuardApp.MODE_SYSTEM + } + } + saveTheme(dynamicMode, darkMode) + dismiss() + } + }.create() + } + + private fun saveTheme(dynamicMode: Int, darkMode: Int) { + lifecycleScope.launch(Dispatchers.IO) { + configDataSource.saveDynamicMode(dynamicMode) + configDataSource.saveDarkMode(darkMode) + } + } + + private fun setCheckedButton(binding: FragmentThemeDialogBinding) { + lifecycleScope.launch { + val dynamicColorMode = configDataSource.getDynamicMode() + val darkMode = configDataSource.getDarkMode() + + binding.rgDynamicColor.check( + when (dynamicColorMode) { + PriceGuardApp.MODE_DYNAMIC -> { + R.id.rb_yes + } + + else -> { + R.id.rb_no + } + } + ) + + binding.rgDarkMode.check( + when (darkMode) { + PriceGuardApp.MODE_LIGHT -> { + R.id.rb_light + } + + PriceGuardApp.MODE_DARK -> { + R.id.rb_dark + } + + else -> { + R.id.rb_system + } + } + ) + } + } +} diff --git a/android/app/src/main/java/app/priceguard/ui/login/LoginActivity.kt b/android/app/src/main/java/app/priceguard/ui/login/LoginActivity.kt index b6a62f3..ed65567 100644 --- a/android/app/src/main/java/app/priceguard/ui/login/LoginActivity.kt +++ b/android/app/src/main/java/app/priceguard/ui/login/LoginActivity.kt @@ -2,6 +2,7 @@ package app.priceguard.ui.login import android.content.Intent import android.os.Bundle +import android.widget.Toast import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import app.priceguard.R @@ -45,16 +46,24 @@ class LoginActivity : AppCompatActivity() { private fun collectEvent() { repeatOnStarted { - loginViewModel.event.collect { eventType -> - when (eventType) { + loginViewModel.event.collect { event -> + when (event) { LoginEvent.LoginStart -> { (binding.btnLoginLogin as MaterialButton).icon = getCircularProgressIndicatorDrawable(this@LoginActivity) } + LoginEvent.TokenUpdateError, LoginEvent.FirebaseError -> { + Toast.makeText( + this@LoginActivity, + getString(R.string.push_notification_not_working), + Toast.LENGTH_LONG + ).show() + } + else -> { (binding.btnLoginLogin as MaterialButton).icon = null - setDialogMessageAndShow(eventType) + setDialogMessageAndShow(event) } } } @@ -74,6 +83,10 @@ class LoginActivity : AppCompatActivity() { showDialog(getString(R.string.login_fail), getString(R.string.login_fail_message)) } + is LoginEvent.UndefinedError -> { + showDialog(getString(R.string.login_fail), getString(R.string.undefined_error)) + } + is LoginEvent.LoginInfoSaved -> { gotoHomeActivity() } diff --git a/android/app/src/main/java/app/priceguard/ui/login/LoginViewModel.kt b/android/app/src/main/java/app/priceguard/ui/login/LoginViewModel.kt index 82069f3..737f9d5 100644 --- a/android/app/src/main/java/app/priceguard/ui/login/LoginViewModel.kt +++ b/android/app/src/main/java/app/priceguard/ui/login/LoginViewModel.kt @@ -3,9 +3,10 @@ package app.priceguard.ui.login import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import app.priceguard.data.dto.LoginState -import app.priceguard.data.repository.TokenRepository -import app.priceguard.data.repository.UserRepository +import app.priceguard.data.repository.RepositoryResult +import app.priceguard.data.repository.auth.AuthErrorState +import app.priceguard.data.repository.auth.AuthRepository +import app.priceguard.data.repository.token.TokenRepository import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.MutableSharedFlow @@ -18,7 +19,7 @@ import kotlinx.coroutines.launch @HiltViewModel class LoginViewModel @Inject constructor( - private val userRepository: UserRepository, + private val authRepository: AuthRepository, private val tokenRepository: TokenRepository ) : ViewModel() { @@ -32,29 +33,31 @@ class LoginViewModel @Inject constructor( sealed class LoginEvent { data object LoginStart : LoginEvent() data object Invalid : LoginEvent() - data class LoginSuccess(val accessToken: String, val refreshToken: String) : LoginEvent() - data class LoginFailure(val status: LoginState) : LoginEvent() + data object LoginFailure : LoginEvent() + data object UndefinedError : LoginEvent() data object LoginInfoSaved : LoginEvent() + data object FirebaseError : LoginEvent() + data object TokenUpdateError : LoginEvent() } private val emailPattern = """^[\w.+-]+@((?!-)[A-Za-z0-9-]{1,63}(?|\-\[\]\\/]*\d)(?=[A-Za-z\d!@#$%^&*()_+={};:'"~`,.?<>|\-\[\]\\/]*[a-z])(?=[A-Za-z\d!@#$%^&*()_+={};:'"~`,.?<>|\-\[\]\\/]*[A-Z])(?=[A-Za-z\d!@#$%^&*()_+={};:'"~`,.?<>|\-\[\]\\/]*[A-Za-z\d!@#$%^&*()_+={};:'"~`,.?<>|\-\[\]\\/])[A-Za-z\d!@#$%^&*()_+={};:'"~`,.?<>|\-\[\]\\/]{8,16}$""".toRegex() private var _event = MutableSharedFlow() val event: SharedFlow = _event.asSharedFlow() private val _state = MutableStateFlow(State()) var state: StateFlow = _state.asStateFlow() - fun setID(s: CharSequence, start: Int, before: Int, count: Int) { + fun setEmail(s: String) { if (_state.value.isLoading) return - _state.value = _state.value.copy(email = s.toString()) + _state.value = _state.value.copy(email = s) } - fun setPassword(s: CharSequence, start: Int, before: Int, count: Int) { + fun setPassword(s: String) { if (_state.value.isLoading) return - _state.value = _state.value.copy(password = s.toString()) + _state.value = _state.value.copy(password = s) } fun login() { @@ -73,30 +76,52 @@ class LoginViewModel @Inject constructor( return@launch } - val result = userRepository.login(_state.value.email, _state.value.password) + when (val result = authRepository.login(_state.value.email, _state.value.password)) { + is RepositoryResult.Success -> { + if (result.data.accessToken.isEmpty() || result.data.refreshToken.isEmpty()) { + sendLoginEvent(LoginEvent.UndefinedError) + setLoading(false) + return@launch + } - if (result.accessToken == null || result.refreshToken == null) { - sendLoginEvent(LoginEvent.LoginFailure(result.loginState)) - setLoading(false) - return@launch - } - - when (result.loginState) { - LoginState.SUCCESS -> { + val firebaseToken = tokenRepository.getFirebaseToken() setLoginFinished(true) - sendLoginEvent(LoginEvent.LoginSuccess(result.accessToken, result.refreshToken)) - saveTokens(result.accessToken, result.refreshToken) + saveTokens(result.data.accessToken, result.data.refreshToken) + updateFirebaseToken(result.data.accessToken, firebaseToken) sendLoginEvent(LoginEvent.LoginInfoSaved) } - else -> { - sendLoginEvent(LoginEvent.LoginFailure(result.loginState)) + is RepositoryResult.Error -> { + sendLoginEvent( + when (result.errorState) { + AuthErrorState.INVALID_REQUEST -> { + LoginEvent.LoginFailure + } + + else -> { + LoginEvent.UndefinedError + } + } + ) } } setLoading(false) } } + private suspend fun updateFirebaseToken(accessToken: String, firebaseToken: String?) { + if (firebaseToken != null) { + when (tokenRepository.updateFirebaseToken(accessToken, firebaseToken)) { + is RepositoryResult.Error -> { + sendLoginEvent(LoginEvent.TokenUpdateError) + } + else -> {} + } + } else { + sendLoginEvent(LoginEvent.FirebaseError) + } + } + private suspend fun saveTokens(accessToken: String, refreshToken: String) { tokenRepository.storeTokens(accessToken, refreshToken) } diff --git a/android/app/src/main/java/app/priceguard/ui/signup/SignupActivity.kt b/android/app/src/main/java/app/priceguard/ui/signup/SignupActivity.kt index c903d68..2591df6 100644 --- a/android/app/src/main/java/app/priceguard/ui/signup/SignupActivity.kt +++ b/android/app/src/main/java/app/priceguard/ui/signup/SignupActivity.kt @@ -2,13 +2,13 @@ package app.priceguard.ui.signup import android.content.Intent import android.os.Bundle +import android.widget.Toast import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.widget.NestedScrollView import androidx.databinding.DataBindingUtil import app.priceguard.R -import app.priceguard.data.dto.SignupState import app.priceguard.databinding.ActivitySignupBinding import app.priceguard.ui.home.HomeActivity import app.priceguard.ui.signup.SignupViewModel.SignupEvent @@ -71,32 +71,36 @@ class SignupActivity : AppCompatActivity() { (binding.btnSignupSignup as MaterialButton).icon = circularProgressIndicator } - is SignupEvent.SignupSuccess -> { - (binding.btnSignupSignup as MaterialButton).icon = null - } - - is SignupEvent.SignupFailure -> { + else -> { (binding.btnSignupSignup as MaterialButton).icon = null - when (event.errorState) { - SignupState.INVALID_PARAMETER -> { - showDialog(getString(R.string.error), getString(R.string.invalid_parameter)) + when (event) { + SignupEvent.SignupInfoSaved -> { + gotoHomeActivity() } - SignupState.DUPLICATE_EMAIL -> { + SignupEvent.DuplicatedEmail -> { showDialog(getString(R.string.error), getString(R.string.duplicate_email)) } - SignupState.UNDEFINED_ERROR -> { + SignupEvent.InvalidRequest -> { + showDialog(getString(R.string.error), getString(R.string.invalid_parameter)) + } + + SignupEvent.UndefinedError -> { showDialog(getString(R.string.error), getString(R.string.undefined_error)) } + SignupEvent.TokenUpdateError, SignupEvent.FirebaseError -> { + Toast.makeText( + this@SignupActivity, + getString(R.string.push_notification_not_working), + Toast.LENGTH_LONG + ).show() + } + else -> {} } } - - SignupEvent.SignupInfoSaved -> { - gotoHomeActivity() - } } } @@ -142,7 +146,7 @@ class SignupActivity : AppCompatActivity() { private fun updateNameTextFieldUI(state: SignupUIState) { when (state.isNameError) { true -> { - binding.tilSignupName.error = getString(R.string.name_required) + binding.tilSignupName.error = getString(R.string.invalid_name) } else -> { diff --git a/android/app/src/main/java/app/priceguard/ui/signup/SignupViewModel.kt b/android/app/src/main/java/app/priceguard/ui/signup/SignupViewModel.kt index 0a32e60..b5e5d10 100644 --- a/android/app/src/main/java/app/priceguard/ui/signup/SignupViewModel.kt +++ b/android/app/src/main/java/app/priceguard/ui/signup/SignupViewModel.kt @@ -3,9 +3,10 @@ package app.priceguard.ui.signup import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import app.priceguard.data.dto.SignupState -import app.priceguard.data.repository.TokenRepository -import app.priceguard.data.repository.UserRepository +import app.priceguard.data.repository.RepositoryResult +import app.priceguard.data.repository.auth.AuthErrorState +import app.priceguard.data.repository.auth.AuthRepository +import app.priceguard.data.repository.token.TokenRepository import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.MutableSharedFlow @@ -18,7 +19,7 @@ import kotlinx.coroutines.launch @HiltViewModel class SignupViewModel @Inject constructor( - private val userRepository: UserRepository, + private val authRepository: AuthRepository, private val tokenRepository: TokenRepository ) : ViewModel() { @@ -38,15 +39,18 @@ class SignupViewModel @Inject constructor( sealed class SignupEvent { data object SignupStart : SignupEvent() - data class SignupSuccess(val accessToken: String, val refreshToken: String) : SignupEvent() - data class SignupFailure(val errorState: SignupState) : SignupEvent() + data object InvalidRequest : SignupEvent() + data object DuplicatedEmail : SignupEvent() + data object UndefinedError : SignupEvent() data object SignupInfoSaved : SignupEvent() + data object FirebaseError : SignupEvent() + data object TokenUpdateError : SignupEvent() } private val emailPattern = """^[\w.+-]+@((?!-)[A-Za-z0-9-]{1,63}(?|\-\[\]\\/]*\d)(?=[A-Za-z\d!@#$%^&*()_+={};:'"~`,.?<>|\-\[\]\\/]*[a-z])(?=[A-Za-z\d!@#$%^&*()_+={};:'"~`,.?<>|\-\[\]\\/]*[A-Z])(?=[A-Za-z\d!@#$%^&*()_+={};:'"~`,.?<>|\-\[\]\\/]*[A-Za-z\d!@#$%^&*()_+={};:'"~`,.?<>|\-\[\]\\/])[A-Za-z\d!@#$%^&*()_+={};:'"~`,.?<>|\-\[\]\\/]{8,16}$""".toRegex() private val _state: MutableStateFlow = MutableStateFlow(SignupUIState()) val state: StateFlow = _state.asStateFlow() @@ -65,30 +69,41 @@ class SignupViewModel @Inject constructor( Log.d("ViewModel", "Event Start Sent") updateSignupStarted(true) val result = - userRepository.signUp(_state.value.email, _state.value.name, _state.value.password) + authRepository.signUp(_state.value.email, _state.value.name, _state.value.password) - if (result.accessToken == null || result.refreshToken == null) { - sendSignupEvent(SignupEvent.SignupFailure(result.signUpState)) - updateSignupStarted(false) - return@launch - } + when (result) { + is RepositoryResult.Success -> { + if (result.data.accessToken.isEmpty() || result.data.refreshToken.isEmpty()) { + sendSignupEvent(SignupEvent.UndefinedError) + updateSignupStarted(false) + return@launch + } + + val firebaseToken = tokenRepository.getFirebaseToken() - when (result.signUpState) { - SignupState.SUCCESS -> { updateSignupFinished(true) - sendSignupEvent( - SignupEvent.SignupSuccess( - result.accessToken, - result.refreshToken - ) - ) - saveTokens(result.accessToken, result.refreshToken) + saveTokens(result.data.accessToken, result.data.refreshToken) + updateFirebaseToken(result.data.accessToken, firebaseToken) sendSignupEvent(SignupEvent.SignupInfoSaved) Log.d("ViewModel", "Event Finish Sent") } - else -> { - sendSignupEvent(SignupEvent.SignupFailure(result.signUpState)) + is RepositoryResult.Error -> { + sendSignupEvent( + when (result.errorState) { + AuthErrorState.INVALID_REQUEST -> { + SignupEvent.InvalidRequest + } + + AuthErrorState.DUPLICATED_EMAIL -> { + SignupEvent.DuplicatedEmail + } + + AuthErrorState.UNDEFINED_ERROR -> { + SignupEvent.UndefinedError + } + } + ) } } updateSignupStarted(false) @@ -125,7 +140,7 @@ class SignupViewModel @Inject constructor( } private fun isValidName(): Boolean { - return _state.value.name.isNotBlank() + return _state.value.name.isNotBlank() && _state.value.name.length <= 16 } private fun isValidEmail(): Boolean { @@ -148,6 +163,20 @@ class SignupViewModel @Inject constructor( _eventFlow.emit(event) } + private suspend fun updateFirebaseToken(accessToken: String, firebaseToken: String?) { + if (firebaseToken != null) { + when (tokenRepository.updateFirebaseToken(accessToken, firebaseToken)) { + is RepositoryResult.Error -> { + sendSignupEvent(SignupEvent.TokenUpdateError) + } + + else -> {} + } + } else { + sendSignupEvent(SignupEvent.FirebaseError) + } + } + private fun updateIsSignupReady() { _state.value = _state.value.copy(isSignupReady = isValidName() && isValidEmail() && isValidPassword() && isValidRetypePassword()) diff --git a/android/app/src/main/java/app/priceguard/ui/splash/SplashScreenActivity.kt b/android/app/src/main/java/app/priceguard/ui/splash/SplashScreenActivity.kt index eedf191..e5af970 100644 --- a/android/app/src/main/java/app/priceguard/ui/splash/SplashScreenActivity.kt +++ b/android/app/src/main/java/app/priceguard/ui/splash/SplashScreenActivity.kt @@ -10,6 +10,7 @@ import android.view.ViewTreeObserver import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import app.priceguard.databinding.ActivitySplashScreenBinding +import app.priceguard.ui.detail.DetailActivity import app.priceguard.ui.home.HomeActivity import app.priceguard.ui.intro.IntroActivity import app.priceguard.ui.util.lifecycle.repeatOnCreated @@ -45,10 +46,15 @@ class SplashScreenActivity : AppCompatActivity() { splashViewModel.event.collect { event -> when (event) { SplashScreenViewModel.SplashEvent.OpenHome -> { - launchActivityAndExit( - this@SplashScreenActivity, - HomeActivity::class.java - ) + val productCode = intent.getStringExtra("productCode") + if (productCode != null) { + receivePushAlarm() + } else { + launchActivityAndExit( + this@SplashScreenActivity, + HomeActivity::class.java + ) + } } SplashScreenViewModel.SplashEvent.OpenIntro -> { @@ -67,4 +73,12 @@ class SplashScreenActivity : AppCompatActivity() { val content: View = findViewById(android.R.id.content) content.viewTreeObserver.addOnPreDrawListener(onPreDrawListener) } + + private fun receivePushAlarm() { + val productCode = intent.getStringExtra("productCode") ?: return + val intent = Intent(this, DetailActivity::class.java) + intent.putExtra("productCode", productCode) + intent.putExtra("directed", true) + startActivity(intent) + } } diff --git a/android/app/src/main/java/app/priceguard/ui/splash/SplashScreenViewModel.kt b/android/app/src/main/java/app/priceguard/ui/splash/SplashScreenViewModel.kt index 6c96540..b4b222d 100644 --- a/android/app/src/main/java/app/priceguard/ui/splash/SplashScreenViewModel.kt +++ b/android/app/src/main/java/app/priceguard/ui/splash/SplashScreenViewModel.kt @@ -2,8 +2,8 @@ package app.priceguard.ui.splash import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import app.priceguard.data.dto.RenewResult -import app.priceguard.data.repository.TokenRepository +import app.priceguard.data.repository.RepositoryResult +import app.priceguard.data.repository.token.TokenRepository import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.MutableSharedFlow @@ -36,17 +36,18 @@ class SplashScreenViewModel @Inject constructor(tokenRepository: TokenRepository } // Renew Token - val renewResult = tokenRepository.renewTokens(refreshToken) - if (renewResult == RenewResult.SUCCESS) { - sendSplashEvent(SplashEvent.OpenHome) - setAsReady() - return@launch - } + when (tokenRepository.renewTokens(refreshToken)) { + is RepositoryResult.Success -> { + sendSplashEvent(SplashEvent.OpenHome) + setAsReady() + } - // Renew Failed - tokenRepository.clearTokens() - sendSplashEvent(SplashEvent.OpenIntro) - setAsReady() + is RepositoryResult.Error -> { + tokenRepository.clearTokens() + sendSplashEvent(SplashEvent.OpenIntro) + setAsReady() + } + } } } diff --git a/android/app/src/main/java/app/priceguard/ui/util/ui/NetworkDialog.kt b/android/app/src/main/java/app/priceguard/ui/util/ui/NetworkDialog.kt index f9eaa17..d76f19c 100644 --- a/android/app/src/main/java/app/priceguard/ui/util/ui/NetworkDialog.kt +++ b/android/app/src/main/java/app/priceguard/ui/util/ui/NetworkDialog.kt @@ -3,7 +3,7 @@ package app.priceguard.ui.util.ui import android.app.Activity import android.content.Intent import app.priceguard.R -import app.priceguard.data.repository.TokenRepository +import app.priceguard.data.repository.token.TokenRepository import app.priceguard.ui.login.LoginActivity import com.google.android.material.dialog.MaterialAlertDialogBuilder import kotlinx.coroutines.CoroutineScope diff --git a/android/app/src/main/java/app/priceguard/ui/util/ui/NotificationSetting.kt b/android/app/src/main/java/app/priceguard/ui/util/ui/NotificationSetting.kt new file mode 100644 index 0000000..428a4fc --- /dev/null +++ b/android/app/src/main/java/app/priceguard/ui/util/ui/NotificationSetting.kt @@ -0,0 +1,11 @@ +package app.priceguard.ui.util.ui + +import android.content.Context +import android.content.Intent +import android.provider.Settings + +fun Context.openNotificationSettings() { + val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) + .putExtra(Settings.EXTRA_APP_PACKAGE, this.packageName) + startActivity(intent) +} diff --git a/android/app/src/main/res/anim/from_left_enter.xml b/android/app/src/main/res/anim/from_left_enter.xml new file mode 100644 index 0000000..5e0a2b4 --- /dev/null +++ b/android/app/src/main/res/anim/from_left_enter.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/android/app/src/main/res/anim/from_right_enter.xml b/android/app/src/main/res/anim/from_right_enter.xml new file mode 100644 index 0000000..5dc2643 --- /dev/null +++ b/android/app/src/main/res/anim/from_right_enter.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/android/app/src/main/res/anim/to_left_exit.xml b/android/app/src/main/res/anim/to_left_exit.xml new file mode 100644 index 0000000..ce0463c --- /dev/null +++ b/android/app/src/main/res/anim/to_left_exit.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/android/app/src/main/res/anim/to_right_exit.xml b/android/app/src/main/res/anim/to_right_exit.xml new file mode 100644 index 0000000..db8f610 --- /dev/null +++ b/android/app/src/main/res/anim/to_right_exit.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable-anydpi/ic_priceguard_notification.xml b/android/app/src/main/res/drawable-anydpi/ic_priceguard_notification.xml new file mode 100644 index 0000000..64e86fe --- /dev/null +++ b/android/app/src/main/res/drawable-anydpi/ic_priceguard_notification.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/android/app/src/main/res/drawable-hdpi/ic_priceguard_notification.png b/android/app/src/main/res/drawable-hdpi/ic_priceguard_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..214b7a43e29f26833a9c31c82b654d9b79b70b6e GIT binary patch literal 656 zcmV;B0&o3^P)HX?{vifBYaswgNH3TldgU=_3ytSm$XL2ZH}q!K}_EMgbX zzd-S$P}8VEG-4F5zsJmkyXU=`&1Pp76n}8YviF?(4$0;*+t>FmVrCa|tg6 z993VVaNt^W4SkU!p0DUCTBrH0uoRu4|k|%4!zembAF>m&M~+Df1wELbs;i4ExRIKqVH%y=FJzf^uPOU%Pz=8@itos z&jIHz2p8yTbQ8^qzqvxpPdKmgE&usYGR+jS>N1|)lB>UvWjFD%NM!3RWa$rZDT#c2 zg{;FeaL(uR9rYCAbwB#;X6-oc)-GXn70PE5ZdS#F&v~rQLixO1snRk5Yc2Fh=Mk9B zxWJUo1H3=#Jf2bYDtK-<|D?_%Fzs@I4>~VkcHa2{(FZsMzPB!OQ0GZpAh1v81W3>~YhxO+Cwn>{i+J&Q|iJ(z0i2 z&#-#hLFS0;Xn7pz2*R7uf-Ia-Um19xNH#p+Z&6>zu;hg-4mb88^%XGNkA6w!wfee( zh0}6C;1k;DJR8tQ@dO_DUA1A^SK&}Xd1cu%v@(x&=$_i)kn0xzNjzb8OLL9FfdlA{ q6bal#{p#;26qrO8(Hfoo_51-~e*l@a=O`-x0000}0$_cThi)?~BJ7Ps`{1f^BHiXR6?=bd=v0crTT;ETH`1 zccB4v3!NEHS#1HVO9~J83A%=+##ibu!0}1wXYl**2n|d|skea2!!2EtQ_>euep{;e zBJ+}&^RxxHJP4gAS8j%~mIbG_FL@CT+NaLK}LJ0n9tJE8=T~Gi~2b2IYM-%(LV3D}!MZ3V-?&@BbM;lu~tC T44kVU00000NkvXXu0mjfu_?d4 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-xhdpi/ic_priceguard_notification.png b/android/app/src/main/res/drawable-xhdpi/ic_priceguard_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..4c125717ada32dd130f3ea99d7bade9b9f82352d GIT binary patch literal 839 zcmV-N1GxN&P)4f26oo@VNFYgI+h6#4QuBrq zG`f$t@S4xzv3<=S3eeaB;;}Sb26GqGr&Z9vG~x;3Yj1Crp!N}BTKxwG%$E^&5Kj>= z5T{-PW&A+gLfm$*Q=m6%uoi1BYd^_AywYQx&NswC!0*7V)nP^bqCoUrT_SuyTvZ>1 zIWD&f=jszh9RhqoJVV@6Z7S3{4|O-vU0W3Rhs(kV}OX@|e%>k@@!^W6ZKVoeSVA-XzE@$eOV{Hsz zS@U&Ho19Jc%(12iz_C@ysmiu``dAAC@V{;y_>HZppJGi9Kydu7fcWtZ4hJ)Qz< zNX_b}PY$#jY6f0v-k_kj7PG+L%}T)FNFBkE=Jg9bwFr2ld9^UPCOb~8;Fn|-=;6GS z!h26;U^b!d4sPr#nx6`_&LhNx=Jy2*$1VI#kVDOnL5-R?SYurC2Zj|FCO0%+VUCJC z4mH0KP-sgldwfDH1~iL^&ywr2rJ6=T!Fj}6?ZSf_bKON~ljhK4Uj17G#doZDBbGtU zj{0>18ks{pmj=sV?wtB`4w{-myt45BWFF(%lxWHP6?>-}s0c{00N(czgo0 RENK7$002ovPDHLkV1k6Nh=u?F literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_priceguard_notification.png b/android/app/src/main/res/drawable-xxhdpi/ic_priceguard_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..b830f79237cad89f500b61367d325d5928f987af GIT binary patch literal 1321 zcmV+^1=jkBP)-*mCo0m0*HEaGoYgSLs z$N=CL3w?d;p(zhBF5)`&_k)@@cF=@*jH8}0$`Qsqts4ut;qNl8iXmxO&ZO3lB;3&3 z81FEy4SNuT)l4!Lt8S-(+l@A2_!v`{-x!ZEZq|^|bYM+)+Oxt>Vz*wqt z%s3osg0YVAEaQE~U)N_Zjcx2}j5m_MtMVsfFWgLacAU_;8-b1HG7kBZMC52nzhjyd z3EPIb+OHNAT#B$WPFW5z?$mw*_}*QN&!r{?wk`2?Ns67726eE5^&ZBJ+9wOkWxSQt z#^Q|jsf;RD%^Fg_OTN~m@vs54uI1NevW31bK2cGZjp+)3h@q?h$9%WTyw z4MlEJ)r8B8^Nb%EUoxIX3%+RD49P4ulg(x`+N^2J(fZOL+o10?x2@Q$I($NJkYTgx z@Co%#_vj7E2fac0pf@NV^akaF78%5zOBg#D`{BOD57?`@J~peD>(fnx;4V(h47-o? z>2TY0RKo|OVh}Y~GJZvc+a0RqgHbSudaDs;%$6koZSS?8RL=)pH;7vIs(2PsH(;Ot zx`HknM4h!NT)?8FZkzYo4%PKRR}F&u>2MP%M|GtfnYSbq=Vn3H>eHqbSL5YZe% z^%ZoEvms2svip4YLLOaSG2GKmCZ5GutE0dNg5|KHw)&z zpv5Jqv|IU5t0nML^vS(Q120K;IT&YYk~YLfIkKpT~rr|87bU2SwnjZ~{FVb$pVXc{NIKwwO zl(0Z+qQVB7w4)Gom#+!f`z2VTzjUAH0! zyKlT6?*7ejc`py^v0VFi1`fZQ@tz% + + diff --git a/android/app/src/main/res/layout-land/activity_login.xml b/android/app/src/main/res/layout-land/activity_login.xml index 603310b..fba714a 100644 --- a/android/app/src/main/res/layout-land/activity_login.xml +++ b/android/app/src/main/res/layout-land/activity_login.xml @@ -81,7 +81,7 @@ android:imeOptions="actionNext" android:inputType="textEmailAddress" android:maxLines="1" - android:onTextChanged="@{viewModel::setID}" /> + android:onTextChanged="@{(content, s, b, c) -> viewModel.setEmail(content.toString())}" /> @@ -106,7 +106,7 @@ android:imeOptions="actionDone" android:inputType="textPassword" android:maxLines="1" - android:onTextChanged="@{viewModel::setPassword}" /> + android:onTextChanged="@{(content, s, b, c) -> viewModel.setPassword(content.toString())}" /> diff --git a/android/app/src/main/res/layout/activity_add_item.xml b/android/app/src/main/res/layout/activity_add_item.xml index 454a702..c4aa1ae 100644 --- a/android/app/src/main/res/layout/activity_add_item.xml +++ b/android/app/src/main/res/layout/activity_add_item.xml @@ -5,8 +5,8 @@ + android:fillViewport="true" + app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"> @@ -73,6 +74,7 @@ android:id="@+id/iv_detail_product" android:layout_width="match_parent" android:layout_height="256dp" + android:contentDescription="@string/product_image" app:imageFromUrl="@{viewModel.state.imageUrl}" app:layout_constraintTop_toTopOf="parent" /> @@ -81,20 +83,34 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="24dp" + android:contentDescription="@string/shopping_mall_logo" android:src="@drawable/ic_11st_logo" app:layout_constraintStart_toEndOf="@+id/gl_vertical_start_nested" app:layout_constraintTop_toBottomOf="@id/iv_detail_product" /> +