From d3b894f41b8d3c441993a2c21832e70a3cce955a Mon Sep 17 00:00:00 2001 From: Yaechan Park <102402485+dpcks0509@users.noreply.github.com> Date: Sun, 23 Mar 2025 18:46:56 +0900 Subject: [PATCH 01/49] =?UTF-8?q?refactor:=20Contributors=20=EC=82=AC?= =?UTF-8?q?=EC=A7=84=20=ED=81=AC=EA=B8=B0=20=EC=A1=B0=EC=A0=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 09a2265..2bad762 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ ## ๐Ÿ’Ÿ *****Contributors***** -| image | image | +| image | image | |---------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------| | ๊น€์ง„์šฐ([@jinuemong](https://github.com/jinuemong)) | ๋ฐ•์˜ˆ์ฐฌ([@dpcks0509](https://github.com/dpcks0509)) | From 7c9dbad1be6365dce4e306caebf68f1524298dd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=8C=E1=85=B5=E1=86=AB?= =?UTF-8?q?=E1=84=8B=E1=85=AE?= Date: Sun, 23 Mar 2025 20:02:40 +0900 Subject: [PATCH 02/49] test: ci/cd --- .github/workflows/android_cd.yml | 2 +- .github/workflows/android_ci.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/android_cd.yml b/.github/workflows/android_cd.yml index 13e33d1..84da76d 100644 --- a/.github/workflows/android_cd.yml +++ b/.github/workflows/android_cd.yml @@ -57,7 +57,7 @@ jobs: # echo "io.sentry.dsn=\"$IO_SENTRY_DSN\"" >> local.properties echo "kakao.native.app.key.manifest=\"$KAKAO_NATIVE_APP_KEY_MANIFEST\"" >> local.properties echo "kakao.native.app.key=\"$KAKAO_NATIVE_APP_KEY\"" >> local.properties - echo "amplitude.dev.api.key=\"AMPLITUDE_API_KEY\"" >> local.properties + echo "amplitude.dev.api.key=\"$AMPLITUDE_API_KEY\"" >> local.properties # - name: Access sentry properties # env: diff --git a/.github/workflows/android_ci.yml b/.github/workflows/android_ci.yml index f1a08fd..6793269 100644 --- a/.github/workflows/android_ci.yml +++ b/.github/workflows/android_ci.yml @@ -58,7 +58,7 @@ jobs: # echo "io.sentry.dsn=\"$IO_SENTRY_DSN\"" >> local.properties echo "kakao.native.app.key.manifest=\"$KAKAO_NATIVE_APP_KEY_MANIFEST\"" >> local.properties echo "kakao.native.app.key=\"$KAKAO_NATIVE_APP_KEY\"" >> local.properties - echo "amplitude.dev.api.key=\"AMPLITUDE_API_KEY\"" >> local.properties + echo "amplitude.dev.api.key=\"$AMPLITUDE_API_KEY\"" >> local.properties # - name: Access sentry properties # env: From 004da6656671c58d876e14a759047d4bfbb89c46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=8C=E1=85=B5=E1=86=AB?= =?UTF-8?q?=E1=84=8B=E1=85=AE?= Date: Sun, 23 Mar 2025 20:04:17 +0900 Subject: [PATCH 03/49] test: ci/cd --- .github/workflows/android_cd.yml | 12 ------------ .github/workflows/android_ci.yml | 12 ------------ 2 files changed, 24 deletions(-) diff --git a/.github/workflows/android_cd.yml b/.github/workflows/android_cd.yml index 84da76d..7dd3ff4 100644 --- a/.github/workflows/android_cd.yml +++ b/.github/workflows/android_cd.yml @@ -48,27 +48,15 @@ jobs: - name: Access local properties env: HFM_BASE_URL: ${{ secrets.BASE_URL }} -# IO_SENTRY_TOKEN: ${{ secrets.IO_SENTRY_DSN }} KAKAO_NATIVE_APP_KEY_MANIFEST: ${{ secrets.KAKAO_NATIVE_APP_KEY }} KAKAO_NATIVE_APP_KEY: ${{ secrets.KAKAO_NATIVE_APP_KEY }} AMPLITUDE_API_KEY: ${{ secrets.AMPLITUDE_API_KEY }} run: | echo "dev.base.url=\"$BASE_URL\"" >> local.properties -# echo "io.sentry.dsn=\"$IO_SENTRY_DSN\"" >> local.properties echo "kakao.native.app.key.manifest=\"$KAKAO_NATIVE_APP_KEY_MANIFEST\"" >> local.properties echo "kakao.native.app.key=\"$KAKAO_NATIVE_APP_KEY\"" >> local.properties echo "amplitude.dev.api.key=\"$AMPLITUDE_API_KEY\"" >> local.properties - # - name: Access sentry properties - # env: - # DEFAULTS_ORG: ${{ secrets.DEFAULTS_ORG }} - # DEFAULTS_PROJECT: ${{ secrets.DEFAULTS_PROJECT }} - # AUTH_TOKEN: ${{ secrets.AUTH_TOKEN }} - # run: | - # echo "defaults.org=$DEFAULTS_ORG" >> sentry.properties - # echo "defaults.project=$DEFAULTS_PROJECT" >> sentry.properties - # echo "auth.token=$AUTH_TOKEN" >> sentry.properties - - name: Build Release APK run: | ./gradlew :app:assembleRelease diff --git a/.github/workflows/android_ci.yml b/.github/workflows/android_ci.yml index 6793269..4a6a626 100644 --- a/.github/workflows/android_ci.yml +++ b/.github/workflows/android_ci.yml @@ -49,27 +49,15 @@ jobs: - name: Access local properties env: HFM_BASE_URL: ${{ secrets.BASE_URL }} -# IO_SENTRY_TOKEN: ${{ secrets.IO_SENTRY_DSN }} KAKAO_NATIVE_APP_KEY_MANIFEST: ${{ secrets.KAKAO_NATIVE_APP_KEY }} KAKAO_NATIVE_APP_KEY: ${{ secrets.KAKAO_NATIVE_APP_KEY }} AMPLITUDE_API_KEY: ${{ secrets.AMPLITUDE_API_KEY }} run: | echo "dev.base.url=\"$BASE_URL\"" >> local.properties -# echo "io.sentry.dsn=\"$IO_SENTRY_DSN\"" >> local.properties echo "kakao.native.app.key.manifest=\"$KAKAO_NATIVE_APP_KEY_MANIFEST\"" >> local.properties echo "kakao.native.app.key=\"$KAKAO_NATIVE_APP_KEY\"" >> local.properties echo "amplitude.dev.api.key=\"$AMPLITUDE_API_KEY\"" >> local.properties - # - name: Access sentry properties - # env: - # DEFAULTS_ORG: ${{ secrets.DEFAULTS_ORG }} - # DEFAULTS_PROJECT: ${{ secrets.DEFAULTS_PROJECT }} - # AUTH_TOKEN: ${{ secrets.AUTH_TOKEN }} - # run: | - # echo "defaults.org=$DEFAULTS_ORG" >> sentry.properties - # echo "defaults.project=$DEFAULTS_PROJECT" >> sentry.properties - # echo "auth.token=$AUTH_TOKEN" >> sentry.properties - - name: Lint Check run: ./gradlew ktlintCheck -PcompileSdkVersion=34 From e2507b0088eb56b6893ae963c6f35ccc24ff5dc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=8C=E1=85=B5=E1=86=AB?= =?UTF-8?q?=E1=84=8B=E1=85=AE?= Date: Sun, 23 Mar 2025 20:07:14 +0900 Subject: [PATCH 04/49] test: ci/cd --- app/build.gradle.kts | 1 - 1 file changed, 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 62573c2..66b1ff2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -34,7 +34,6 @@ android { buildConfigField("String", "KAKAO_NATIVE_APP_KEY", properties["kakao.native.app.key"].toString()) - manifestPlaceholders["IO_SENTRY_DSN"] = properties["io.sentry.dsn"] as String manifestPlaceholders["KAKAO_NATIVE_APP_KEY_MANIFEST"] = properties["kakao.native.app.key.manifest"] as String } From 42928bb8e9ddbdf0b06cca2ec03bc83827070d2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=8C=E1=85=B5=E1=86=AB?= =?UTF-8?q?=E1=84=8B=E1=85=AE?= Date: Sun, 23 Mar 2025 20:10:47 +0900 Subject: [PATCH 05/49] test: ci/cd --- app/src/main/AndroidManifest.xml | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6aec215..e75d9c2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -18,25 +18,6 @@ android:usesCleartextTraffic="true" tools:targetApi="31"> - - - - - - - Date: Sun, 23 Mar 2025 20:26:01 +0900 Subject: [PATCH 06/49] test: ci/cd --- .gitignore | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index f4dca91..2a37cc1 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,6 @@ build/ # Local configuration file (sdk path, etc) local.properties -sentry.properties # Log/OS Files *.log @@ -185,4 +184,4 @@ fabric.properties !/gradle/wrapper/gradle-wrapper.jar -# End of https://www.toptal.com/developers/gitignore/api/android,androidstudio,kotlin \ No newline at end of file +# End of https://www.toptal.com/developers/gitignore/api/android,androidstudio,kotlin From e635760d37eb4dce461877f02b25b8f106d43fc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=8C=E1=85=B5=E1=86=AB?= =?UTF-8?q?=E1=84=8B=E1=85=AE?= Date: Sun, 23 Mar 2025 20:35:50 +0900 Subject: [PATCH 07/49] test: ci/cd --- README.md | 28 ++++++++++++++-------------- app/build.gradle.kts | 1 - build.gradle.kts | 3 +-- gradle/libs.versions.toml | 4 +--- 4 files changed, 16 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 09a2265..8641a4c 100644 --- a/README.md +++ b/README.md @@ -16,20 +16,20 @@ ## ๐Ÿ‘ฉ๐Ÿปโ€๐Ÿ’ป ***๐™๐™š๐™˜๐™ ๐™Ž๐™ฉ๐™–๐™˜๐™ *** -| Title | Content | -|-------------------------|-------------------------------------------------------------------------------| -| Architecture | Clean Architecture, MVI, Single Activity Architecture | -| Design Pattern | Repository Pattern, Delegation Pattern | -| UI | Jetpack Compose | -| Jetpack Components | encryptedsharedpreferences, Lifecycle, ViewModel, Navigation | -| Dependency Injection | Hilt | -| Network | Retrofit, OkHttp, MultiPart | -| Asynchronous Processing | Coroutine | -| Third Party Library | Coil, Firebase, Timber, kotlinSerialization, sentry, Amplitude, Lottie, Kakao | -| Strategy | Git Flow | -| CI | GitHub Action(KtLint, Complie Check) | -| CD | GitHub Action | -| Other Tool | Slack, Notion, Figma, Postman, Discord | +| Title | Content | +|-------------------------|-----------------------------------------------------------------------------| +| Architecture | Clean Architecture, MVI, Single Activity Architecture | +| Design Pattern | Repository Pattern, Delegation Pattern | +| UI | Jetpack Compose | +| Jetpack Components | encryptedsharedpreferences, Lifecycle, ViewModel, Navigation | +| Dependency Injection | Hilt | +| Network | Retrofit, OkHttp, MultiPart | +| Asynchronous Processing | Coroutine | +| Third Party Library | Coil, Firebase, Timber, kotlinSerialization, Amplitude, Lottie, Kakao | +| Strategy | Git Flow | +| CI | GitHub Action(KtLint, Complie Check) | +| CD | GitHub Action | +| Other Tool | Slack, Notion, Figma, Postman, Discord |
diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 66b1ff2..dbfe070 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -9,7 +9,6 @@ plugins { alias(libs.plugins.google.firebase.crashlytics) alias(libs.plugins.dagger.hilt) alias(libs.plugins.ktlint) - alias(libs.plugins.sentry) } val properties = Properties().apply { diff --git a/build.gradle.kts b/build.gradle.kts index 69d3401..dd091d8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -16,5 +16,4 @@ plugins { alias(libs.plugins.google.firebase.crashlytics) apply false alias(libs.plugins.dagger.hilt) apply false alias(libs.plugins.ktlint) apply false - alias(libs.plugins.sentry) apply false -} \ No newline at end of file +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 68b89ac..06ee401 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -47,7 +47,6 @@ hiltNavigationCompose = "1.2.0" ktlint = "11.5.1" coil = "2.6.0" timber = "5.0.1" -sentry = "4.1.1" kakao = "2.20.3" amplitude = "1.+" lottieCompose = "6.1.0" @@ -121,7 +120,6 @@ google-services = { id = "com.google.gms.google-services", version.ref = "google google-firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebaseCrashlytics" } dagger-hilt = { id = "com.google.dagger.hilt.android", version.ref = "daggerHilt" } ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } -sentry = { id = "io.sentry.android.gradle", version.ref = "sentry" } [bundles] androidx = [ @@ -177,4 +175,4 @@ pager = [ "androidx-paging-common-android", "accompanist-pager", "accompanist-pager-indicators" -] \ No newline at end of file +] From 2407123e39efe1195e7f8eb98c2edbeed3f3bf7c Mon Sep 17 00:00:00 2001 From: Yaechan Park <102402485+dpcks0509@users.noreply.github.com> Date: Tue, 25 Mar 2025 23:34:58 +0900 Subject: [PATCH 08/49] =?UTF-8?q?[feat]=20KaKao=20=EC=9E=A5=EC=86=8C=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20API=20=EC=97=B0=EA=B2=B0=20(#9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: PlaceSearchResult, PlaceInfo์˜ Model, Response, Mapper ์ƒ์„ฑ * feat: ์นด์นด์˜ค ์žฅ์†Œ๊ฒ€์ƒ‰ API ์ดˆ๊ธฐ ์„ธํŒ… * fix: ์นด์นด์˜ค ์žฅ์†Œ ๊ฒ€์ƒ‰ ์˜ค๋ฅ˜ ์ˆ˜์ • * refactor: ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜ํ˜• Result๋กœ ๊ฐ์‹ธ๊ธฐ * feat: GetPlaceSearchResultUseCase ๊ตฌํ˜„ * refactor: PlaceSearch Constraints ๋ณ€์ˆ˜๋ช… ์ˆ˜์ • * refactor: ktlint ์ ์šฉ * refactor: GetPlaceSearchResultUseCase ๊ตฌํ˜„ * feat: ci ํŒŒ์ผ์— kako_base_url, kakao_rest_api_key ์ถ”๊ฐ€ * refactor: GetPlaceSearchResultUseCase ๊ตฌํ˜„ * feat: buildConfig KAKAO_BASE_URL ์ถ”๊ฐ€ * refactor: providesPlaceSearchOkHttpClient ์‹ฑ๊ธ€ํ†ค ์œ ์ง€๋˜๊ฒŒ ์ˆ˜์ • * refactor: ๋ณ€์ˆ˜๋ช… address_name -> addressName์œผ๋กœ ์ˆ˜์ • * refactor: ktlint ์ˆ˜์ • * refactor: OkHttpClient Qualifier ์ถ”๊ฐ€ --- .github/workflows/android_ci.yml | 4 ++ app/build.gradle.kts | 3 ++ .../datasource/PlaceSearchDataSource.kt | 9 ++++ .../PlaceSearchDataSourceImpl.kt | 12 ++++++ .../model/response/ResponsePlaceInfoDto.kt | 12 ++++++ .../response/ResponsePlaceSearchResultDto.kt | 10 +++++ .../dataremote/service/PlaceSearchService.kt | 13 ++++++ .../data/dataremote/util/Constraints.kt | 5 +++ .../todomain/ResponsePlaceInfoDtoMapper.kt | 9 ++++ .../todomain/ResponsePlaceSearchDtoMapper.kt | 8 ++++ .../PlaceSearchRepositoryImpl.kt | 15 +++++++ .../sopt/teamdateroad/di/DataSourceModule.kt | 6 +++ .../org/sopt/teamdateroad/di/NetworkModule.kt | 42 ++++++++++++++++++- .../sopt/teamdateroad/di/RepositoryModule.kt | 6 +++ .../org/sopt/teamdateroad/di/ServiceModule.kt | 15 +++++++ .../teamdateroad/di/qualifier/Qualifier.kt | 3 ++ .../teamdateroad/domain/model/PlaceInfo.kt | 6 +++ .../domain/model/PlaceSearchResult.kt | 5 +++ .../repository/PlaceSearchRepository.kt | 9 ++++ .../usecase/GetPlaceSearchResultUseCase.kt | 11 +++++ .../presentation/ui/navigator/MainActivity.kt | 1 + gradle/libs.versions.toml | 2 +- 22 files changed, 204 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/org/sopt/teamdateroad/data/dataremote/datasource/PlaceSearchDataSource.kt create mode 100644 app/src/main/java/org/sopt/teamdateroad/data/dataremote/datasourceimpl/PlaceSearchDataSourceImpl.kt create mode 100644 app/src/main/java/org/sopt/teamdateroad/data/dataremote/model/response/ResponsePlaceInfoDto.kt create mode 100644 app/src/main/java/org/sopt/teamdateroad/data/dataremote/model/response/ResponsePlaceSearchResultDto.kt create mode 100644 app/src/main/java/org/sopt/teamdateroad/data/dataremote/service/PlaceSearchService.kt create mode 100644 app/src/main/java/org/sopt/teamdateroad/data/mapper/todomain/ResponsePlaceInfoDtoMapper.kt create mode 100644 app/src/main/java/org/sopt/teamdateroad/data/mapper/todomain/ResponsePlaceSearchDtoMapper.kt create mode 100644 app/src/main/java/org/sopt/teamdateroad/data/repositoryimpl/PlaceSearchRepositoryImpl.kt create mode 100644 app/src/main/java/org/sopt/teamdateroad/domain/model/PlaceInfo.kt create mode 100644 app/src/main/java/org/sopt/teamdateroad/domain/model/PlaceSearchResult.kt create mode 100644 app/src/main/java/org/sopt/teamdateroad/domain/repository/PlaceSearchRepository.kt create mode 100644 app/src/main/java/org/sopt/teamdateroad/domain/usecase/GetPlaceSearchResultUseCase.kt diff --git a/.github/workflows/android_ci.yml b/.github/workflows/android_ci.yml index 4a6a626..4c764ee 100644 --- a/.github/workflows/android_ci.yml +++ b/.github/workflows/android_ci.yml @@ -49,13 +49,17 @@ jobs: - name: Access local properties env: HFM_BASE_URL: ${{ secrets.BASE_URL }} + KAKAO_BASE_URL: ${{ secrets.KAKAO_BASE_URL }} KAKAO_NATIVE_APP_KEY_MANIFEST: ${{ secrets.KAKAO_NATIVE_APP_KEY }} KAKAO_NATIVE_APP_KEY: ${{ secrets.KAKAO_NATIVE_APP_KEY }} + KAKAO_REST_API_KEY: ${{ secrets.KAKAO_REST_API_KEY }} AMPLITUDE_API_KEY: ${{ secrets.AMPLITUDE_API_KEY }} run: | echo "dev.base.url=\"$BASE_URL\"" >> local.properties + echo "kakao.base.url=\"$KAKAO_BASE_URL\"" >> local.properties echo "kakao.native.app.key.manifest=\"$KAKAO_NATIVE_APP_KEY_MANIFEST\"" >> local.properties echo "kakao.native.app.key=\"$KAKAO_NATIVE_APP_KEY\"" >> local.properties + echo "kakao.rest.api.key=\"$KAKAO_REST_API_KEY\"" >> local.properties echo "amplitude.dev.api.key=\"$AMPLITUDE_API_KEY\"" >> local.properties - name: Lint Check diff --git a/app/build.gradle.kts b/app/build.gradle.kts index dbfe070..2832b3f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -32,6 +32,7 @@ android { } buildConfigField("String", "KAKAO_NATIVE_APP_KEY", properties["kakao.native.app.key"].toString()) + buildConfigField("String", "KAKAO_REST_API_KEY", properties["kakao.rest.api.key"].toString()) manifestPlaceholders["KAKAO_NATIVE_APP_KEY_MANIFEST"] = properties["kakao.native.app.key.manifest"] as String } @@ -40,6 +41,7 @@ android { debug { isMinifyEnabled = false buildConfigField("String", "BASE_URL", properties["dev.base.url"].toString()) + buildConfigField("String", "KAKAO_BASE_URL", properties["kakao.base.url"].toString()) buildConfigField("String", "AMPLITUDE_API_KEY", properties["amplitude.dev.api.key"].toString()) } @@ -47,6 +49,7 @@ android { isMinifyEnabled = true isShrinkResources = true buildConfigField("String", "BASE_URL", properties["prod.base.url"].toString()) + buildConfigField("String", "KAKAO_BASE_URL", properties["kakao.base.url"].toString()) buildConfigField("String", "AMPLITUDE_API_KEY", properties["amplitude.prod.api.key"].toString()) proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), diff --git a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/datasource/PlaceSearchDataSource.kt b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/datasource/PlaceSearchDataSource.kt new file mode 100644 index 0000000..30831a0 --- /dev/null +++ b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/datasource/PlaceSearchDataSource.kt @@ -0,0 +1,9 @@ +package org.sopt.teamdateroad.data.dataremote.datasource + +import org.sopt.teamdateroad.data.dataremote.model.response.ResponsePlaceSearchResultDto + +interface PlaceSearchDataSource { + suspend fun getPlaceSearchResult( + keyword: String + ): Result +} diff --git a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/datasourceimpl/PlaceSearchDataSourceImpl.kt b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/datasourceimpl/PlaceSearchDataSourceImpl.kt new file mode 100644 index 0000000..b21d37c --- /dev/null +++ b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/datasourceimpl/PlaceSearchDataSourceImpl.kt @@ -0,0 +1,12 @@ +package org.sopt.teamdateroad.data.dataremote.datasourceimpl + +import javax.inject.Inject +import org.sopt.teamdateroad.data.dataremote.datasource.PlaceSearchDataSource +import org.sopt.teamdateroad.data.dataremote.model.response.ResponsePlaceSearchResultDto +import org.sopt.teamdateroad.data.dataremote.service.PlaceSearchService + +class PlaceSearchDataSourceImpl @Inject constructor(private val placeSearchService: PlaceSearchService) : PlaceSearchDataSource { + override suspend fun getPlaceSearchResult(keyword: String): Result { + return runCatching { placeSearchService.getPlaceSearchResult(keyword) } + } +} diff --git a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/model/response/ResponsePlaceInfoDto.kt b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/model/response/ResponsePlaceInfoDto.kt new file mode 100644 index 0000000..6150533 --- /dev/null +++ b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/model/response/ResponsePlaceInfoDto.kt @@ -0,0 +1,12 @@ +package org.sopt.teamdateroad.data.dataremote.model.response + +import com.google.gson.annotations.SerializedName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponsePlaceInfoDto( + @SerializedName("place_name") + val placeName: String, + @SerializedName("address_name") + val addressName: String +) diff --git a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/model/response/ResponsePlaceSearchResultDto.kt b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/model/response/ResponsePlaceSearchResultDto.kt new file mode 100644 index 0000000..105bc53 --- /dev/null +++ b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/model/response/ResponsePlaceSearchResultDto.kt @@ -0,0 +1,10 @@ +package org.sopt.teamdateroad.data.dataremote.model.response + +import com.google.gson.annotations.SerializedName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponsePlaceSearchResultDto( + @SerializedName("documents") + val placeInfos: List +) diff --git a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/service/PlaceSearchService.kt b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/service/PlaceSearchService.kt new file mode 100644 index 0000000..7c7d3a9 --- /dev/null +++ b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/service/PlaceSearchService.kt @@ -0,0 +1,13 @@ +package org.sopt.teamdateroad.data.dataremote.service + +import org.sopt.teamdateroad.data.dataremote.model.response.ResponsePlaceSearchResultDto +import org.sopt.teamdateroad.data.dataremote.util.PlaceSearch.LOCAL_SEARCH_KEYWORD_JSON +import retrofit2.http.GET +import retrofit2.http.Query + +interface PlaceSearchService { + @GET(LOCAL_SEARCH_KEYWORD_JSON) + suspend fun getPlaceSearchResult( + @Query("query") keyword: String + ): ResponsePlaceSearchResultDto +} diff --git a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/util/Constraints.kt b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/util/Constraints.kt index 5e2f227..aaedeb7 100644 --- a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/util/Constraints.kt +++ b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/util/Constraints.kt @@ -75,3 +75,8 @@ object Point { object TotalCostZero { const val ZERO_COST = "๋ฌด์ง€์ถœ" } + +object PlaceSearch { + private const val VERSION = "v2" + const val LOCAL_SEARCH_KEYWORD_JSON = "$VERSION/local/search/keyword.json" +} diff --git a/app/src/main/java/org/sopt/teamdateroad/data/mapper/todomain/ResponsePlaceInfoDtoMapper.kt b/app/src/main/java/org/sopt/teamdateroad/data/mapper/todomain/ResponsePlaceInfoDtoMapper.kt new file mode 100644 index 0000000..9f13083 --- /dev/null +++ b/app/src/main/java/org/sopt/teamdateroad/data/mapper/todomain/ResponsePlaceInfoDtoMapper.kt @@ -0,0 +1,9 @@ +package org.sopt.teamdateroad.data.mapper.todomain + +import org.sopt.teamdateroad.data.dataremote.model.response.ResponsePlaceInfoDto +import org.sopt.teamdateroad.domain.model.PlaceInfo + +fun ResponsePlaceInfoDto.toDomain(): PlaceInfo = PlaceInfo( + placeName = this.placeName, + addressName = this.addressName +) diff --git a/app/src/main/java/org/sopt/teamdateroad/data/mapper/todomain/ResponsePlaceSearchDtoMapper.kt b/app/src/main/java/org/sopt/teamdateroad/data/mapper/todomain/ResponsePlaceSearchDtoMapper.kt new file mode 100644 index 0000000..28a0e74 --- /dev/null +++ b/app/src/main/java/org/sopt/teamdateroad/data/mapper/todomain/ResponsePlaceSearchDtoMapper.kt @@ -0,0 +1,8 @@ +package org.sopt.teamdateroad.data.mapper.todomain + +import org.sopt.teamdateroad.data.dataremote.model.response.ResponsePlaceSearchResultDto +import org.sopt.teamdateroad.domain.model.PlaceSearchResult + +fun ResponsePlaceSearchResultDto.toDomain(): PlaceSearchResult = PlaceSearchResult( + placeInfos = this.placeInfos.map { responsePlaceInfoDto -> responsePlaceInfoDto.toDomain() } +) diff --git a/app/src/main/java/org/sopt/teamdateroad/data/repositoryimpl/PlaceSearchRepositoryImpl.kt b/app/src/main/java/org/sopt/teamdateroad/data/repositoryimpl/PlaceSearchRepositoryImpl.kt new file mode 100644 index 0000000..611f426 --- /dev/null +++ b/app/src/main/java/org/sopt/teamdateroad/data/repositoryimpl/PlaceSearchRepositoryImpl.kt @@ -0,0 +1,15 @@ +package org.sopt.teamdateroad.data.repositoryimpl + +import javax.inject.Inject +import org.sopt.teamdateroad.data.dataremote.datasource.PlaceSearchDataSource +import org.sopt.teamdateroad.data.mapper.todomain.toDomain +import org.sopt.teamdateroad.domain.model.PlaceSearchResult +import org.sopt.teamdateroad.domain.repository.PlaceSearchRepository + +class PlaceSearchRepositoryImpl @Inject constructor(private val placeSearchDataSource: PlaceSearchDataSource) : PlaceSearchRepository { + override suspend fun getPlaceSearchResult(keyword: String): Result { + return placeSearchDataSource.getPlaceSearchResult(keyword).map { responsePlaceSearchResultDto -> + responsePlaceSearchResultDto.toDomain() + } + } +} diff --git a/app/src/main/java/org/sopt/teamdateroad/di/DataSourceModule.kt b/app/src/main/java/org/sopt/teamdateroad/di/DataSourceModule.kt index 072b5b6..d61a635 100644 --- a/app/src/main/java/org/sopt/teamdateroad/di/DataSourceModule.kt +++ b/app/src/main/java/org/sopt/teamdateroad/di/DataSourceModule.kt @@ -11,6 +11,7 @@ import org.sopt.teamdateroad.data.dataremote.datasource.AdvertisementRemoteDataS import org.sopt.teamdateroad.data.dataremote.datasource.AuthRemoteDataSource import org.sopt.teamdateroad.data.dataremote.datasource.CourseRemoteDataSource import org.sopt.teamdateroad.data.dataremote.datasource.MyCourseRemoteDataSource +import org.sopt.teamdateroad.data.dataremote.datasource.PlaceSearchDataSource import org.sopt.teamdateroad.data.dataremote.datasource.ProfileRemoteDataSource import org.sopt.teamdateroad.data.dataremote.datasource.TimelineRemoteDataSource import org.sopt.teamdateroad.data.dataremote.datasource.UserPointRemoteDataSource @@ -18,6 +19,7 @@ import org.sopt.teamdateroad.data.dataremote.datasourceimpl.AdvertisementRemoteD import org.sopt.teamdateroad.data.dataremote.datasourceimpl.AuthRemoteDataSourceImpl import org.sopt.teamdateroad.data.dataremote.datasourceimpl.CourseRemoteDataSourceImpl import org.sopt.teamdateroad.data.dataremote.datasourceimpl.MyCourseRemoteDataSourceImpl +import org.sopt.teamdateroad.data.dataremote.datasourceimpl.PlaceSearchDataSourceImpl import org.sopt.teamdateroad.data.dataremote.datasourceimpl.ProfileRemoteDataSourceImpl import org.sopt.teamdateroad.data.dataremote.datasourceimpl.TimelineRemoteDataSourceImpl import org.sopt.teamdateroad.data.dataremote.datasourceimpl.UserPointRemoteDataSourceImpl @@ -56,4 +58,8 @@ abstract class DataSourceModule { @Binds @Singleton abstract fun bindsUserPointRemoteDataSource(userPointRemoteDataSourceImpl: UserPointRemoteDataSourceImpl): UserPointRemoteDataSource + + @Binds + @Singleton + abstract fun bindsPlaceSearchDataSource(placeSearchDataSourceImpl: PlaceSearchDataSourceImpl): PlaceSearchDataSource } diff --git a/app/src/main/java/org/sopt/teamdateroad/di/NetworkModule.kt b/app/src/main/java/org/sopt/teamdateroad/di/NetworkModule.kt index e263980..a35b157 100644 --- a/app/src/main/java/org/sopt/teamdateroad/di/NetworkModule.kt +++ b/app/src/main/java/org/sopt/teamdateroad/di/NetworkModule.kt @@ -18,7 +18,9 @@ import org.sopt.teamdateroad.BuildConfig.DEBUG import org.sopt.teamdateroad.data.dataremote.interceptor.AuthInterceptor import org.sopt.teamdateroad.di.qualifier.Auth import org.sopt.teamdateroad.di.qualifier.DateRoad +import org.sopt.teamdateroad.di.qualifier.PlaceSearch import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory @Module @InstallIn(SingletonComponent::class) @@ -35,6 +37,7 @@ object NetworkModule { } @Provides + @DateRoad @Singleton fun providesOkHttpClient( loggingInterceptor: HttpLoggingInterceptor, @@ -65,7 +68,7 @@ object NetworkModule { @DateRoad @Singleton fun providesDateRoadRetrofit( - okHttpClient: OkHttpClient, + @DateRoad okHttpClient: OkHttpClient, json: Json ): Retrofit = Retrofit.Builder() @@ -75,4 +78,41 @@ object NetworkModule { json.asConverterFactory(requireNotNull("application/json".toMediaTypeOrNull())) ) .build() + + @PlaceSearch + @Provides + @Singleton + fun providesPlaceSearchRetrofit( + @PlaceSearch okHttpClient: OkHttpClient + ): Retrofit = + Retrofit.Builder() + .baseUrl(BuildConfig.KAKAO_BASE_URL) + .client(okHttpClient) + .addConverterFactory(GsonConverterFactory.create()) + .build() + + @PlaceSearch + @Provides + @Singleton + fun providesPlaceSearchOkHttpClient( + loggingInterceptor: HttpLoggingInterceptor, + @PlaceSearch interceptor: Interceptor + ): OkHttpClient = + OkHttpClient.Builder().apply { + connectTimeout(10, TimeUnit.SECONDS) + writeTimeout(10, TimeUnit.SECONDS) + readTimeout(10, TimeUnit.SECONDS) + addInterceptor(interceptor) + if (DEBUG) addInterceptor(loggingInterceptor) + }.build() + + @PlaceSearch + @Provides + @Singleton + fun providesPlaceSearchAuthInterceptor(): Interceptor = Interceptor { chain -> + val request = chain.request().newBuilder() + .addHeader("Authorization", "KakaoAK ${BuildConfig.KAKAO_REST_API_KEY}") + .build() + chain.proceed(request) + } } diff --git a/app/src/main/java/org/sopt/teamdateroad/di/RepositoryModule.kt b/app/src/main/java/org/sopt/teamdateroad/di/RepositoryModule.kt index 7a35d9c..409d22c 100644 --- a/app/src/main/java/org/sopt/teamdateroad/di/RepositoryModule.kt +++ b/app/src/main/java/org/sopt/teamdateroad/di/RepositoryModule.kt @@ -9,6 +9,7 @@ import org.sopt.teamdateroad.data.repositoryimpl.AdvertisementRepositoryImpl import org.sopt.teamdateroad.data.repositoryimpl.AuthRepositoryImpl import org.sopt.teamdateroad.data.repositoryimpl.CourseRepositoryImpl import org.sopt.teamdateroad.data.repositoryimpl.MyCourseRepositoryImpl +import org.sopt.teamdateroad.data.repositoryimpl.PlaceSearchRepositoryImpl import org.sopt.teamdateroad.data.repositoryimpl.ProfileRepositoryImpl import org.sopt.teamdateroad.data.repositoryimpl.TimelineRepositoryImpl import org.sopt.teamdateroad.data.repositoryimpl.UserInfoRepositoryImpl @@ -17,6 +18,7 @@ import org.sopt.teamdateroad.domain.repository.AdvertisementRepository import org.sopt.teamdateroad.domain.repository.AuthRepository import org.sopt.teamdateroad.domain.repository.CourseRepository import org.sopt.teamdateroad.domain.repository.MyCourseRepository +import org.sopt.teamdateroad.domain.repository.PlaceSearchRepository import org.sopt.teamdateroad.domain.repository.ProfileRepository import org.sopt.teamdateroad.domain.repository.TimelineRepository import org.sopt.teamdateroad.domain.repository.UserInfoRepository @@ -56,4 +58,8 @@ abstract class RepositoryModule { @Binds @Singleton abstract fun bindsUserPointRepository(userPointRepositoryImpl: UserPointRepositoryImpl): UserPointRepository + + @Binds + @Singleton + abstract fun bindsPlaceSearchRepository(placeSearchRepositoryImpl: PlaceSearchRepositoryImpl): PlaceSearchRepository } diff --git a/app/src/main/java/org/sopt/teamdateroad/di/ServiceModule.kt b/app/src/main/java/org/sopt/teamdateroad/di/ServiceModule.kt index 5af0faf..a37c8f3 100644 --- a/app/src/main/java/org/sopt/teamdateroad/di/ServiceModule.kt +++ b/app/src/main/java/org/sopt/teamdateroad/di/ServiceModule.kt @@ -4,44 +4,59 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton import org.sopt.teamdateroad.data.dataremote.service.AdvertisementService import org.sopt.teamdateroad.data.dataremote.service.AuthService import org.sopt.teamdateroad.data.dataremote.service.CourseService import org.sopt.teamdateroad.data.dataremote.service.MyCourseService +import org.sopt.teamdateroad.data.dataremote.service.PlaceSearchService import org.sopt.teamdateroad.data.dataremote.service.ProfileService import org.sopt.teamdateroad.data.dataremote.service.TimelineService import org.sopt.teamdateroad.data.dataremote.service.UserPointService import org.sopt.teamdateroad.di.qualifier.DateRoad +import org.sopt.teamdateroad.di.qualifier.PlaceSearch import retrofit2.Retrofit @Module @InstallIn(SingletonComponent::class) object ServiceModule { @Provides + @Singleton fun providesAdvertisementService(@DateRoad retrofit: Retrofit): AdvertisementService = retrofit.create(AdvertisementService::class.java) @Provides + @Singleton fun providesAuthService(@DateRoad retrofit: Retrofit): AuthService = retrofit.create(AuthService::class.java) @Provides + @Singleton fun providesCourseService(@DateRoad retrofit: Retrofit): CourseService = retrofit.create(CourseService::class.java) @Provides + @Singleton fun providesMyCourseService(@DateRoad retrofit: Retrofit): MyCourseService = retrofit.create(MyCourseService::class.java) @Provides + @Singleton fun providesProfileService(@DateRoad retrofit: Retrofit): ProfileService = retrofit.create(ProfileService::class.java) @Provides + @Singleton fun provideTimelineService(@DateRoad retrofit: Retrofit): TimelineService = retrofit.create(TimelineService::class.java) @Provides + @Singleton fun providesUserPointService(@DateRoad retrofit: Retrofit): UserPointService = retrofit.create(UserPointService::class.java) + + @Provides + @Singleton + fun providesPlaceSearchService(@PlaceSearch retrofit: Retrofit): PlaceSearchService = + retrofit.create(PlaceSearchService::class.java) } diff --git a/app/src/main/java/org/sopt/teamdateroad/di/qualifier/Qualifier.kt b/app/src/main/java/org/sopt/teamdateroad/di/qualifier/Qualifier.kt index 4a103a5..43e51de 100644 --- a/app/src/main/java/org/sopt/teamdateroad/di/qualifier/Qualifier.kt +++ b/app/src/main/java/org/sopt/teamdateroad/di/qualifier/Qualifier.kt @@ -7,3 +7,6 @@ annotation class DateRoad @Qualifier annotation class Auth + +@Qualifier +annotation class PlaceSearch diff --git a/app/src/main/java/org/sopt/teamdateroad/domain/model/PlaceInfo.kt b/app/src/main/java/org/sopt/teamdateroad/domain/model/PlaceInfo.kt new file mode 100644 index 0000000..5657ace --- /dev/null +++ b/app/src/main/java/org/sopt/teamdateroad/domain/model/PlaceInfo.kt @@ -0,0 +1,6 @@ +package org.sopt.teamdateroad.domain.model + +data class PlaceInfo( + val placeName: String, + val addressName: String +) diff --git a/app/src/main/java/org/sopt/teamdateroad/domain/model/PlaceSearchResult.kt b/app/src/main/java/org/sopt/teamdateroad/domain/model/PlaceSearchResult.kt new file mode 100644 index 0000000..78a45ee --- /dev/null +++ b/app/src/main/java/org/sopt/teamdateroad/domain/model/PlaceSearchResult.kt @@ -0,0 +1,5 @@ +package org.sopt.teamdateroad.domain.model + +data class PlaceSearchResult( + val placeInfos: List +) diff --git a/app/src/main/java/org/sopt/teamdateroad/domain/repository/PlaceSearchRepository.kt b/app/src/main/java/org/sopt/teamdateroad/domain/repository/PlaceSearchRepository.kt new file mode 100644 index 0000000..0506f04 --- /dev/null +++ b/app/src/main/java/org/sopt/teamdateroad/domain/repository/PlaceSearchRepository.kt @@ -0,0 +1,9 @@ +package org.sopt.teamdateroad.domain.repository + +import org.sopt.teamdateroad.domain.model.PlaceSearchResult + +interface PlaceSearchRepository { + suspend fun getPlaceSearchResult( + keyword: String + ): Result +} diff --git a/app/src/main/java/org/sopt/teamdateroad/domain/usecase/GetPlaceSearchResultUseCase.kt b/app/src/main/java/org/sopt/teamdateroad/domain/usecase/GetPlaceSearchResultUseCase.kt new file mode 100644 index 0000000..4866486 --- /dev/null +++ b/app/src/main/java/org/sopt/teamdateroad/domain/usecase/GetPlaceSearchResultUseCase.kt @@ -0,0 +1,11 @@ +package org.sopt.teamdateroad.domain.usecase + +import javax.inject.Inject +import org.sopt.teamdateroad.domain.model.PlaceSearchResult +import org.sopt.teamdateroad.domain.repository.PlaceSearchRepository + +class GetPlaceSearchResultUseCase @Inject constructor( + private val placeSearchRepository: PlaceSearchRepository +) { + suspend operator fun invoke(keyword: String): Result = placeSearchRepository.getPlaceSearchResult(keyword) +} diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/navigator/MainActivity.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/navigator/MainActivity.kt index 4f3dad9..22c0ce3 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/navigator/MainActivity.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/navigator/MainActivity.kt @@ -27,6 +27,7 @@ class MainActivity : ComponentActivity() { delay(SPLASH_SCREEN_DELAY) showSplash = false } + if (showSplash) { SplashScreen() } else { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 06ee401..7fbd2c4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -39,7 +39,7 @@ accompanistWebview = "0.24.13-rc" # Third Party okhttp = "4.11.0" -retrofit = "2.9.0" +retrofit = "2.11.0" retrofitKotlinSerializationConverter = "1.0.0" kotlinxSerializationJson = "1.6.3" daggerHilt = "2.51" From 1e55fae970202d3276285cdc565543e31a0246a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=84=EC=9A=B0?= <85734140+jinuemong@users.noreply.github.com> Date: Wed, 26 Mar 2025 09:04:59 +0900 Subject: [PATCH 09/49] =?UTF-8?q?[feat]=20DefaultButtonSheet=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20&=20Point=20=EC=88=98=EC=A7=91=20UI=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20(#10)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: ํฌ์ธํŠธ ui ๋ณ€๊ฒฝ์‚ฌํ•ญ ์ ์šฉ * feat: ํฌ์ธํŠธ ๋ฐ”ํ…€์‹œํŠธ ๊ตฌํ˜„ * feat: ํฌ์ธํŠธ ๋ฐ”ํ…€์‹œํŠธ ์—ฐ๊ฒฐ * style : ์ฝ”๋“œ ํฌ๋งท ์ˆ˜์ • & ๋ฆฐํŠธ ์ฒดํฌ * refactor : ์ค‘๋ณต ์ฝ”๋“œ ์ œ๊ฑฐ --- .../bottomsheet/DateRoadPointBottomSheet.kt | 134 ++++++++++++++++++ .../bottomsheet/DefaultDateRoadBottomSheet.kt | 84 +++++++++++ .../model/collect/DateRoadCollectPointType.kt | 22 +++ .../ui/pointhistory/PointHistoryContract.kt | 3 + .../ui/pointhistory/PointHistoryScreen.kt | 28 +++- .../ui/pointhistory/PointHistoryViewModel.kt | 2 + .../component/PointHistoryPointBox.kt | 32 +++-- app/src/main/res/drawable/ic_right_arrow.xml | 13 ++ app/src/main/res/drawable/img_collect_ads.png | Bin 0 -> 6906 bytes .../main/res/drawable/img_collect_course.png | Bin 0 -> 11789 bytes app/src/main/res/values/strings.xml | 9 +- 11 files changed, 306 insertions(+), 21 deletions(-) create mode 100644 app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadPointBottomSheet.kt create mode 100644 app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DefaultDateRoadBottomSheet.kt create mode 100644 app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/model/collect/DateRoadCollectPointType.kt create mode 100644 app/src/main/res/drawable/ic_right_arrow.xml create mode 100644 app/src/main/res/drawable/img_collect_ads.png create mode 100644 app/src/main/res/drawable/img_collect_course.png diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadPointBottomSheet.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadPointBottomSheet.kt new file mode 100644 index 0000000..f4a0407 --- /dev/null +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadPointBottomSheet.kt @@ -0,0 +1,134 @@ +package org.sopt.teamdateroad.presentation.ui.component.bottomsheet + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.teamdateroad.R +import org.sopt.teamdateroad.presentation.ui.component.bottomsheet.model.collect.DateRoadCollectPointType +import org.sopt.teamdateroad.presentation.util.modifier.noRippleClickable +import org.sopt.teamdateroad.ui.theme.DateRoadTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DateRoadPointBottomSheet( + isBottomSheetOpen: Boolean, + onClick: (DateRoadCollectPointType) -> Unit, + onDismissRequest: () -> Unit = {} +) { + DefaultDateRoadBottomSheet( + isBottomSheetOpen = isBottomSheetOpen, + onDismissRequest = onDismissRequest, + content = { + Spacer(modifier = Modifier.height(15.dp)) + Box( + modifier = Modifier + .fillMaxWidth() + .padding( + start = 16.dp, + end = 6.dp + ) + ) { + Text( + text = stringResource(R.string.point_box_get_point_button_text), + color = DateRoadTheme.colors.black, + style = DateRoadTheme.typography.bodyBold17, + modifier = Modifier.align(Alignment.CenterStart) + ) + Image( + modifier = Modifier + .align(Alignment.CenterEnd) + .padding(15.dp) + .noRippleClickable(onClick = onDismissRequest), + painter = painterResource(id = R.drawable.ic_bottom_sheet_close), + contentDescription = null + ) + } + Spacer(modifier = Modifier.height(17.dp)) + DateRoadBottonSheetContent( + dateLoadCollectPoint = DateRoadCollectPointType.WATCH_ADS, + onClick = onClick + ) + DateRoadBottonSheetContent( + dateLoadCollectPoint = DateRoadCollectPointType.COURSE_REGISTRATION, + onClick = onClick + ) + Spacer(modifier = Modifier.height(10.dp)) + } + ) +} + +@Composable +fun DateRoadBottonSheetContent( + dateLoadCollectPoint: DateRoadCollectPointType, + onClick: (DateRoadCollectPointType) -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + onClick(dateLoadCollectPoint) + } + .padding( + start = 20.dp, + end = 23.dp + ) + .padding(vertical = 15.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + modifier = Modifier.size(48.dp), + painter = painterResource(id = dateLoadCollectPoint.imageResource), + contentDescription = null + ) + Spacer(modifier = Modifier.width(14.dp)) + Column( + modifier = Modifier.wrapContentWidth() + ) { + Text( + text = stringResource(dateLoadCollectPoint.titleRes), + color = DateRoadTheme.colors.gray600, + style = DateRoadTheme.typography.bodyBold15 + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = stringResource(dateLoadCollectPoint.contentRes), + color = DateRoadTheme.colors.gray400, + style = DateRoadTheme.typography.bodySemi13 + + ) + } + Spacer(modifier = Modifier.weight(1f)) + Image( + painter = painterResource(R.drawable.ic_my_page_arrow), + contentDescription = null, + modifier = Modifier.align(Alignment.CenterVertically) + ) + } +} + +@Preview +@Composable +fun DateRoadPointBottomSheetPreView() { + DateRoadPointBottomSheet( + isBottomSheetOpen = true, + onClick = {} + ) +} diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DefaultDateRoadBottomSheet.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DefaultDateRoadBottomSheet.kt new file mode 100644 index 0000000..8023a8f --- /dev/null +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DefaultDateRoadBottomSheet.kt @@ -0,0 +1,84 @@ +package org.sopt.teamdateroad.presentation.ui.component.bottomsheet + +import android.annotation.SuppressLint +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.windowInsetsBottomHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.SheetState +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch +import org.sopt.teamdateroad.ui.theme.DATEROADTheme +import org.sopt.teamdateroad.ui.theme.DateRoadTheme + +@SuppressLint("CoroutineCreationDuringComposition") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DefaultDateRoadBottomSheet( + modifier: Modifier = Modifier, + sheetShape: RoundedCornerShape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), + sheetState: SheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), + isBottomSheetOpen: Boolean, + onDismissRequest: () -> Unit = {}, + content: @Composable () -> Unit +) { + val coroutineScope = rememberCoroutineScope() + if (isBottomSheetOpen) { + coroutineScope.launch { + sheetState.show() + } + ModalBottomSheet( + modifier = Modifier.padding(bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()), + sheetState = sheetState, + shape = sheetShape, + onDismissRequest = onDismissRequest, + containerColor = DateRoadTheme.colors.white, + dragHandle = null + ) { + Column( + modifier = modifier + ) { + content() + Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +@Composable +fun DefaultDateRoadBottomSheetPreview() { + DATEROADTheme { + var isBottomSheetOpen by rememberSaveable { mutableStateOf(false) } + + DefaultDateRoadBottomSheet( + modifier = Modifier.padding(top = 20.dp, bottom = 16.dp, start = 16.dp, end = 16.dp), + isBottomSheetOpen = isBottomSheetOpen, + onDismissRequest = { isBottomSheetOpen = !isBottomSheetOpen }, + content = { + Text( + text = "BottomSheet", + color = DateRoadTheme.colors.white, + style = DateRoadTheme.typography.titleExtra24 + ) + } + ) + } +} diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/model/collect/DateRoadCollectPointType.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/model/collect/DateRoadCollectPointType.kt new file mode 100644 index 0000000..f7458d7 --- /dev/null +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/model/collect/DateRoadCollectPointType.kt @@ -0,0 +1,22 @@ +package org.sopt.teamdateroad.presentation.ui.component.bottomsheet.model.collect + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import org.sopt.teamdateroad.R + +enum class DateRoadCollectPointType( + @StringRes val titleRes: Int, + @StringRes val contentRes: Int, + @DrawableRes val imageResource: Int +) { + WATCH_ADS( + titleRes = R.string.collect_point_ads_title, + contentRes = R.string.collect_point_ads_content, + imageResource = R.drawable.img_collect_ads + ), + COURSE_REGISTRATION( + titleRes = R.string.collect_point_registration_title, + contentRes = R.string.collect_point_registration_content, + imageResource = R.drawable.img_collect_course + ) +} diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryContract.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryContract.kt index 07d07ae..edccc6a 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryContract.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryContract.kt @@ -12,6 +12,7 @@ class PointHistoryContract { data class PointHistoryUiState( val loadState: LoadState = LoadState.Idle, val userPoint: UserPoint = UserPoint(), + val isPointCollectBottomSheetOpen: Boolean = false, val pointHistoryTabType: PointHistoryTabType = PointHistoryTabType.GAINED_HISTORY, val pointHistory: PointHistory = PointHistory() ) : UiState @@ -24,5 +25,7 @@ class PointHistoryContract { data class FetchPointHistory(val loadState: LoadState, val pointHistory: PointHistory) : PointHistoryEvent() data class FetchUserPoint(val loadState: LoadState, val userPoint: UserPoint) : PointHistoryEvent() data class OnTabBarClicked(val pointHistoryTabType: PointHistoryTabType) : PointHistoryEvent() + data object OnPointCollectBottomSheetClick : PointHistoryEvent() + data object OnPointCollectBottomSheetDismiss : PointHistoryEvent() } } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryScreen.kt index 0768afc..778bb45 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryScreen.kt @@ -27,6 +27,7 @@ import org.sopt.teamdateroad.domain.model.PointHistory import org.sopt.teamdateroad.domain.model.UserPoint import org.sopt.teamdateroad.presentation.type.EmptyViewType import org.sopt.teamdateroad.presentation.type.PointHistoryTabType +import org.sopt.teamdateroad.presentation.ui.component.bottomsheet.DateRoadPointBottomSheet import org.sopt.teamdateroad.presentation.ui.component.tabbar.DateRoadTabBar import org.sopt.teamdateroad.presentation.ui.component.tabbar.DateRoadTabTitle import org.sopt.teamdateroad.presentation.ui.component.topbar.DateRoadBasicTopBar @@ -77,7 +78,9 @@ fun PointHistoryRoute( PointHistoryContract.PointHistoryEvent.OnTabBarClicked(pointHistoryTabType) ) }, - onTopBarIconClicked = { viewModel.setSideEffect(PointHistoryContract.PointHistorySideEffect.PopBackStack) } + onTopBarIconClicked = { viewModel.setSideEffect(PointHistoryContract.PointHistorySideEffect.PopBackStack) }, + onClickCollectPoint = { viewModel.setEvent(PointHistoryContract.PointHistoryEvent.OnPointCollectBottomSheetClick) }, + onDisMissCollectPoint = { viewModel.setEvent(PointHistoryContract.PointHistoryEvent.OnPointCollectBottomSheetDismiss) } ) } @@ -90,7 +93,9 @@ fun PointHistoryScreen( padding: PaddingValues, pointHistoryUiState: PointHistoryContract.PointHistoryUiState = PointHistoryContract.PointHistoryUiState(), onTabBarClicked: (PointHistoryTabType) -> Unit, - onTopBarIconClicked: () -> Unit + onTopBarIconClicked: () -> Unit, + onClickCollectPoint: () -> Unit, + onDisMissCollectPoint: () -> Unit ) { Column( modifier = Modifier @@ -104,10 +109,11 @@ fun PointHistoryScreen( backGroundColor = DateRoadTheme.colors.white, onLeftIconClick = onTopBarIconClicked ) - Spacer(modifier = Modifier.height(22.dp)) + Spacer(modifier = Modifier.height(16.dp)) PointHistoryPointBox( modifier = Modifier.padding(horizontal = 16.dp), - userPoint = pointHistoryUiState.userPoint + userPoint = pointHistoryUiState.userPoint, + onClickCollectPoint = onClickCollectPoint ) Spacer(modifier = Modifier.height(16.dp)) DateRoadTabBar( @@ -155,6 +161,16 @@ fun PointHistoryScreen( } } } + + DateRoadPointBottomSheet( + isBottomSheetOpen = pointHistoryUiState.isPointCollectBottomSheetOpen, + onClick = { + onDisMissCollectPoint() + }, + onDismissRequest = { + onDisMissCollectPoint() + } + ) } @Preview @@ -176,7 +192,9 @@ fun PointHistoryPreview() { ) ), onTabBarClicked = {}, - onTopBarIconClicked = {} + onTopBarIconClicked = {}, + onClickCollectPoint = {}, + onDisMissCollectPoint = {} ) } } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryViewModel.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryViewModel.kt index 8ca13b3..2772fcb 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryViewModel.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryViewModel.kt @@ -22,6 +22,8 @@ class PointHistoryViewModel @Inject constructor( is PointHistoryContract.PointHistoryEvent.FetchPointHistory -> setState { copy(loadState = event.loadState, pointHistory = event.pointHistory) } is PointHistoryContract.PointHistoryEvent.OnTabBarClicked -> setState { copy(pointHistoryTabType = event.pointHistoryTabType) } is PointHistoryContract.PointHistoryEvent.FetchUserPoint -> setState { copy(loadState = event.loadState, userPoint = event.userPoint) } + PointHistoryContract.PointHistoryEvent.OnPointCollectBottomSheetClick -> setState { copy(isPointCollectBottomSheetOpen = true) } + PointHistoryContract.PointHistoryEvent.OnPointCollectBottomSheetDismiss -> setState { copy(isPointCollectBottomSheetOpen = false) } } } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/component/PointHistoryPointBox.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/component/PointHistoryPointBox.kt index 7e0e164..b158521 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/component/PointHistoryPointBox.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/component/PointHistoryPointBox.kt @@ -1,49 +1,48 @@ package org.sopt.teamdateroad.presentation.ui.pointhistory.component -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.sopt.teamdateroad.R import org.sopt.teamdateroad.domain.model.UserPoint +import org.sopt.teamdateroad.presentation.ui.component.button.DateRoadBasicButton import org.sopt.teamdateroad.ui.theme.DateRoadTheme @Composable fun PointHistoryPointBox( modifier: Modifier = Modifier, - userPoint: UserPoint + userPoint: UserPoint, + onClickCollectPoint: () -> Unit ) { Column( - modifier = modifier - .fillMaxWidth() - .clip(RoundedCornerShape(14.dp)) - .background(DateRoadTheme.colors.purple600) - .padding(start = 16.dp, top = 16.dp, bottom = 14.dp) + modifier = modifier.fillMaxWidth() ) { Text( text = stringResource(id = R.string.point_box_nickname, userPoint.name), - color = DateRoadTheme.colors.white, - style = DateRoadTheme.typography.bodyMed13 + color = DateRoadTheme.colors.gray400, + style = DateRoadTheme.typography.bodyBold13 ) - Spacer(modifier = Modifier.height(11.dp)) + Spacer(modifier = Modifier.height(10.dp)) Text( text = userPoint.point, - color = DateRoadTheme.colors.white, + color = DateRoadTheme.colors.black, style = DateRoadTheme.typography.titleExtra24, maxLines = 1, overflow = TextOverflow.Ellipsis ) + Spacer(modifier = Modifier.height(20.dp)) + DateRoadBasicButton( + textContent = stringResource(R.string.point_box_get_point_button_text), + onClick = onClickCollectPoint + ) } } @@ -51,6 +50,9 @@ fun PointHistoryPointBox( @Composable fun PointHistoryPointBoxPreview() { Column { - PointHistoryPointBox(userPoint = UserPoint()) + PointHistoryPointBox( + userPoint = UserPoint(), + onClickCollectPoint = {} + ) } } diff --git a/app/src/main/res/drawable/ic_right_arrow.xml b/app/src/main/res/drawable/ic_right_arrow.xml new file mode 100644 index 0000000..bcbd45c --- /dev/null +++ b/app/src/main/res/drawable/ic_right_arrow.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/img_collect_ads.png b/app/src/main/res/drawable/img_collect_ads.png new file mode 100644 index 0000000000000000000000000000000000000000..dbe93ac60f51c6b826981dcb373fe22b0565ca4c GIT binary patch literal 6906 zcmVA3YkqC8BfaVjYF2-ms=hQBCbrF+m!gj}#84bx5*VlM1 z&ixc?O>7GWX|R8wV}xo5jH(!tEHUa4Z}Mb8iuq#Gd`<>QiUjdmAkd(AFc>HLZ9_nk zYKW>7RYf2WW+F!28qv1|QF>gJr1ThMI-%AHLJd@tO8K5ZAkE|?^@?}0SdgZhd;syG z4{%1S!#bIU$j9&{fk2v=P?|WWoBcu>Hz+c_ys@K?d%_4`hPEJGU5TNsq6lPoocr*Q>PP`XTcvrViXqUVvxe`iN^@Ksta$m2j+Ima zC_8}=uHuYVRZ!8-9}roas3R>bw;Gj=K(YWB6F!I2EmTK7D61Nkg+PYac|km!Pj%#v zGOCeAC58xDH%~_CU8<9(a1h~Nd?DYx;2XmwSM!o8(1m9Y-&YS=#f8Y!?tf`x$8stW zNuEd9PCeku^|cs7S3y0F+@c~Q3=9}DgyEp0V5jI zZkapvuGLfo6oo(_506tH)gY7)k*Z+|Gt)i~*lkLcKw zwii=H3W8#gsGtO@74kqu{`|c6=wv~hV+3*hghG?3U<9fa!+g3R&Y~;ctZ1dlfPxz9 zf)EI)I;zXnOdb$s=?`A@o#MD)go42Zg-G%9CQvQZc|e-$f4r`22Dm6FP@O19dBUhL zcTtt%^mE^%rGNV>RUJ6*qG%Ltw}A3MUY)7v+oCG&o~T;#Br$*6{yXlYJdiVi(5qg} z9i#9(7yZo!s(NsK%eK9@=iH7td7x@H&G0(6{^ChLXIfs7v#HKWfnb`eIpbP%<)&I9 z56})jv~75W=Qj0BAPj!5=8Ox(S=jS7)gb0mD0U}#geL+abcxjps$!r>=vR#Hosdh; zfFIiT58Yl)@s!5PdQTMt>VryirQ@|%KB6x^8>KHl-%DR?AEghr?4o@j&!|qGqFd!SR^$PJJ|S zTsnOJ@6Hs<8RQjlqLoLr+_NH^W3wWVF(h>xRXOhJd$erk`N?0tLr*>OwuKl=go1qU zhiB5PBU&xTUUbe=^#0ae#xY-~!{@ZhrB#p)D)jKrHwlH>EBE*HUrMK)Ih#Bp>#QuR z5Qh7=sLBE26p}n30>TO!`b+T`%W*QaoRV_vFMjr>ST?0>e__pgv|fa()h}Ek!qSYy z_T$83MWF8qg_$+SN-#X{U9wg_epp=lG;zL1AS*+GaY?2FGZM&9pI)kRU@T9*xUqL# ziXSoK+l!7)AyB^{i6YOeBiiWJySk}Ggs#^Gah`nS9eJ%Am#w2;zkXFBq?rkW{WQn= zI;VYO4xM0-Oes+g=z*c0*K*H5x`Q$jNE8fLQ4L^r0SE;0IC&E^cxziD&6?eovJdJb zj}heQ6H1}T0TO^nAQ!IrlpvGxGiCyPAV>n!Y)6YhHYXx`03y#ox&zV@NZz+zBU7vt zGJK)&?tS2VI{w5N@-e)L6Oc(M!u^@>DfUnV6a3U8?~2rXT&U68v{a}URLC68oCu5S z{`Y_C!%j{x9h(EXGpSJ8IuNJo_dW{KI&+r#1fU`S!V1FGEW_Bk;e)M7)jQ|>qaBY~ zdi{dLH9vT7w|v}U%FYylQ2)VW_@>?^&n5ORzBGqa-iovwa8;mMQlCP>o_3>Azcd=y zDa`CMg_qX$MyE>(WrD%cJ<|UORXNR2DuhHRQ~#Q+V*6)~N(2R4a#`L4N~=Py1mcXA zL4s-pX^68F##L#l9tx6B#?bO0RIH#DFAsS zc6@q>s~2)4P_1NMW*j`wK!t>zJOIKAyot)FT1?^X%g>!XnB5mCoFM#_A`X=1_^%x* z$#(b=>BD9&c>kRvy1@yjTcsU6P%YGb?AWujmGXtXvA=!6(e#rC&!=1OUYJl4H2Oc? zq4x;EWGgrk4%p|?w{^HFqeGVkJQu=bp~(a6+0{yJg5pS^TAbVxA5y+RH5lOo%dPix z(?hEl%P?})4X4n*zqVBR768RT_#+Tbk?yruK6LB|ecxoX%|7aI6ItKil79&z9lrQP z68&aO3TEGzLZC%w5BC;gxJHBcB1~;z)Gq_kXftjLLJ1P$^X~M3E7`2~cGL1+%&Sa(n(I2sixh%_M0x zPB)8Hzj?ofK!$nlrW_CyS8DIjD4at(ge+uZWG5IGjGm0>go?$uiD;A{TE2h>0*RXP zxOL})ExW0I?N;jlxA*Ap?^$Tcf*K9r6<>2t~zqod=pR~@+hrdR2I-`q}%MQ=H}ILtlm2wP@_g17ng>q{kxRNs{% z5BwC2xZR*D;=zc!Wor52EAgc=!dC(llhRDzubx}J>KC3hydf6@-FaB?$Wtho)k=~} zsv0*`P2Y~uh!*)7-D-hN2-VFJFzJSGP~ z28M2lr8k^flvKOWfco6gd!6IHaF9Aw7)!MLUz|PMBkaWsGiFv$Dph2FL?Dw| z)bqyX3F@4ano2`S;A=o0r>=}j#*YPg+-hT@mtx5)8#|<#PIw@p;ETy48k_vDJYF#J zr&hV748SqaDAcM`s1^v-HcgQ15J9r(qvA`pQ|$H6Z9*?Ub7tDYs>sy4$;if%CYpP{ zMY*xDwg{97;|Ck*)89LjYwmH|>7+Ba%B7OAq5l+m>FEsPsF@U-Tl9yN`5z|W~; ztrLiQPu+tRp0-yq_U>t`7!sj)`m0}di)Ep=6AjBF2_)j!$>b55R`);o03>>`ZxNLf zf7m>me)|2Zg!-K7`CP>r6^QB~tPDUn!vbGaN`N@Xv_9ONdy$eGoME{Z5Ye+fWoli8 z@&(9qsP2xMdD)-gGp5{&8Ld?Gu2J4vK!FoMspch3e!f1fW3U2ODJVC|{@! zVGsn5zuMJGpMTOxU+riu_=rm&5}~eOoKaUUW1lN&ERWyu+3$~_V;6jw@i>s<;C?}n zPi9C`?Ag^OiGl%NV`6(TNRk!-njg${@K`9+NKAv7;<7Y}&Loa{Hld?ZU+TyXBYTqB z7ziQNV`p1QR3tA5?gT}@Mi=wg*2utA7RH#dPGn_VZsVj#q+gu#xbZx0Jg<{X;kW;B zD!ushDFqpqT~aV@ZcyVi*K0EdgG}~bgFDGJm$@C&HA(V@$tr=d9dc!#3`D9H5H|>= zD_E_m*zP~J(t-Uxe6R6%M^4GOAv(|+(zV9ejz}i|vr&aRJYkYm#&LnfH50F)j+P|T zmCH&p{QU(OidX@NPWtqV!f37xVO~viC~TCW4FzFDrWQA^oSDRob5o}$N>EXM#FVN* zy)+Zabm|J;8KKf3mG~FuvBWWcQ8C|s4;Z!Qq(B-=wm;g`wT+n00Xq_#m?9Ckou*jm zAWQNTW`s=T!vgv)hRSF_(aU%(Zg4AZ5I#w`N+DP*M!rQ(Sy(7+9Tyle8*cvaf*@yc0 zR)LU(Q4WyKM+pE|qL6%&3{>3wVwN!H*d)d^n7StCrsKFtfs%iT9?tA9!6@P6jpy=8 zOuyqJsa`B`8Af_o)`Zzt&W;(^=cc#}^@v2w9gvz*BG!1u$0 zoJs0iVGiT^HMJz~Buz|Y7w}Q#4uv$i#;_5P3$Lq*3&X&x>bIG_3BTeRCV46`Ec+c! zayfw8UfYahiGrN|0?P+x=V36Y$rcHdvl1&pL9L=78#mU*9lPaFv7mUzL*yY7W9fQP zZ59J>YGh=BA`{-6B0h*rWCM{vC@3Px6_hH*B-sK%vpFV9)!f)@|8s3nX5Q$MA!qe7 zU&K;2NM0g^w006%*6OLtAEv)QqXy?N9~KlIN`*DljK_^N=fzPm!5z;E6biD8$KV4v zcrYqdCsMR;#bhMC`&5L|)r`r$x^O5U!z2hMRm@2}h!QPm%K@zi#wkJ2l>KUhqrQr_Peh0jv=#E*@o z?$^Vk3X93`sH>WxVh&!+l1*}l1~)4Yw{gf4`%*&~IXy2hEGp&zrCuOXf@}#@vl2{j zX9yqD5ce>o72YHr@S=N1B2?+X2N93xAI5|W*OOt<941X>U%`PiNk);l4 z;TR2OdqN;h1_tkg*qALkXKHe0k0Xx>&`%$nO6Oh>rd%+kg~_GTfX`#st})uT&-$g} z+`9M~JitU69~KIfD=d*s0}y7i|0a`sm!nf(*p>12!H=_??GDk`~+=zH%hnOxL=+N5-Q*t z*WJ`azj-A>_laXcu53IP)D5qJMJZf9fZ5)=xAIFHULZtOaz=hSqV0c2SMC7 zWV);M@@pDpLqcKfK*N6WQHcq%Pw(L-YEXsE@1z6U5Q?MIr+_pxB88Ee#Y6! zd4=Jlph^lL7s!LKc;VS2bj`)1k|dX#6cHpt2)*OpDbl-TQcQ*Y2eLGqegNYGSjn%0 zC_MH@viN1fbXDkJ$eXvA>Y_lhkwwUPO)MaU9V+zg+L8K=N5d*5fD&yMp>3z_I9%VX ze*Gc@nnN1Y3Sm*bHqOU&%!G$Rf!rV$_Cx8e5X)wvLiB#=FXp<&UO>()-B&BFGl*@s3}4G5y4K{LS|1_4R99m(e)mm(ve z{FfpR0MeazepnjzbOZv%$D-8K93nqMwA^6d8a9N-<~b7AiT`hQJ1H@c-UaACL7_3* zZVaVBXc#o&dZ9Skr0+Opmr$9LL>PloC?KqXl$$oDFJ^*d@K2e5kaZr&A_#=PQTZt4 zzwF9}V@^CJv%f)o$nW4L%aeG=qK9H2#@z{&&3sa*b=V7MZa@YFDrfq}IsqXLAw!Zu zgc_*C4}=#%qk@RQ$D)z#J`2ZzcxIBFF8t724y402khSNM2H6Xt2!m@nkrc| zbG}YJOuS=J;~7YIPP!Ccp?OLu>zT{8Wbg~=pgao9FZ>AB_+H;8Qh5as;K-u`mJ0V` zVXUE2N=-l>7~#_mQw?$?3X_6>c%8d%J0Q2I?Ks~l2#v zr6rzqD9Z;jT)pDHAGIEk14KfIdE}w76xB0_ul27+=-fqNsY)OkNT<^Ct;TuqIw%9& zXS#+m*3UdKPMHvH@b~Pb@i?tEKewC+GB6OPn{G9r`u02!sFsWG^+PM+FCl!dO&bWBD9{c3&705RTAb<`F_ z=4I~lazj?;CdT5+Da(7u>ZlI5)qU*9ia>p9nuZwh2dIvkfN1NL*{F|xtOs0sX=EGa zFfn-@zJg<)mm56g=fX^9G*xx@8!;`k(97|j2=v&R(11{&dMK+>cnMt!o-2g0$4~uz1C3?7>a74C6p%$(o5QV*;rQ~9)J?HQyqQ)O!Iz;kIaupX#>@> zvvNT<%mws3b`*WEvrO@MU_*TzQOPlc{;fh~TW%=&{W?hTV=RUgzutpiBA~)t3i24Q zRXWuB&Psqh`?kEIq<0MBjfr0T`T+gnU_|t)FE1r|fU?~1Rrf`O1wt13wMr4P9A3M8 z`0gbWD>$(`sgB$sLKa4enrBWeCCYxwXeHg+5@pjV4z&JoP6N>smGv78U?!jL?)0XPVsf|iH1Hu zlE#dxn!B#Ihqnj9v2LPR4YgbypLo2RX>_ts zC=jaHgWE)>CJo|u)p>vhEOcp2nKV^}!T|+VNhyVZp07*qoM6N<$g4>Jy AtpET3 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/img_collect_course.png b/app/src/main/res/drawable/img_collect_course.png new file mode 100644 index 0000000000000000000000000000000000000000..f847dcbd370e4c42650e3c740bb4d72c0a63a18b GIT binary patch literal 11789 zcmV+oF7nZdP)D&3o(K z>Z*nhsxvb(UK#Pl_x}-@HDJa!UwzCT27y zH(YITEYd!OZ9#;UO0;eB=)fL&34zf;3`v$ctrHjdW+4-3LvzSz|!FND7|B4&Cw{mV}WMrna#K@;eu zMjRVZB=NUtAromq66mE)92?TKm1x64G;IrtK%tLwHFoJ`qyW>l(0rr?L!h-MjbDCH zI>(*C#;rdc*hC8e2b@4KR{^6N7to^L;((a7iFKq?4zwB_FapU1z`F2rI9*6BqPK5` z4twnmI{Y;|X~n7+sb|RyEe7mvGl0ewuA$yM)OV}VY0A#ys=qd2Shn#-7jhQ z%wH^e()jSq<16Xz&2OQf2{N^SJwnYl-Fff8H8hWyR|1`O^7z%Qm@E)y?MLoAXo+Lv z*Y_Vqw|(OjsZPy8%+#*AHN{P;#50ln$0!i}J>F#D{Su0+#moE6yEpq!(21sLZ;ZQQHZm$wMt*?T7E9!(Y3T zUWOZIed6wx2Un*^)K&t`g*<4O(>~IYOuuB%BW9)NeC7w#Dmp=;wh;&pvvAm2Uc`d> zyo|=clE%?X@o6r^lVXEGW!>j@IIq^L$+4q+u)_!Q^h?@& z^KT;pjcoodf=MfBFaXqdOmq!Vv$`zdMEA{l23T zao4ZAg?u>k>|^Qti`UR$uWtVKgbQyv@$tD3NdEdZ?7QP>4w#b(!@VtP;f}fqkf$Bf z_6P3WPXFO8-=yoWye}e;3!gf73w`^aej&;8@7}mdbm%e}|BAo4TZI2I5f|k7Pw)D6 zL>@Cf^7;PFZnKLxZ+-jf=np{L^=IB9^K1v&H0XxHK5Q8+6G+}wpTn~}8m~L9J?e9e z=v>@MT)M2=sU*N=(TSQ2U%c+U^uK?64*l@wAE!@VqE>l9qGz9u$@d*WG>`>({+}Oz zG>Q*3k?}9P+ewH|Ui>y1`Thr^Kcoea*XCQ=`bg!SBH% z8Po=>eocGUefouuaox!2^nZWxacPcR_)e%W>jVMTogF_i@yScpNOh5MzWIwNd~>KU z=U;MS!tc5C^QTBs?Gxn5LkOhRO7zHY+qcfOx=m|lV%%Ncs#Dz(f#86(>)GW)Uil)m z3+N;`xh|ly|LwOs#|- z_Rd{3GzM0+t?ib>AHq|j)FhfF5FUPS=kv?+b=yLd2XM{Iw8A{QU3JPn!!)_#zy4UI zSdSR_`T(=|Z@*FTjyZPDkFZ8)bK4E!5o2l+P0hl>=Xy3$8_~1C9um{U*=Nkqvg4-} zz^$sh&~p5jbD0f%o9DA4-|Un8>s`B z+&@2!svKYCC~x^*dgk<_JAW^?+Bt?#q*ZIPwcZ1t=B`j--u2$2QhB{wgkfnw|7AJ% zb3rT5Oy|S}d1qz02-J#ml^4%G-aefRQB zm1Nu=fnsTu0uiBR+9Vzbpxve>@=XjhLSj%?m-y=z`$a}Q-32NbVO?FZQ3{mh8O_Qw z^PzhAyLIaP%Wu>29CMz`qvQdyHgkppA^ zVfy+6NyIS-q*1Hk*&s2qo&{03JPraCUdLZu9kT*E>V%}%Ks#qWe7)n>b>R;|$97gkbrri-TbE~AOPOQ}#lf=)cSAc<0`6h&t< z`B$A4+q&3R6$xrpM*9Wt>yjLt>Q%K5XgYC#;%LS6-$FZBViqWZa!Z_-N% zAVs~}MKhC2Xj=T$1yQCZ`vqYtG(EADruX;L)c&P{1O?HFOvGi9471e&&CZDJOhJ&P zAV|SzW(r#s4ia(vp=&wGv~o)pF(@XC2^WbdLxsEwB+5NEm3xPP>FBTSKT5tZ;b4Fy zgdm6_2Dbe)GqFSxX5z&|go#k9qPq!7Q4fR}sd7U-K$yB9iTD$wsSARDOwznyA8Jxp z@q*CYaP*h%Gaeoru)EV%9&&8efnSB33KY(~o}_sMROqwUe|y2*+Ax7m$KU=vhe)M> zddy7p%P^2dhE4UWMrG(+E;|uE2K)^i2LYFz4Z?r`JWwjZx!x)T34($$OM*1`Q{=(6 zD7R*qj)%0^rzk&?`#`3dBDWfmk)~8AANlp0*_eIKsX(1v_wfNLbjNqs&{=;u|949? zF{US$NTsL=rGRSG!?0-?RtZ%Dovw%hLew=m!mw&}R>nbi)Je?jE@t!;Q@aLn6yXFZ zcp+3477`@ff^fz*A{~zvC>A2-R2iatYNYq^R|{s>lVdU2sX%K_9)I#cy)g{GNPXP%}*?y9PX6qlWPW_S9#~M~-Y}dmL93Ly%{^NO>b2Ai#wE zwJ>FeG6 z31ghjZLS&H3rt(TvGGH6-eq@+naDC>5)={YHQ`@~nTaMxAdc0QX}?076XA@8s!1c4H*(`o0qyBiBzYS9Y|T-`lb2^B1^V7-Q@+{MM32B$(n zGf?2nED2C1E){ z)yLViY6~2^Gdne&URw@uNCfylJS0#ZVNjvS7t%8{Wu~;#%{fbVHcvXiHtgYGlgLN7 zQrFCSv59_xoFI_Pe4*pbdnbwz$@?jKiCm2cwHz*&#syKPr&ah1ySP*htYgTsWQTKu z2uu>B5N>fSC?i#fivV6kV~P0UxPWwArH73&6-aGaqNQAvaZ$-fuPn{UlKf-_dd5Ww z1xn1MK%j)Fiq%J%t3qV9B+*RrM585bzKqaJwaFq{bQx4QgA4*i=QnQT%XdFLJr33A zT>|w`s-lr~??jPBozJX*9hJoAJl{0@US6q~Wg+}Ja)a2CevicH-0eFBzP?+L;@%)BEXE93HW%(}cV%~+&B zlh49|sClkg_iJ zC4oev6?mjz0gKfv8hk8SOO|Q1+GWPVPh&fKTT=xD?D(LPK(n<_X7!wm;ouN}q2 zY+OTIX>87C`9Wr!Vr6!iLT5T6(C!z4GklU~w1G3+Jm<5ma-UnBDcd{@h)ARo1Yvwh zp3q#$ElgG+Hd~rFa$iSAYV7M9iAE^D9~GIVw+CJ_iFon#8+ zR-QEp1hNFN0>#Q>nz6WKEpqM9n7l}|hoMH%aoG8sVl$SU%Bwhm6`mxICL&Rzt8*z! zqE6%%3hs$30^tW;80Gbev!@2DLGekuuq<-q8=r0@W=n?e$H-6c`Jh(CQAA^fwAbwjddqu z_SMLQ<>{|h{2-}7)u24YC(h)8>X31gl@UB_7$ z#jJ=pltfNwS7{u8A6iH$ple)7V+p?Np)g*d~`vBqBp`Od`c0 zoG6!7FiBvB+l0x=TZReFU_VqK_d9;<%*^mCMRFDFyU3{W(GD|~;JHC0$uNopD%vJ5 zo!QeoWAy7=9MNphXgTM#@#v&ht}G9(8#uZJfKN;$nej?XQHN}4O?tXCuA7)ct2 zH&UNOc_C-m`gMGHu~Hp3Ba$a0vd(cwn6Glaw9IvanL#`mDP)^zp6oczJX0rHg;l|v zNs`2jv@&_#B1&RCi92^wXk;zyxH`W^g5b&zj&!NiT^quhgRh;rBViWu#xdC(<;`t1 zibrL1B+pr^yGG>W@vMQw>`;z3xhuqDg^ARL8+wB* z0#hyUPa!VCE{~}7(Z&5qn^BIuu={AuJJof)`UnyPO2{`&hAmngf4OD-=7 zGo#3o2fG}(*@G219X=T62PL}wwt7S=e|>_1lFR(qaFNHC&WvZDcQM~JLh~q`U7oL_ z>~hJI-goQ^fBhSn?8xm;k@Oc*Ce{Kv9Et#CfNHET)B*}}>0i~Hb++4a^`KInT|2o@ z7cR?3N8@->9Pi#q@(+1_^5Rk^(agNxvo`)sd~cshd*!B@Gj0^d3r9pI%vEuhqWDIM zoi2|WWaZbW4q2+?GKpEX+=lO(>oUTaZ$J`>jcb6Bs(7AV;w_BYD)KPsE#;NSA4s z4WfZ4S#AO|{smC}?wx6;m>cJ{|8wJ4>-3@ZML9q&|5Ayrzp5_D=@EkEC07MZm>q~M zo+_+X1NAePS`fu6l?zlV6{uWFzH+FMX|ajtx6)no6kM6<;aQeIo3KTLXF~Yzs55 zk~kV{%w{}^Sf_pas#L26>bYeP)LS8nw4}cxpKA1}LY5!oB2tz~Yh-pXJslDqddokgQl#63uIX!%IE%TO%SUjRTS!kKePoD z>cI!NuGe76%uw%}RhS8hGdokKJb*GyPS)gKPj89(1(Axyti`vukz94L>%~3k%=mU* zz2OW6-(e zB#7BD`0F(~1_}ccgpmUv9BE+&D2!ub!}$eC7?A3!&(*}WE44yLVEf<$0sZb_WkPm` zjos6iuIFxJw4=i%k$SWJAWbxvGMiIvUGAd~b9wxg+%qFpWUhcBjlVD>l!yznR`&WUHeCLI%iNv) zQ6aCx?pHlN@=zejpyeB*9%&H<|7FF>I9GJw>))XAx#4SyJiuLogs&Fn0cD*2fud9j z*_ok2xzs=tR^`iN=fDqAogB}go_A#1a#KBJ?rb2W? znLbEygD^$gejXFY=c}67y)zjjr9h_gw1Ye*_U)UMzw+qqiGN9C64$I$*2UP3r#lNu z1BF0mOiyBD#+|H3rZzXGBI$hKL#VjvX*nEA5J<;S<;8W>rgNyBKwD-Z@c7=aEgHr0ejhiR6eg<3~&VN2p_M!>24oB2O8n@1Ygt&~QdGVT76Y zj&%9E!%l_o^mgHd=2;KYYeA+e_$CP^0kluoV~`bfe((Wz>o)Qop|n}`g+hk}8Y`KZ zHbb7ieny}9Y%jg$s3L8?b%wUwn(xI>SVNi$XJ|fTIrcWRnyj!c_Vlxq&tgMQSpm}l z)&$Ta4!()YteExFDx*8jtmAIH$o>ssC#azSVB6=s-!_f*gQDy_!3?A#Xjm=qMCQrE1Uc14@@Ko zdzzlbPp!b}HIk%LEOXL~q9!NtReL8F2kI*mEPc7=<3O0ifp+u~t7gacS))AiXM znT|Lgod$jr{`8dEK5xS3``Bu$>uj48?VGI`hO=Mt&iGJ|+iz8arMEY^xM`Dl*Ut2i zwS87`UwKUUKzMB#ADY;oUww`nD$)M^oc`g9lafeF`WZC?s1Oz=@pe(7w9jIP5&20q zY55{p7(@u&DgPYn9RD zGV5&;FzdR^PSNC$E7cR}xF7p3MemFg0C8|FyLb3kz5zV%lSO2o+PnNFVMl@&k#u%8 zpuG5>H%?2FH6ciZzh)H(!1_>^gFuaDU-pG3{1aLeuZ#YCw?BQjV2v{73Vd> zYK;Up6YttumkVg8u2oG7`A?J`k{5ZppyTFw?7~#B>#wZI1yB3Bp?)hK4pnW3x(0hW zz-})eT!WU#Ke2J;%i#lwfH63?P=#gy;>hhjPOtcr<_T1-ajyyUfm!h*6^dp&=`?yB zM&EkSYN6AZ4lp|4W^udYV`6z9E3n6(5c_GkPo$^58nOQs*A0(F;LbLDvb>L7r2L~@ z={UGnIIeeXrBs{v6x^pW_PIQ5PmZ{Z^QTHdfsNIgB#(*bp9|OQnvY$8JoXB*xg6l~|Lk_K+k+hO?elrn$yG+Q&)Hb1 zC>A8EjeZxKf#?4iNT(VOwN|A(&`;~heQRr0G;1~%by}z{m@=QbHmQc_u%6!7&(W*V ziCK~b#J%xrNrJmb?Yid$vxY}5pd@-VJCDNJgU2>2c0SyZwo&`#aSwcXn|VZGMM0oy zwLB(PfT&gM4@m<@x)}n&Y}yUwtYi+rH&|^vGV%178uh<>I3C@rCsj%CLjdi+m<}*e zdgNy*qB#SUH()1}^J^=LuS0A?xCr9&{e1anh1sypohx0IDXdMGww3S$`H!ACjj4P~ zUt!+yoRW4p(omzkz#V#BK_d4k@R=|B*!B%vouvI0J;jau7s~;5jaai0&3a)qTALu# z8Ei(jg5uBPj9F;mBy?S&lz9m>k2pXv(sj!vFn-@<}8az z&V)a0W`$oBi-qBOy?JN)iN}=rTe5^Tl@3oZs(Qo~)5op`aD|(2`2l7){2KMli|TkR zFv4NpmGRYXUtWzGIgcV_<>Q)J@r-js`G{qw_oy$cXd=wig1Hp88b9YZuUjUB)6NZJw-#nQA@ei0Uy@=?Wi813Z zQE@BWY;d0w>Ov#`!j+a;`*vp&`N`wD4j&P-)YNzEOUpcIiQM(K+tF1Hj?0CLG+~lJ zaE2;f#WC?m^8q|Bok!w*$G#Dh6_!U~vhr)x5zLb=E6hH}a>R9&%ROm*=E{|ix$h&R z5pSFRa*y$WOxn`lU#3pOeyGgHbn~2RXkb!?X9io+X} z*{E)|da#Um=Dv$`jbJNIRv9V*-=cRT0@Y`WquAnC6yuhNSBXtulE|-o_1Vg(-jA39KZr3J#f!WT zIiy>dn)Z?i@c4i z`P#(=9|@y81V_w|tynAxq%`3Z(JK$_j;7jk#j<4;scgC;F2~Py-pnl-$9YEGjhbhWHT z_-fAzLBzg3<(%7%;~V`hUx*xWU7cw*p52kYtzCAOAK){8K0^BP$m^T9u8U>Fv+Lqg zLm%=eR8UNQy*U-{FFrJTx2VBzB2k@3zfd)Q5AOqr19OG79(z1Fc`K`{L+%UAIws2Bk<1g{rIJ{JST=xrX^bje`i5{E zV|IqC&vn-V`B^J`*{Bw-uVP<)vOD>xEBDkX%lz2s^QQB`m&PWoZN7jMfiN2@R)Xm6 zyxoWuF+;;&-iq1wIIk*X+5Ef&8NKRoCbxL6#OyRhpSuEKibXj(<1uB%MSsAE>W&AeSBSQ7_7VNy>M;#qEL3HWMb zA^fVJ59<830-t$81 zibIZiC&CP(m`@bTx575l&0 zkFGN7N{zh3f4Q!|u}x%^=$LQ^Q_R4q7rwI1_@=D#8u3G_kT9deS)Gt&ntWkd=d(X)k|+!BG1HiFt_7uq9K94b#r;N!1(68yv>3=HFxFZB3o8I zE@#}%!zQIk)-Itwhpc3@Z*QD|ZghF*=mfoQo=;y5PhKleaUs=lDK?RvEsIQxt8U5q zm}S`J(Ijb_$YEI{S6ri>I?O_@yQD=HTqHJo?2b~GL|zk-Zc=hCXt~b$Od&b)Bu844 zGiJ4nyOc4O`jt?&s|rX+)t5$lIp>2QaHKBQv`?zOKD}3VSz~!Zfz;4MwAbU zbj9MPJZW+f=<7H4j0x^v>*{=8~^sd2a$9Kie@`Hf>e&+losY#`TGiv>j zOF4{$yy2f2og0eeB9yHjl!;hHU4Ln(JuaL!~KwJ_IR+_mr|4WlBN?|Sn> z<`L%Q&ctjnBKMHC!qoMQed+qjk(3inJuxO4ZG$M}ZnCP7S5?e&JR_Vr;+bJs#s>i^ zl_W8W23hpVD(-$`DSu{~)3m&AB(~G)Qw`nSjFv9zQRLA*LfkX5S)vvZd1QroIB;_i zf>Y{xy-_(iT7?wwY;%ZslHQGYA9>UZHDQ4FQ!7^ z*_xtZPD(~iv}#Q34`_BWNeq)LbA>FE^GsiYd%{vCC_G@~Qw|fZl`g9UnbcGly9U{A zLe_b*tEcwIzI=UiWTXJkWvhT3%U!{mPLW?m)69lBVw50b}&b7JUrQokHmceo~g0;e^0SeZpw6# zk!4G`>@%|$mMiRPeLPolav*Tr@!hm+S&3f6<8!u@IE_F9W9{(f9YG1SLXboi^gdC; z>bXu~Kj6VdSADJf8fpP86A1Xox_#FOGjl;Z%*lbkTi5i;w}k9_5l0S*u{uaw6}KA2 zaP?ia%4z@px_rJ|-sNXXoy%e&v!}Z#Ou5w+S*l?bW?MPCiVd>3_R6nTE~FMQCjtSd zoiq+pHbjdMZ(Y+POm0!VvS0 zMqAtHd;?3jwN1@!Fp8R1JZTKS5KSf47T%B48_=$ym-c9Nc_(~lL( zz&$bgZqI+)uGiMRHjc^=RvBn*S8di>ETu{b%Kn>;4) z+%t&u`0uA=UPdvAD(Ns=jfis1&h`|9)0}s;ISXz1{W3_zgT+T8%)(N{(SD7GQ<`hs zh_5?xO8A0LJU%V%?9bgNgd#opcuXR#D2@DPGUC}p3Xjo8Z6OcPwjaQ6y>DQfuml&; zB7r6k9x&(JFEA!tqhBARqn{p|Y3S=T1Ki-%Bw`umGfzFuaAW@>;WTffHleL=P}q9U zz=){n23iE@3E$Rg|JuSeYF(XSyFO0}?UPJD1TN_bjI?u3M7(J0mCq%L1;cJ9pI2s3V@^ zYP<4ibmmP`q)4=o9qS%3h3A&~=zX7WL6r1EfM&=8!mRs17ybId8r^m0tPK0d8~SsR z2s1h?3pl3Z&ia`SbEn7ybc{d%9BZ*obUfj+nhOl23X5KRV4Eb@DE9$3d~-o$2I74F zuP5m5#NX9loRr^N!%vz|KdUQO7Wy^JS?fTCIU5uAw~lY8!|Xyw2?QAHL>;}5W}$r` zmtWIp9YvdniHUk{-v?hc0%?ABPyA+kyc&7OAM=0h2)>Yf^p^Z2=cr=h(l~C|T|f%}J-r2K7V@Z!J%y%le`b#BJuqFX#9uQnX`h&=(eg^!Y0AQ| zYsKw}mhWvfi;gRhiLE~!xR7zWXd(DTcnrlg19QPP75aE~jx(=6-_R|$_urHMGwXrD zHEVTx4;OdU=W=7T70fGvfIIFTxMm^vMR*KW6UgT<+oU1uIB5MVS#;i~dg#>mRpb}1 z;ZWy=zCDsyixMUa{!XipWXuKgP9Oj#P0Z4c6uX<~00Aq}u9qidk)|EMyM{0uJL7|0 z(jWTfbyL(TLK0ouSDqh#$Ax+EfqW1qZR2}~$43st-TZQi62HXo`4p~uN@kp90B0Fr zxyGllpgeOS58+$41-9X;uOwf3Io~*-1Oh?_`$*wnZxq|hX`TQl8edzq7Y|+cn|$c% z5duC=xq$VbKmVBWd9MEZNxExmezRqw(p}ht-{^h$tIa>Z+zGTYkU9pTMuV(Q8-=5N zE_E1x_WmVeYOQ_`<|2ygK`yewFpVD*_rUT1W((hzm7hxA$K|vhz!pyQ&vH4^Y;#cJ z7lngvL0~}~<%!7qY9E}A{APo?y&S-tROxqzVigd!;dLjZfa+I^q?Qg=2 z<;7wkj>e)OkVfPq5xsLRv$j>Q6y{{8TbeLiaZKcOrt!Sap7FV<5L0)}uMJ8xvXFe7 zMp#q?GO_mLaj8hHn76|YFVWF&=%yXdWt*@@=kzgrpeKGm-O|c5)FxnbZC8;-H;)c1 zy64IlErB$kN@5*pxEV4Xb##Ree|3d+?%+Qt`NkPwAoaiMP(K|`X$m&5;AK2W3=cB^={3N@OEk>kAHPEQ7V#>S?% z)oc7$xJOkf(&mGSFg6Zq0=WQ{8alCis6fTR{d@T!XY2?OJcx5FcFYGo)AunPb4Gg@ v=VKx*woeH|i12NFmGaoeYuG_c78m|Mu>SR#8_@-D00000NkvXXu0mjfp`ypc literal 0 HcmV?d00001 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4ed1bb5..a622fa7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -191,6 +191,7 @@ %1$s\๋‹˜์˜ ํฌ์ธํŠธ %1$d P + ํฌ์ธํŠธ ๋ชจ์œผ๋Ÿฌ ๊ฐ€๊ธฐ ํฌ์ธํŠธ ๋‚ด์—ญ ๋ณด๊ธฐ @@ -283,6 +284,12 @@ ๋ชจ์ธ ํฌ์ธํŠธ๋Š” ๋ฐ์ดํŠธ ์žฅ์†Œ๋ฅผ\n์˜ˆ์•ฝํ•  ๋•Œ ํ˜„๊ธˆ์ฒ˜๋Ÿผ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•ด์š” ์ถ”ํ›„ ๋งŒ๋“ค์–ด์งˆ ๊ธฐ๋Šฅ์„ ๊ธฐ๋Œ€ํ•ด์ฃผ์„ธ์š” + + ๊ด‘๊ณ  ์‹œ์ฒญํ•˜๊ธฐ + ์งง์€ ๊ด‘๊ณ  ๋ณด๊ณ  ๋ฐ”๋กœ ํฌ์ธํŠธ ํš๋“ํ•˜๊ธฐ + ์ฝ”์Šค ๋“ฑ๋กํ•˜๊ธฐ + ๋‚˜๋งŒ์˜ ์ฝ”์Šค ๋“ฑ๋กํ•˜๊ณ  ํฌ์ธํŠธ ํš๋“ํ•˜๊ธฐ + ๋‚ด ํ”„๋กœํ•„ ํ”„๋กœํ•„ ๋ณ€๊ฒฝ @@ -313,4 +320,4 @@ ๊ธ€ ์‚ญ์ œ ๋ฐ์ดํŠธ ์ฝ”์Šค ์˜ฌ๋ฆฌ๊ณ  100P ๋ฐ›๊ธฐ - \ No newline at end of file + From 3e38d70162c5abdebaef99295c6c6b24b49f945f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=84=EC=9A=B0?= <85734140+jinuemong@users.noreply.github.com> Date: Thu, 27 Mar 2025 18:14:34 +0900 Subject: [PATCH 10/49] =?UTF-8?q?[feat]=20=EC=8D=B8=EB=84=A4=EC=9D=BC=20UI?= =?UTF-8?q?=20&=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84=20(#16)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat : ์ธ๋„ค์ผ ์ง€์ • ๋กœ์ง ๊ตฌํ˜„ * feat : ์ธ๋„ค์ผ ์ง€์ • UI ๊ตฌํ˜„ * feat : ๋…ผ๋ฆฌ ์˜ค๋ฅ˜ ์ˆ˜์ •, ๊ธฐ์กด ์ธ๋„ค์ผ ์‚ญ์ œ ์‹œ ์˜† ์ธ๋„ค์ผ๋กœ ์ง€์ • * style: ์ฝ”๋“œ ํฌ๋งท ์ˆ˜์ • & ๋ฆฐํŠธ ์ฒดํฌ --- .../presentation/ui/enroll/EnrollContract.kt | 4 +- .../presentation/ui/enroll/EnrollScreen.kt | 26 +++++++--- .../presentation/ui/enroll/EnrollViewModel.kt | 14 ++++-- .../component/EnrollPhotoPreviewCard.kt | 50 ++++++++++++++++--- .../ui/enroll/component/EnrollPhotos.kt | 19 ++++--- app/src/main/res/values/strings.xml | 1 + 6 files changed, 89 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollContract.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollContract.kt index e1a6261..d727e68 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollContract.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollContract.kt @@ -34,6 +34,7 @@ class EnrollContract { val fetchEnrollState: LoadState = LoadState.Idle, val enrollType: EnrollType = EnrollType.COURSE, val page: EnrollScreenType = EnrollScreenType.FIRST, + val thumbnailIndex: Int = 0, val enroll: Enroll = Enroll(), val isEnrollButtonEnabled: Boolean = false, val titleValidateState: TextFieldValidateResult = TextFieldValidateResult.Basic, @@ -81,7 +82,8 @@ class EnrollContract { data class FetchTimelineDetail(val fetchEnrollState: LoadState, val timelineDetail: TimelineDetail?) : EnrollEvent() data class SetEnrollButtonEnabled(val isEnrollButtonEnabled: Boolean) : EnrollEvent() data class SetImage(val images: List) : EnrollEvent() - data class OnImageDeleteButtonClick(val index: Int) : EnrollEvent() + data class OnSelectThumbnail(val index: Int) : EnrollEvent() + data class OnImageDeleteButtonClick(val index: Int, val moveThumbnail: Boolean) : EnrollEvent() data class OnTitleValueChange(val title: String) : EnrollEvent() data class OnDatePickerBottomSheetButtonClick(val date: String) : EnrollEvent() data class OnTimePickerBottomSheetButtonClick(val startAt: String) : EnrollEvent() diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt index fa56b1f..5f79966 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt @@ -267,7 +267,14 @@ fun EnrollRoute( ) } }, - onImageDeleteButtonClick = { index -> viewModel.setEvent(EnrollContract.EnrollEvent.OnImageDeleteButtonClick(index = index)) }, + onImageDeleteButtonClick = { index -> + viewModel.setEvent( + EnrollContract.EnrollEvent.OnImageDeleteButtonClick( + index = index, + moveThumbnail = index <= uiState.thumbnailIndex + ) + ) + }, onTitleValueChange = { title -> viewModel.setEvent(EnrollContract.EnrollEvent.OnTitleValueChange(title = title)) }, onDatePickerBottomSheetButtonClick = { date -> viewModel.setEvent(EnrollContract.EnrollEvent.OnDatePickerBottomSheetButtonClick(date = date)) }, onTimePickerBottomSheetButtonClick = { startAt -> viewModel.setEvent(EnrollContract.EnrollEvent.OnTimePickerBottomSheetButtonClick(startAt = startAt)) }, @@ -283,9 +290,8 @@ fun EnrollRoute( onPlaceCardDeleteButtonClick = { index -> viewModel.setEvent(EnrollContract.EnrollEvent.OnPlaceCardDeleteButtonClick(index = index)) }, onDescriptionValueChange = { description -> viewModel.setEvent(EnrollContract.EnrollEvent.OnDescriptionValueChange(description = description)) }, onCostValueChange = { cost -> viewModel.setEvent(EnrollContract.EnrollEvent.OnCostValueChange(cost = cost)) }, - onEnrollSuccessDialogButtonClick = { - viewModel.setSideEffect(EnrollContract.EnrollSideEffect.PopBackStack) - } + onEnrollSuccessDialogButtonClick = { viewModel.setSideEffect(EnrollContract.EnrollSideEffect.PopBackStack) }, + onSelectThumbnail = { viewModel.setEvent(EnrollContract.EnrollEvent.OnSelectThumbnail(index = it)) } ) when (uiState.loadState) { @@ -353,7 +359,8 @@ fun EnrollScreen( onPlaceCardDeleteButtonClick: (Int) -> Unit, onDescriptionValueChange: (String) -> Unit, onCostValueChange: (String) -> Unit, - onEnrollSuccessDialogButtonClick: () -> Unit + onEnrollSuccessDialogButtonClick: () -> Unit, + onSelectThumbnail: (Int) -> Unit ) { val focusManager = LocalFocusManager.current @@ -378,10 +385,12 @@ fun EnrollScreen( ) Spacer(modifier = Modifier.height(8.dp)) EnrollPhotos( - isDeletable = enrollUiState.page == EnrollScreenType.FIRST, + isEditable = enrollUiState.page == EnrollScreenType.FIRST, images = enrollUiState.enroll.images, + thumbnailIndex = enrollUiState.thumbnailIndex, onPhotoButtonClick = onPhotoButtonClick, - onDeleteButtonClick = onImageDeleteButtonClick + onDeleteButtonClick = onImageDeleteButtonClick, + onSelectThumbnail = onSelectThumbnail ) } @@ -581,7 +590,8 @@ fun EnrollScreenPreview() { onPlaceCardDragAndDrop = {}, onDescriptionValueChange = {}, onCostValueChange = {}, - onEnrollSuccessDialogButtonClick = {} + onEnrollSuccessDialogButtonClick = {}, + onSelectThumbnail = {} ) } } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollViewModel.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollViewModel.kt index 5b8489e..e06298f 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollViewModel.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollViewModel.kt @@ -83,8 +83,15 @@ class EnrollViewModel @Inject constructor( is EnrollContract.EnrollEvent.FetchCourseDetail -> setState { copy(fetchEnrollState = event.fetchEnrollState, enroll = event.courseDetail?.toEnroll() ?: currentState.enroll) } is EnrollContract.EnrollEvent.FetchTimelineDetail -> setState { copy(fetchEnrollState = event.fetchEnrollState, enroll = event.timelineDetail?.toEnroll() ?: currentState.enroll) } is EnrollContract.EnrollEvent.SetEnrollButtonEnabled -> setState { copy(isEnrollButtonEnabled = event.isEnrollButtonEnabled) } - is EnrollContract.EnrollEvent.SetImage -> setState { copy(enroll = currentState.enroll.copy(images = event.images)) } - is EnrollContract.EnrollEvent.OnImageDeleteButtonClick -> setState { copy(enroll = currentState.enroll.copy(images = currentState.enroll.images.toMutableList().apply { removeAt(event.index) })) } + is EnrollContract.EnrollEvent.SetImage -> setState { copy(enroll = currentState.enroll.copy(images = event.images), thumbnailIndex = 0) } + is EnrollContract.EnrollEvent.OnImageDeleteButtonClick -> setState { + copy( + enroll = currentState.enroll.copy( + images = currentState.enroll.images.toMutableList().apply { removeAt(event.index) } + ), + thumbnailIndex = if (event.moveThumbnail) (thumbnailIndex - 1).coerceAtLeast(0) else thumbnailIndex + ) + } is EnrollContract.EnrollEvent.OnTitleValueChange -> setState { copy(enroll = currentState.enroll.copy(title = event.title)) } is EnrollContract.EnrollEvent.OnDatePickerBottomSheetButtonClick -> setState { copy(enroll = currentState.enroll.copy(date = event.date), isDatePickerBottomSheetOpen = false) } @@ -115,7 +122,8 @@ class EnrollViewModel @Inject constructor( is EnrollContract.EnrollEvent.OnPlaceCardDeleteButtonClick -> setState { copy(enroll = currentState.enroll.copy(places = currentState.enroll.places.toMutableList().apply { removeAt(event.index) })) } is EnrollContract.EnrollEvent.OnDescriptionValueChange -> setState { copy(enroll = currentState.enroll.copy(description = event.description)) } is EnrollContract.EnrollEvent.OnCostValueChange -> setState { copy(enroll = currentState.enroll.copy(cost = event.cost)) } - is EnrollContract.EnrollEvent.Enroll -> setState { copy(loadState = event.loadState) } + is EnrollContract.EnrollEvent.Enroll -> setState { copy(loadState = event.loadState, thumbnailIndex = 0) } + is EnrollContract.EnrollEvent.OnSelectThumbnail -> setState { copy(thumbnailIndex = event.index) } is EnrollContract.EnrollEvent.SetTitleValidationState -> setState { copy(titleValidateState = event.titleValidationState) } is EnrollContract.EnrollEvent.SetDateValidationState -> setState { copy(dateValidateState = event.dateValidationState) } is EnrollContract.EnrollEvent.SetIsEnrollSuccessDialogOpen -> setState { copy(isEnrollSuccessDialogOpen = event.isEnrollSuccessDialogOpen) } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/component/EnrollPhotoPreviewCard.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/component/EnrollPhotoPreviewCard.kt index f2151a2..fe05616 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/component/EnrollPhotoPreviewCard.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/component/EnrollPhotoPreviewCard.kt @@ -3,20 +3,25 @@ package org.sopt.teamdateroad.presentation.ui.enroll.component import android.content.Context import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil.compose.AsyncImage @@ -31,14 +36,18 @@ fun EnrollPhotoPreviewCard( modifier: Modifier = Modifier, context: Context = LocalContext.current, id: Int, - isDeletable: Boolean, + isEditable: Boolean, + isThumbnail: Boolean, image: String, - onDeleteButtonClick: (Int) -> Unit = {} + onDeleteButtonClick: (Int) -> Unit = {}, + onSelectThumbnail: (Int) -> Unit = {} ) { + val borderColor = if (isThumbnail && isEditable) DateRoadTheme.colors.purple600 else Color.Transparent Box( modifier = modifier .width(130.dp) .aspectRatio(1f) + .clip(RoundedCornerShape(14.dp)) ) { AsyncImage( model = ImageRequest.Builder(context = context) @@ -52,8 +61,34 @@ fun EnrollPhotoPreviewCard( .fillMaxWidth() .aspectRatio(1f) .clip(RoundedCornerShape(14.dp)) + .border( + width = 2.dp, + color = borderColor, + shape = RoundedCornerShape(14.dp) + ) + .noRippleClickable { + if (isEditable) { + onSelectThumbnail(id) + } + } + ) - if (isDeletable) { + if (isThumbnail) { + Box( + modifier = modifier + .width(56.dp) + .height(26.dp) + .background(DateRoadTheme.colors.purple600) + ) { + Text( + text = stringResource(R.string.enroll_thumbnail_text), + color = DateRoadTheme.colors.white, + style = DateRoadTheme.typography.bodySemi13, + modifier = Modifier.align(Alignment.Center) + ) + } + } + if (isEditable) { Image( modifier = Modifier .align(Alignment.TopEnd) @@ -61,7 +96,9 @@ fun EnrollPhotoPreviewCard( .clip(CircleShape) .background(DateRoadTheme.colors.gray200) .padding(5.dp) - .noRippleClickable(onClick = { onDeleteButtonClick(id) }), + .noRippleClickable { + onDeleteButtonClick(id) + }, painter = painterResource(id = R.drawable.ic_all_close), contentDescription = null ) @@ -75,8 +112,9 @@ fun EnrollPhotoPreviewCardPreview() { DATEROADTheme { EnrollPhotoPreviewCard( id = 0, - isDeletable = true, - image = "" + isEditable = true, + image = "", + isThumbnail = true ) } } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/component/EnrollPhotos.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/component/EnrollPhotos.kt index 2faba1b..3c9ad09 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/component/EnrollPhotos.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/component/EnrollPhotos.kt @@ -27,10 +27,12 @@ import org.sopt.teamdateroad.ui.theme.DateRoadTheme @Composable fun EnrollPhotos( modifier: Modifier = Modifier, - isDeletable: Boolean, + isEditable: Boolean, images: List, + thumbnailIndex: Int, onPhotoButtonClick: () -> Unit = {}, - onDeleteButtonClick: (Int) -> Unit = {} + onDeleteButtonClick: (Int) -> Unit = {}, + onSelectThumbnail: (Int) -> Unit = {} ) { Box( modifier = modifier @@ -51,13 +53,15 @@ fun EnrollPhotos( items(images.size) { index -> EnrollPhotoPreviewCard( id = index, - isDeletable = isDeletable, + isEditable = isEditable, + isThumbnail = thumbnailIndex == index, image = images[index], - onDeleteButtonClick = { onDeleteButtonClick(index) } + onDeleteButtonClick = onDeleteButtonClick, + onSelectThumbnail = onSelectThumbnail ) } } - if (images.isNotEmpty() && isDeletable) { + if (images.isNotEmpty() && isEditable) { Image( modifier = Modifier .align(Alignment.BottomStart) @@ -85,8 +89,9 @@ fun EnrollPhotos( fun EnrollPhotosPreview() { DATEROADTheme { EnrollPhotos( - isDeletable = false, - images = listOf() + isEditable = false, + images = listOf(), + thumbnailIndex = 0 ) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a622fa7..dc363f2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -251,6 +251,7 @@ ์ด ๋น„์šฉ์„ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š” ๋ฐ์ดํŠธ ์˜ˆ์ƒ ์ด ๋น„์šฉ์„ ์ˆซ์ž๋กœ๋งŒ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š” ์™„๋ฃŒ + ๋Œ€ํ‘œ %s๋‹˜, ์˜ค๋Š˜์€\n์ด๋Ÿฐ ๋ฐ์ดํŠธ ์ฝ”์Šค ์–ด๋– ์„ธ์š”? From 9cbfc1f6369a438f55b78f9a3f668f8d325e8176 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=84=EC=9A=B0?= <85734140+jinuemong@users.noreply.github.com> Date: Sat, 29 Mar 2025 12:03:49 +0900 Subject: [PATCH 11/49] =?UTF-8?q?[feat]=20=EC=BD=94=EC=8A=A4=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20&=20=EB=94=94=EC=9E=90=EC=9D=B8=20?= =?UTF-8?q?=EC=9A=94=EA=B5=AC=EC=82=AC=ED=95=AD=20=EC=A0=81=EC=9A=A9=20=20?= =?UTF-8?q?(#18)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: ํฌ์ธํŠธ ๋ถ€์กฑ ์‹œ ์ฝ”์Šค ๋“ฑ๋ก ์—ฐ๊ฒฐ * feat: ์ฝ”์Šค ์ƒ์„ธ์—์„œ ํฌ์ธํŠธ ๋ถ€์กฑ ์‹œ ์ฝ”์Šค ๋“ฑ๋ก ์—ฐ๊ฒฐ * feat: ์ค‘๋ณต ๋กœ์ง ์ œ๊ฑฐ * refactor : ํฌ์ธํŠธ ์ˆ˜์ง‘ ui ๋ณ€๊ฒฝ์‚ฌํ•ญ ์ ์šฉ * style: ์ฝ”๋“œ ํฌ๋งท ์ˆ˜์ • & ๋ฆฐํŠธ ์ฒดํฌ --- .../teamdateroad/domain/util/Constraints.kt | 5 ++ .../bottomsheet/DateRoadPointBottomSheet.kt | 14 ++++-- .../ui/coursedetail/CourseDetailContract.kt | 2 +- .../ui/coursedetail/CourseDetailScreen.kt | 48 +++++++++++-------- .../ui/coursedetail/CourseDetailViewModel.kt | 4 +- .../ui/navigator/component/MainNavHost.kt | 3 +- .../ui/pointhistory/PointHistoryContract.kt | 2 + .../ui/pointhistory/PointHistoryScreen.kt | 46 +++++++++++++----- .../navigation/PointHistoryNavigation.kt | 10 +++- .../presentation/util/Constraints.kt | 1 + app/src/main/res/values/strings.xml | 5 +- 11 files changed, 99 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/org/sopt/teamdateroad/domain/util/Constraints.kt b/app/src/main/java/org/sopt/teamdateroad/domain/util/Constraints.kt index c6a91c2..906b3ef 100644 --- a/app/src/main/java/org/sopt/teamdateroad/domain/util/Constraints.kt +++ b/app/src/main/java/org/sopt/teamdateroad/domain/util/Constraints.kt @@ -75,3 +75,8 @@ object Seoul { const val NOWON_DOBONG_GANGBUK = "๋…ธ์›/๋„๋ด‰/๊ฐ•๋ถ" const val GWANAK_DONGJAK_GEUMCHEON = "๊ด€์•…/๋™์ž‘/๊ธˆ์ฒœ" } + +object PointCollect { + const val ADS = 50 + const val COURSE = 100 +} diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadPointBottomSheet.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadPointBottomSheet.kt index f4a0407..f463f91 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadPointBottomSheet.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadPointBottomSheet.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.sopt.teamdateroad.R +import org.sopt.teamdateroad.domain.util.PointCollect import org.sopt.teamdateroad.presentation.ui.component.bottomsheet.model.collect.DateRoadCollectPointType import org.sopt.teamdateroad.presentation.util.modifier.noRippleClickable import org.sopt.teamdateroad.ui.theme.DateRoadTheme @@ -30,6 +31,7 @@ import org.sopt.teamdateroad.ui.theme.DateRoadTheme @Composable fun DateRoadPointBottomSheet( isBottomSheetOpen: Boolean, + title: String, onClick: (DateRoadCollectPointType) -> Unit, onDismissRequest: () -> Unit = {} ) { @@ -47,7 +49,7 @@ fun DateRoadPointBottomSheet( ) ) { Text( - text = stringResource(R.string.point_box_get_point_button_text), + text = title, color = DateRoadTheme.colors.black, style = DateRoadTheme.typography.bodyBold17, modifier = Modifier.align(Alignment.CenterStart) @@ -80,6 +82,11 @@ fun DateRoadBottonSheetContent( dateLoadCollectPoint: DateRoadCollectPointType, onClick: (DateRoadCollectPointType) -> Unit ) { + val pointAmount = when (dateLoadCollectPoint) { + DateRoadCollectPointType.WATCH_ADS -> PointCollect.ADS + DateRoadCollectPointType.COURSE_REGISTRATION -> PointCollect.COURSE + } + Row( modifier = Modifier .fillMaxWidth() @@ -109,7 +116,7 @@ fun DateRoadBottonSheetContent( ) Spacer(modifier = Modifier.height(4.dp)) Text( - text = stringResource(dateLoadCollectPoint.contentRes), + text = stringResource(dateLoadCollectPoint.contentRes, pointAmount), color = DateRoadTheme.colors.gray400, style = DateRoadTheme.typography.bodySemi13 @@ -129,6 +136,7 @@ fun DateRoadBottonSheetContent( fun DateRoadPointBottomSheetPreView() { DateRoadPointBottomSheet( isBottomSheetOpen = true, - onClick = {} + onClick = {}, + title = "" ) } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailContract.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailContract.kt index 0b0c807..529de20 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailContract.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailContract.kt @@ -13,8 +13,8 @@ class CourseDetailContract { val isDeleteCourseBottomSheetOpen: Boolean = false, val isRegionBottomSheetOpen: Boolean = false, val isReportCourseBottomSheetOpen: Boolean = false, + val isPointCollectBottomSheetOpen: Boolean = false, val isPointReadDialogOpen: Boolean = false, - val isPointLackDialogOpen: Boolean = false, val isFreeReadDialogOpen: Boolean = false, val isDeleteCourseDialogOpen: Boolean = false, val isReportCourseDialogOpen: Boolean = false, diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailScreen.kt index a8c22b9..9a97bb8 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailScreen.kt @@ -34,6 +34,8 @@ import org.sopt.teamdateroad.presentation.type.DateTagType.Companion.getDateTagT import org.sopt.teamdateroad.presentation.type.EnrollType import org.sopt.teamdateroad.presentation.type.TwoButtonDialogWithDescriptionType import org.sopt.teamdateroad.presentation.ui.component.bottomsheet.DateRoadBasicBottomSheet +import org.sopt.teamdateroad.presentation.ui.component.bottomsheet.DateRoadPointBottomSheet +import org.sopt.teamdateroad.presentation.ui.component.bottomsheet.model.collect.DateRoadCollectPointType import org.sopt.teamdateroad.presentation.ui.component.dialog.DateRoadTwoButtonDialogWithDescription import org.sopt.teamdateroad.presentation.ui.component.pager.DateRoadImagePager import org.sopt.teamdateroad.presentation.ui.component.topbar.DateRoadScrollResponsiveTopBar @@ -106,11 +108,6 @@ fun CourseDetailRoute( CourseDetailScreen( courseDetailUiState = uiState, onDialogPointLack = { viewModel.setEvent(CourseDetailContract.CourseDetailEvent.OnDialogPointLack) }, - onDialogPointLackConfirm = { - viewModel.setEvent(CourseDetailContract.CourseDetailEvent.DismissDialogPointLack) - viewModel.setSideEffect(CourseDetailContract.CourseDetailSideEffect.NavigateToEnroll(enrollType = EnrollType.COURSE, viewPath = COURSE_DETAIL, id = null)) - }, - dismissDialogPointLack = { viewModel.setEvent(CourseDetailContract.CourseDetailEvent.DismissDialogPointLack) }, onDialogLookedForFree = { viewModel.setEvent(CourseDetailContract.CourseDetailEvent.OnDialogLookedForFree) }, dismissDialogLookedForFree = { viewModel.setEvent(CourseDetailContract.CourseDetailEvent.DismissDialogLookedForFree) }, onDialogLookedByPoint = { viewModel.setEvent(CourseDetailContract.CourseDetailEvent.OnDialogLookedByPoint) }, @@ -147,7 +144,13 @@ fun CourseDetailRoute( onReportButtonClicked = { viewModel.setEvent(CourseDetailContract.CourseDetailEvent.OnReportWebViewClicked) }, - onReportWebViewClose = { viewModel.setEvent(CourseDetailContract.CourseDetailEvent.DismissReportWebView) } + onReportWebViewClose = { viewModel.setEvent(CourseDetailContract.CourseDetailEvent.DismissReportWebView) }, + onSelectEnroll = { + viewModel.setSideEffect(CourseDetailContract.CourseDetailSideEffect.NavigateToEnroll(enrollType = EnrollType.COURSE, viewPath = COURSE_DETAIL, id = null)) + }, + onDismissCollectPoint = { + viewModel.setEvent(CourseDetailContract.CourseDetailEvent.DismissDialogPointLack) + } ) } @@ -178,8 +181,8 @@ fun CourseDetailRoute( fun CourseDetailScreen( courseDetailUiState: CourseDetailContract.CourseDetailUiState, onDialogPointLack: () -> Unit, - onDialogPointLackConfirm: () -> Unit, - dismissDialogPointLack: () -> Unit, + onSelectEnroll: () -> Unit, + onDismissCollectPoint: () -> Unit, onDialogLookedForFree: () -> Unit, dismissDialogLookedForFree: () -> Unit, onDialogLookedByPoint: () -> Unit, @@ -311,15 +314,6 @@ fun CourseDetailScreen( ) } - if (courseDetailUiState.isPointLackDialogOpen) { - DateRoadTwoButtonDialogWithDescription( - twoButtonDialogWithDescriptionType = TwoButtonDialogWithDescriptionType.POINT_LACK, - onDismissRequest = dismissDialogPointLack, - onClickConfirm = onDialogPointLackConfirm, - onClickDismiss = dismissDialogPointLack - ) - } - if (courseDetailUiState.isFreeReadDialogOpen) { DateRoadTwoButtonDialogWithDescription( twoButtonDialogWithDescriptionType = TwoButtonDialogWithDescriptionType.FREE_READ, @@ -356,6 +350,22 @@ fun CourseDetailScreen( ) } + DateRoadPointBottomSheet( + isBottomSheetOpen = courseDetailUiState.isPointCollectBottomSheetOpen, + title = stringResource(R.string.point_box_lack_point_button_text), + onClick = { dateRoadCollectPointType -> + when (dateRoadCollectPointType) { + // TODO : add ADS + DateRoadCollectPointType.WATCH_ADS -> Unit + DateRoadCollectPointType.COURSE_REGISTRATION -> onSelectEnroll() + } + onDismissCollectPoint() + }, + onDismissRequest = { + onDismissCollectPoint() + } + ) + DateRoadBasicBottomSheet( isBottomSheetOpen = courseDetailUiState.isDeleteCourseBottomSheetOpen, title = stringResource(id = R.string.course_detail_bottom_sheet_title), @@ -431,8 +441,6 @@ fun CourseDetailScreenPreview() { CourseDetailScreen( courseDetailUiState = dummyCourseDetail, onDialogPointLack = {}, - dismissDialogPointLack = {}, - onDialogPointLackConfirm = {}, onDialogLookedForFree = {}, dismissDialogLookedForFree = {}, onDialogLookedByPoint = {}, @@ -451,6 +459,8 @@ fun CourseDetailScreenPreview() { onDialogDeleteCourse = {}, onDialogReportCourse = {}, dismissDialogDeleteCourse = {}, + onDismissCollectPoint = {}, + onSelectEnroll = {}, dismissDialogReportCourse = {} ) } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailViewModel.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailViewModel.kt index d922ec4..3bd782e 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailViewModel.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailViewModel.kt @@ -32,8 +32,8 @@ class CourseDetailViewModel @Inject constructor( override suspend fun handleEvent(event: CourseDetailContract.CourseDetailEvent) { when (event) { - is CourseDetailContract.CourseDetailEvent.OnDialogPointLack -> setState { copy(isPointLackDialogOpen = true) } - is CourseDetailContract.CourseDetailEvent.DismissDialogPointLack -> setState { copy(isPointLackDialogOpen = false) } + is CourseDetailContract.CourseDetailEvent.OnDialogPointLack -> setState { copy(isPointCollectBottomSheetOpen = true) } + is CourseDetailContract.CourseDetailEvent.DismissDialogPointLack -> setState { copy(isPointCollectBottomSheetOpen = false) } is CourseDetailContract.CourseDetailEvent.OnDialogLookedForFree -> setState { copy(isFreeReadDialogOpen = true) } is CourseDetailContract.CourseDetailEvent.DismissDialogLookedForFree -> setState { copy(isFreeReadDialogOpen = false) } is CourseDetailContract.CourseDetailEvent.OnDialogDeleteCourse -> setState { copy(isDeleteCourseDialogOpen = true) } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/navigator/component/MainNavHost.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/navigator/component/MainNavHost.kt index 233d147..5d1b9a4 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/navigator/component/MainNavHost.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/navigator/component/MainNavHost.kt @@ -109,7 +109,8 @@ fun MainNavHost( pointHistoryGraph( padding = padding, - popBackStack = navigator::popBackStackIfNotHome + popBackStack = navigator::popBackStackIfNotHome, + navigateToEnroll = navigator::navigateToEnroll ) profileNavGraph( diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryContract.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryContract.kt index edccc6a..79dc391 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryContract.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryContract.kt @@ -2,6 +2,7 @@ package org.sopt.teamdateroad.presentation.ui.pointhistory import org.sopt.teamdateroad.domain.model.PointHistory import org.sopt.teamdateroad.domain.model.UserPoint +import org.sopt.teamdateroad.presentation.type.EnrollType import org.sopt.teamdateroad.presentation.type.PointHistoryTabType import org.sopt.teamdateroad.presentation.util.base.UiEvent import org.sopt.teamdateroad.presentation.util.base.UiSideEffect @@ -19,6 +20,7 @@ class PointHistoryContract { sealed interface PointHistorySideEffect : UiSideEffect { data object PopBackStack : PointHistorySideEffect + data class NavigateToEnroll(val enrollType: EnrollType, val viewPath: String, val id: Int?) : PointHistorySideEffect } sealed class PointHistoryEvent : UiEvent { diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryScreen.kt index 778bb45..140953a 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryScreen.kt @@ -26,8 +26,10 @@ import org.sopt.teamdateroad.domain.model.Point import org.sopt.teamdateroad.domain.model.PointHistory import org.sopt.teamdateroad.domain.model.UserPoint import org.sopt.teamdateroad.presentation.type.EmptyViewType +import org.sopt.teamdateroad.presentation.type.EnrollType import org.sopt.teamdateroad.presentation.type.PointHistoryTabType import org.sopt.teamdateroad.presentation.ui.component.bottomsheet.DateRoadPointBottomSheet +import org.sopt.teamdateroad.presentation.ui.component.bottomsheet.model.collect.DateRoadCollectPointType import org.sopt.teamdateroad.presentation.ui.component.tabbar.DateRoadTabBar import org.sopt.teamdateroad.presentation.ui.component.tabbar.DateRoadTabTitle import org.sopt.teamdateroad.presentation.ui.component.topbar.DateRoadBasicTopBar @@ -37,6 +39,7 @@ import org.sopt.teamdateroad.presentation.ui.component.view.DateRoadIdleView import org.sopt.teamdateroad.presentation.ui.component.view.DateRoadLoadingView import org.sopt.teamdateroad.presentation.ui.pointhistory.component.PointHistoryCard import org.sopt.teamdateroad.presentation.ui.pointhistory.component.PointHistoryPointBox +import org.sopt.teamdateroad.presentation.util.ViewPath.POINT_HISTORY import org.sopt.teamdateroad.presentation.util.view.LoadState import org.sopt.teamdateroad.ui.theme.DATEROADTheme import org.sopt.teamdateroad.ui.theme.DateRoadTheme @@ -45,7 +48,8 @@ import org.sopt.teamdateroad.ui.theme.DateRoadTheme fun PointHistoryRoute( padding: PaddingValues, viewModel: PointHistoryViewModel = hiltViewModel(), - popBackStack: () -> Unit + popBackStack: () -> Unit, + navigateToEnroll: (EnrollType, String, Int?) -> Unit ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val lifecycleOwner = LocalLifecycleOwner.current @@ -60,6 +64,11 @@ fun PointHistoryRoute( .collect { pointHistorySideEffect -> when (pointHistorySideEffect) { is PointHistoryContract.PointHistorySideEffect.PopBackStack -> popBackStack() + is PointHistoryContract.PointHistorySideEffect.NavigateToEnroll -> navigateToEnroll( + pointHistorySideEffect.enrollType, + pointHistorySideEffect.viewPath, + pointHistorySideEffect.id + ) } } } @@ -80,7 +89,16 @@ fun PointHistoryRoute( }, onTopBarIconClicked = { viewModel.setSideEffect(PointHistoryContract.PointHistorySideEffect.PopBackStack) }, onClickCollectPoint = { viewModel.setEvent(PointHistoryContract.PointHistoryEvent.OnPointCollectBottomSheetClick) }, - onDisMissCollectPoint = { viewModel.setEvent(PointHistoryContract.PointHistoryEvent.OnPointCollectBottomSheetDismiss) } + onDisMissCollectPoint = { viewModel.setEvent(PointHistoryContract.PointHistoryEvent.OnPointCollectBottomSheetDismiss) }, + onSelectEnroll = { + viewModel.setSideEffect( + PointHistoryContract.PointHistorySideEffect.NavigateToEnroll( + enrollType = EnrollType.COURSE, + viewPath = POINT_HISTORY, + id = null + ) + ) + } ) } @@ -95,7 +113,8 @@ fun PointHistoryScreen( onTabBarClicked: (PointHistoryTabType) -> Unit, onTopBarIconClicked: () -> Unit, onClickCollectPoint: () -> Unit, - onDisMissCollectPoint: () -> Unit + onDisMissCollectPoint: () -> Unit, + onSelectEnroll: () -> Unit ) { Column( modifier = Modifier @@ -152,19 +171,23 @@ fun PointHistoryScreen( } items(pointHistory.size) { index -> PointHistoryCard(point = pointHistory[index]) - if (index < pointHistory.size - 1) { - HorizontalDivider( - color = DateRoadTheme.colors.gray100, - thickness = 1.dp - ) - } + HorizontalDivider( + color = DateRoadTheme.colors.gray100, + thickness = 1.dp + ) } } } DateRoadPointBottomSheet( isBottomSheetOpen = pointHistoryUiState.isPointCollectBottomSheetOpen, - onClick = { + title = stringResource(R.string.point_box_get_point_button_text), + onClick = { dateRoadCollectPointType -> + when (dateRoadCollectPointType) { + // TODO : add ADS + DateRoadCollectPointType.WATCH_ADS -> Unit + DateRoadCollectPointType.COURSE_REGISTRATION -> onSelectEnroll() + } onDisMissCollectPoint() }, onDismissRequest = { @@ -194,7 +217,8 @@ fun PointHistoryPreview() { onTabBarClicked = {}, onTopBarIconClicked = {}, onClickCollectPoint = {}, - onDisMissCollectPoint = {} + onDisMissCollectPoint = {}, + onSelectEnroll = {} ) } } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/navigation/PointHistoryNavigation.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/navigation/PointHistoryNavigation.kt index 3ab626b..dd28bad 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/navigation/PointHistoryNavigation.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/navigation/PointHistoryNavigation.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable +import org.sopt.teamdateroad.presentation.type.EnrollType import org.sopt.teamdateroad.presentation.ui.pointhistory.PointHistoryRoute fun NavController.navigationPointHistory() { @@ -14,10 +15,15 @@ fun NavController.navigationPointHistory() { fun NavGraphBuilder.pointHistoryGraph( padding: PaddingValues, - popBackStack: () -> Unit + popBackStack: () -> Unit, + navigateToEnroll: (EnrollType, String, Int?) -> Unit ) { composable(route = PointHistoryRoute.ROUTE) { - PointHistoryRoute(padding = padding, popBackStack = popBackStack) + PointHistoryRoute( + padding = padding, + popBackStack = popBackStack, + navigateToEnroll = navigateToEnroll + ) } } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/util/Constraints.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/util/Constraints.kt index c6984b8..5fc1fe0 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/util/Constraints.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/util/Constraints.kt @@ -197,6 +197,7 @@ object ViewPath { const val MY_COURSE_READ = "๋‚ด๊ฐ€ ์—ด๋žŒํ•œ ์ฝ”์Šค" const val COURSE_DETAIL = "์ฝ”์Šค ์ƒ์„ธ" const val LOOK = "์ฝ”์Šค ๋‘˜๋Ÿฌ๋ณด๊ธฐ" + const val POINT_HISTORY = "ํฌ์ธํŠธ ๋‚ด์—ญ" } object WebViewUrl { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dc363f2..18e211a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -192,6 +192,7 @@ %1$s\๋‹˜์˜ ํฌ์ธํŠธ %1$d P ํฌ์ธํŠธ ๋ชจ์œผ๋Ÿฌ ๊ฐ€๊ธฐ + ํฌ์ธํŠธ๊ฐ€ ๋ถ€์กฑํ•ด์š”! ํฌ์ธํŠธ ๋‚ด์—ญ ๋ณด๊ธฐ @@ -287,9 +288,9 @@ ๊ด‘๊ณ  ์‹œ์ฒญํ•˜๊ธฐ - ์งง์€ ๊ด‘๊ณ  ๋ณด๊ณ  ๋ฐ”๋กœ ํฌ์ธํŠธ ํš๋“ํ•˜๊ธฐ + ์งง์€ ๊ด‘๊ณ  ๋ณด๊ณ  ๋ฐ”๋กœ %dP ํš๋“ํ•˜๊ธฐ ์ฝ”์Šค ๋“ฑ๋กํ•˜๊ธฐ - ๋‚˜๋งŒ์˜ ์ฝ”์Šค ๋“ฑ๋กํ•˜๊ณ  ํฌ์ธํŠธ ํš๋“ํ•˜๊ธฐ + ๋‚˜๋งŒ์˜ ์ฝ”์Šค ๋“ฑ๋กํ•˜๊ณ  %dP ํš๋“ํ•˜๊ธฐ ๋‚ด ํ”„๋กœํ•„ From f34e4cea5658026e0a2a70acc5bd3c789f65b121 Mon Sep 17 00:00:00 2001 From: Yaechan Park <102402485+dpcks0509@users.noreply.github.com> Date: Sun, 30 Mar 2025 19:09:45 +0900 Subject: [PATCH 12/49] =?UTF-8?q?[feat]=20=EC=9E=A5=EC=86=8C=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EB=B0=94=ED=85=80=EC=8B=9C=ED=8A=B8=20UI=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0=20(#19)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: bottomsheet preview ๋ฐ ์˜คํƒ€ ์ˆ˜์ • * feat: DateRoadPlaceSearchBottomSheet ์ดˆ๊ธฐ UI ๊ตฌํ˜„ * feat: ์žฅ์†Œ๊ฒ€์ƒ‰ ๋ฐ”ํ…€์‹œํŠธ ViewModel ์—ฐ๊ฒฐ * refactor: ์žฅ์†Œ ๊ฒ€์ƒ‰ ํƒ€์ดํ•‘์‹œ ์ž๋™ ๊ฒ€์ƒ‰ ๋˜๊ฒŒ ์ˆ˜์ • * refactor: PlaceSearchBottomSheet ๋…ธ์ถœ ํฌ๊ธฐ ์ˆ˜์ • * feat: ์žฅ์†Œ๊ฒ€์ƒ‰ BottomSheet ํ‚ค๋ณด๋“œ ์•ˆ์˜ฌ๋ผ์˜ค๊ฒŒ ๊ตฌํ˜„ * feat: ์žฅ์†Œ ํ‚ค์›Œ๋“œ ์ผ์น˜ ํ•˜์ด๋ผ์ดํŠธ ๊ตฌํ˜„ * refactor: DateRoadPlaceSearchBottomSheet height ์ˆ˜์ • * fix: git ์ถฉ๋Œ ํ•ด๊ฒฐ * refactor: DateRoadPlaceSearchBottomSheet isDraggable false ์ถ”๊ฐ€ * refactor: EnrollPlaceSearchItem ํ„ฐ์น˜ ํฌ๊ธฐ ๋ฒ”์œ„ ์ˆ˜์ • * refactor: BOTTOM_SHEET_OPEN_RATIO ์ƒ์ˆ˜ํ™” * refactor: onValueChange์— onKeywordChanged ์„ค์ • ์‹ฌํ”Œํ•˜๊ฒŒ ์ˆ˜์ • * refactor: EmptyPlaceSearchResult ์ปดํฌ๋„ŒํŠธ ๋ถ„๋ฆฌ * refactor: ktLint ์ ์šฉ --- app/build.gradle.kts | 3 + .../DateRoadPlaceSearchBottomSheet.kt | 249 ++++++++++++++++++ .../bottomsheet/DateRoadPointBottomSheet.kt | 6 +- .../bottomsheet/DefaultDateRoadBottomSheet.kt | 9 + .../presentation/ui/enroll/EnrollContract.kt | 10 + .../presentation/ui/enroll/EnrollScreen.kt | 23 +- .../ui/enroll/EnrollSecondScreen.kt | 34 ++- .../presentation/ui/enroll/EnrollViewModel.kt | 30 ++- .../enroll/component/EnrollPlaceInsertBar.kt | 96 ++++--- .../enroll/component/EnrollPlaceSearchItem.kt | 115 ++++++++ .../ui/navigator/component/MainNavHost.kt | 1 - .../drawable/img_place_search_no_match.png | Bin 0 -> 7952 bytes app/src/main/res/values/strings.xml | 2 + gradle/libs.versions.toml | 2 + 14 files changed, 521 insertions(+), 59 deletions(-) create mode 100644 app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadPlaceSearchBottomSheet.kt create mode 100644 app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/component/EnrollPlaceSearchItem.kt create mode 100644 app/src/main/res/drawable/img_place_search_no_match.png diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2832b3f..98a5785 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -127,6 +127,9 @@ dependencies { // Lottie implementation(libs.lottie.compose) + + // BottomSheet + implementation(libs.bottom.sheet) } ktlint { diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadPlaceSearchBottomSheet.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadPlaceSearchBottomSheet.kt new file mode 100644 index 0000000..5e22de8 --- /dev/null +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadPlaceSearchBottomSheet.kt @@ -0,0 +1,249 @@ +package org.sopt.teamdateroad.presentation.ui.component.bottomsheet + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Button +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.holix.android.bottomsheetdialog.compose.BottomSheetBehaviorProperties +import com.holix.android.bottomsheetdialog.compose.BottomSheetDialog +import com.holix.android.bottomsheetdialog.compose.BottomSheetDialogProperties +import org.sopt.teamdateroad.R +import org.sopt.teamdateroad.domain.model.PlaceInfo +import org.sopt.teamdateroad.domain.model.PlaceSearchResult +import org.sopt.teamdateroad.presentation.ui.enroll.component.EnrollPlaceSearchItem +import org.sopt.teamdateroad.presentation.util.modifier.noRippleClickable +import org.sopt.teamdateroad.ui.theme.DATEROADTheme +import org.sopt.teamdateroad.ui.theme.DateRoadTheme + +@Composable +fun DateRoadPlaceSearchBottomSheet( + isBottomSheetOpen: Boolean, + keyword: String, + placeSearchResult: PlaceSearchResult, + onKeywordChanged: (String) -> Unit, + onPlaceSelected: (PlaceInfo) -> Unit, + onDismissRequest: () -> Unit = {} +) { + if (isBottomSheetOpen) { + BottomSheetDialog( + onDismissRequest = onDismissRequest, + properties = BottomSheetDialogProperties( + behaviorProperties = BottomSheetBehaviorProperties( + state = BottomSheetBehaviorProperties.State.HalfExpanded, + halfExpandedRatio = BOTTOM_SHEET_OPEN_RATIO, + skipCollapsed = true, + isDraggable = false + ) + ) + ) { + Column( + modifier = Modifier + .clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)) + .background(DateRoadTheme.colors.white) + ) { + Spacer(modifier = Modifier.height(23.dp)) + + Row( + modifier = Modifier + .fillMaxWidth() + .height(40.dp) + .padding(start = 25.dp, end = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(R.string.enroll_place_search_bottom_sheet_title), + color = DateRoadTheme.colors.black, + style = DateRoadTheme.typography.bodyBold17 + ) + + Spacer(modifier = Modifier.weight(1f)) + + Image( + modifier = Modifier + .size(40.dp) + .noRippleClickable(onClick = onDismissRequest) + .padding(15.dp), + painter = painterResource(id = R.drawable.ic_bottom_sheet_close), + contentDescription = null + ) + } + + Spacer(modifier = Modifier.height(22.dp)) + + TextField( + value = keyword, + onValueChange = onKeywordChanged, + modifier = Modifier + .fillMaxWidth() + .height(54.dp) + .padding(start = 14.dp, end = 20.dp), + placeholder = { + Text( + modifier = Modifier, + text = stringResource(R.string.enroll_place_insert_bar_enter_place_placeholder), + color = DateRoadTheme.colors.gray300, + style = DateRoadTheme.typography.bodySemi15 + ) + }, + trailingIcon = { + IconButton( + modifier = Modifier.size(20.dp), + onClick = { + onKeywordChanged("") + } + ) { + Icon( + painter = painterResource(id = R.drawable.btn_enroll_delete_picture), + contentDescription = null, + tint = Color.Unspecified + ) + } + }, + colors = TextFieldDefaults.colors( + focusedContainerColor = DateRoadTheme.colors.gray100, + unfocusedContainerColor = DateRoadTheme.colors.gray100, + cursorColor = DateRoadTheme.colors.purple600, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent + ), + shape = RoundedCornerShape(14.dp), + singleLine = true, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Search + ) + ) + + Spacer(modifier = Modifier.height(10.dp)) + + if (placeSearchResult.placeInfos.isNotEmpty()) { + LazyColumn(modifier = Modifier.weight(1f)) { + itemsIndexed( + items = placeSearchResult.placeInfos, + key = { index, placeInfo -> placeInfo.hashCode() + index } + ) { index, placeInfo -> + EnrollPlaceSearchItem( + keyword = keyword, + placeInfo = placeInfo, + onClick = { onPlaceSelected(placeInfo) } + ) + + if (index != placeSearchResult.placeInfos.lastIndex) { + HorizontalDivider( + modifier = Modifier.fillMaxWidth(), + color = DateRoadTheme.colors.gray100, + thickness = 1.dp + ) + } + } + } + } else { + EmptyPlaceSearchResult() + } + } + } + } +} + +@Composable +private fun EmptyPlaceSearchResult() { + Box( + modifier = Modifier + .fillMaxSize() + .padding(bottom = 70.dp) + ) { + Column(modifier = Modifier.align(Alignment.Center)) { + Image( + modifier = Modifier + .width(167.dp) + .height(191.dp), + painter = painterResource(R.drawable.img_place_search_no_match), + contentDescription = null + ) + Spacer(modifier = Modifier.height(40.dp)) + Text( + text = stringResource(R.string.enroll_place_search_no_match), + color = DateRoadTheme.colors.gray300, + style = DateRoadTheme.typography.titleBold18 + ) + } + } +} + +@Preview +@Composable +fun DateRoadPlaceSearchBottomSheetPreview() { + DATEROADTheme { + var isBottomSheetOpen by rememberSaveable { mutableStateOf(false) } + var text by rememberSaveable { mutableStateOf("") } + + Button(onClick = { isBottomSheetOpen = true }) { + Text( + text = "DateRoadPlaceSearchBottomSheet", + color = DateRoadTheme.colors.black, + style = DateRoadTheme.typography.titleBold18 + ) + } + + DateRoadPlaceSearchBottomSheet( + isBottomSheetOpen = isBottomSheetOpen, + keyword = text, + placeSearchResult = PlaceSearchResult( + List(10) { + PlaceInfo( + "์นดํŽ˜ ๋‚˜๋ž‘", + "๊ฒฝ๊ธฐ ์˜์™•์‹œ ์ฒญ๊ณ„๋กœ 217" + ) + } + ), + onKeywordChanged = { text = it }, + onPlaceSelected = {}, + onDismissRequest = { isBottomSheetOpen = !isBottomSheetOpen } + ) + } +} + +@Preview(showBackground = true) +@Composable +fun EmptyPlaceSearchResultPreview() { + DATEROADTheme { + EmptyPlaceSearchResult() + } +} + +private const val BOTTOM_SHEET_OPEN_RATIO = 0.8f diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadPointBottomSheet.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadPointBottomSheet.kt index f463f91..5a24ef5 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadPointBottomSheet.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadPointBottomSheet.kt @@ -64,11 +64,11 @@ fun DateRoadPointBottomSheet( ) } Spacer(modifier = Modifier.height(17.dp)) - DateRoadBottonSheetContent( + DateRoadBottomSheetContent( dateLoadCollectPoint = DateRoadCollectPointType.WATCH_ADS, onClick = onClick ) - DateRoadBottonSheetContent( + DateRoadBottomSheetContent( dateLoadCollectPoint = DateRoadCollectPointType.COURSE_REGISTRATION, onClick = onClick ) @@ -78,7 +78,7 @@ fun DateRoadPointBottomSheet( } @Composable -fun DateRoadBottonSheetContent( +fun DateRoadBottomSheetContent( dateLoadCollectPoint: DateRoadCollectPointType, onClick: (DateRoadCollectPointType) -> Unit ) { diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DefaultDateRoadBottomSheet.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DefaultDateRoadBottomSheet.kt index 8023a8f..1398558 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DefaultDateRoadBottomSheet.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DefaultDateRoadBottomSheet.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.windowInsetsBottomHeight import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.SheetState @@ -68,6 +69,14 @@ fun DefaultDateRoadBottomSheetPreview() { DATEROADTheme { var isBottomSheetOpen by rememberSaveable { mutableStateOf(false) } + Button(onClick = { isBottomSheetOpen = true }) { + Text( + text = "DefaultDateRoadBottomSheet", + color = DateRoadTheme.colors.black, + style = DateRoadTheme.typography.titleExtra20 + ) + } + DefaultDateRoadBottomSheet( modifier = Modifier.padding(top = 20.dp, bottom = 16.dp, start = 16.dp, end = 16.dp), isBottomSheetOpen = isBottomSheetOpen, diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollContract.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollContract.kt index d727e68..3e5b010 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollContract.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollContract.kt @@ -3,6 +3,8 @@ package org.sopt.teamdateroad.presentation.ui.enroll import org.sopt.teamdateroad.domain.model.CourseDetail import org.sopt.teamdateroad.domain.model.Enroll import org.sopt.teamdateroad.domain.model.Place +import org.sopt.teamdateroad.domain.model.PlaceInfo +import org.sopt.teamdateroad.domain.model.PlaceSearchResult import org.sopt.teamdateroad.domain.model.TimelineDetail import org.sopt.teamdateroad.domain.type.RegionType import org.sopt.teamdateroad.presentation.type.EnrollScreenType @@ -54,7 +56,11 @@ class EnrollContract { val isRegionBottomSheetOpen: Boolean = false, val onRegionBottomSheetRegionSelected: RegionType? = RegionType.SEOUL, val onRegionBottomSheetAreaSelected: Any? = null, + val isPlaceSearchBottomSheetOpen: Boolean = false, + val keyword: String = "", val place: Place = Place(), + val placeSearchResult: PlaceSearchResult = PlaceSearchResult(placeInfos = emptyList()), + val placeInfos: List = emptyList(), val isPlaceEditable: Boolean = true, val isDurationBottomSheetOpen: Boolean = false, val durationPicker: List = listOf(Picker(items = (DURATION_START..DURATION_END).map { (it * 0.5).toString() })), @@ -77,6 +83,10 @@ class EnrollContract { data object OnDurationBottomSheetDismissRequest : EnrollEvent() data object OnTimeTextFieldClick : EnrollEvent() data object OnRegionTextFieldClick : EnrollEvent() + data object OnPlaceSearchButtonClick : EnrollEvent() + data class OnKeywordChanged(val keyword: String) : EnrollEvent() + data class OnPlaceSelected(val placeInfo: PlaceInfo) : EnrollEvent() + data object OnPlaceSearchBottomSheetDismiss : EnrollEvent() data class FetchEnrollCourseType(val enrollType: EnrollType) : EnrollEvent() data class FetchCourseDetail(val fetchEnrollState: LoadState, val courseDetail: CourseDetail?) : EnrollEvent() data class FetchTimelineDetail(val fetchEnrollState: LoadState, val timelineDetail: TimelineDetail?) : EnrollEvent() diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt index 5f79966..9bf5968 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt @@ -33,6 +33,7 @@ import java.time.LocalDate import java.time.format.DateTimeFormatter import org.sopt.teamdateroad.R import org.sopt.teamdateroad.domain.model.Place +import org.sopt.teamdateroad.domain.model.PlaceInfo import org.sopt.teamdateroad.domain.type.RegionType import org.sopt.teamdateroad.presentation.type.DateRoadRegionBottomSheetType import org.sopt.teamdateroad.presentation.type.DateTagType @@ -253,6 +254,10 @@ fun EnrollRoute( onDateTextFieldClick = { viewModel.setEvent(EnrollContract.EnrollEvent.OnDateTextFieldClick) }, onTimeTextFieldClick = { viewModel.setEvent(EnrollContract.EnrollEvent.OnTimeTextFieldClick) }, onRegionTextFieldClick = { viewModel.setEvent(EnrollContract.EnrollEvent.OnRegionTextFieldClick) }, + onPlaceSearchButtonClick = { viewModel.setEvent(EnrollContract.EnrollEvent.OnPlaceSearchButtonClick) }, + onKeywordChanged = { keyword -> viewModel.setEvent(EnrollContract.EnrollEvent.OnKeywordChanged(keyword = keyword)) }, + onPlaceSelected = { placeInfo -> viewModel.setEvent(EnrollContract.EnrollEvent.OnPlaceSelected(placeInfo = placeInfo)) }, + onPlaceSearchBottomSheetDismiss = { viewModel.setEvent(EnrollContract.EnrollEvent.OnPlaceSearchBottomSheetDismiss) }, onSelectedPlaceCourseTimeClick = { viewModel.setEvent(EnrollContract.EnrollEvent.OnSelectedPlaceCourseTimeClick) }, onDatePickerBottomSheetDismissRequest = { viewModel.setEvent(EnrollContract.EnrollEvent.OnDatePickerBottomSheetDismissRequest) }, onTimePickerBottomSheetDismissRequest = { viewModel.setEvent(EnrollContract.EnrollEvent.OnTimePickerBottomSheetDismissRequest) }, @@ -284,7 +289,6 @@ fun EnrollRoute( onRegionBottomSheetButtonClick = { region: RegionType?, area: Any? -> viewModel.setEvent(EnrollContract.EnrollEvent.OnRegionBottomSheetButtonClick(region = region, area = area)) }, onAddPlaceButtonClick = { place -> viewModel.setEvent(EnrollContract.EnrollEvent.OnAddPlaceButtonClick(place = place)) }, onPlaceCardDragAndDrop = { places -> viewModel.setEvent(EnrollContract.EnrollEvent.OnPlaceCardDragAndDrop(places = places)) }, - onPlaceTitleValueChange = { placeTitle -> viewModel.setEvent(EnrollContract.EnrollEvent.OnPlaceTitleValueChange(placeTitle = placeTitle)) }, onDurationBottomSheetButtonClick = { placeDuration -> viewModel.setEvent(EnrollContract.EnrollEvent.OnDurationBottomSheetButtonClick(placeDuration = placeDuration)) }, onPlaceEditButtonClick = { editable -> viewModel.setEvent(EnrollContract.EnrollEvent.OnEditableValueChange(editable = editable)) }, onPlaceCardDeleteButtonClick = { index -> viewModel.setEvent(EnrollContract.EnrollEvent.OnPlaceCardDeleteButtonClick(index = index)) }, @@ -337,6 +341,10 @@ fun EnrollScreen( onDateTextFieldClick: () -> Unit, onTimeTextFieldClick: () -> Unit, onRegionTextFieldClick: () -> Unit, + onPlaceSearchButtonClick: () -> Unit, + onKeywordChanged: (String) -> Unit, + onPlaceSelected: (PlaceInfo) -> Unit, + onPlaceSearchBottomSheetDismiss: () -> Unit, onSelectedPlaceCourseTimeClick: () -> Unit, onDatePickerBottomSheetDismissRequest: () -> Unit, onTimePickerBottomSheetDismissRequest: () -> Unit, @@ -353,7 +361,6 @@ fun EnrollScreen( onRegionBottomSheetButtonClick: (RegionType?, Any?) -> Unit, onAddPlaceButtonClick: (Place) -> Unit, onPlaceCardDragAndDrop: (List) -> Unit, - onPlaceTitleValueChange: (String) -> Unit, onDurationBottomSheetButtonClick: (String) -> Unit, onPlaceEditButtonClick: (Boolean) -> Unit, onPlaceCardDeleteButtonClick: (Int) -> Unit, @@ -438,9 +445,12 @@ fun EnrollScreen( EnrollScreenType.SECOND -> EnrollSecondScreen( enrollUiState = enrollUiState, + onPlaceSearchButtonClick = onPlaceSearchButtonClick, + onKeywordChanged = onKeywordChanged, + onPlaceSelected = onPlaceSelected, + onPlaceSearchBottomSheetDismiss = onPlaceSearchBottomSheetDismiss, onSelectedPlaceCourseTimeClick = onSelectedPlaceCourseTimeClick, onAddPlaceButtonClick = onAddPlaceButtonClick, - onPlaceTitleValueChange = onPlaceTitleValueChange, onPlaceEditButtonClick = onPlaceEditButtonClick, onPlaceCardDeleteButtonClick = onPlaceCardDeleteButtonClick, onPlaceCardDragAndDrop = onPlaceCardDragAndDrop @@ -568,6 +578,7 @@ fun EnrollScreenPreview() { onDateTextFieldClick = {}, onTimeTextFieldClick = {}, onRegionTextFieldClick = {}, + onPlaceSearchButtonClick = {}, onSelectedPlaceCourseTimeClick = {}, onDatePickerBottomSheetDismissRequest = {}, onTimePickerBottomSheetDismissRequest = {}, @@ -583,7 +594,6 @@ fun EnrollScreenPreview() { onRegionBottomSheetAreaChipClick = {}, onRegionBottomSheetButtonClick = { _, _ -> }, onAddPlaceButtonClick = {}, - onPlaceTitleValueChange = {}, onDurationBottomSheetButtonClick = {}, onPlaceEditButtonClick = {}, onPlaceCardDeleteButtonClick = {}, @@ -591,7 +601,10 @@ fun EnrollScreenPreview() { onDescriptionValueChange = {}, onCostValueChange = {}, onEnrollSuccessDialogButtonClick = {}, - onSelectThumbnail = {} + onSelectThumbnail = {}, + onPlaceSearchBottomSheetDismiss = {}, + onKeywordChanged = { }, + onPlaceSelected = { } ) } } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollSecondScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollSecondScreen.kt index c5c1e7d..9eebbe9 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollSecondScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollSecondScreen.kt @@ -35,7 +35,9 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.launch import org.sopt.teamdateroad.R import org.sopt.teamdateroad.domain.model.Place +import org.sopt.teamdateroad.domain.model.PlaceInfo import org.sopt.teamdateroad.presentation.type.PlaceCardType +import org.sopt.teamdateroad.presentation.ui.component.bottomsheet.DateRoadPlaceSearchBottomSheet import org.sopt.teamdateroad.presentation.ui.component.button.DateRoadTextButton import org.sopt.teamdateroad.presentation.ui.component.card.DateRoadPlaceCard import org.sopt.teamdateroad.presentation.ui.enroll.component.EnrollPlaceInsertBar @@ -49,9 +51,12 @@ import org.sopt.teamdateroad.ui.theme.DateRoadTheme @Composable fun EnrollSecondScreen( enrollUiState: EnrollContract.EnrollUiState = EnrollContract.EnrollUiState(), + onPlaceSearchButtonClick: () -> Unit, + onKeywordChanged: (String) -> Unit, + onPlaceSelected: (PlaceInfo) -> Unit, + onPlaceSearchBottomSheetDismiss: () -> Unit, onSelectedPlaceCourseTimeClick: () -> Unit, onAddPlaceButtonClick: (Place) -> Unit, - onPlaceTitleValueChange: (String) -> Unit, onPlaceEditButtonClick: (Boolean) -> Unit, onPlaceCardDeleteButtonClick: (Int) -> Unit, onPlaceCardDragAndDrop: (List) -> Unit @@ -84,10 +89,13 @@ fun EnrollSecondScreen( ) Spacer(modifier = Modifier.height(13.dp)) EnrollPlaceInsertBar( - modifier = Modifier.padding(horizontal = 16.dp), - title = enrollUiState.place.title, + modifier = Modifier + .fillMaxWidth() + .height(44.dp) + .padding(horizontal = 16.dp), + placeName = enrollUiState.place.title, duration = enrollUiState.place.duration, - onTitleChange = onPlaceTitleValueChange, + onPlaceSearchButtonClick = onPlaceSearchButtonClick, onSelectedCourseTimeClick = onSelectedPlaceCourseTimeClick, onAddCourseButtonClick = { onAddPlaceButtonClick(Place(title = enrollUiState.place.title, duration = enrollUiState.place.duration + Time.TIME)) @@ -167,19 +175,31 @@ fun EnrollSecondScreen( } Spacer(modifier = Modifier.height(16.dp)) } + + DateRoadPlaceSearchBottomSheet( + isBottomSheetOpen = enrollUiState.isPlaceSearchBottomSheetOpen, + keyword = enrollUiState.keyword, + placeSearchResult = enrollUiState.placeSearchResult, + onKeywordChanged = onKeywordChanged, + onPlaceSelected = onPlaceSelected, + onDismissRequest = onPlaceSearchBottomSheetDismiss + ) } -@Preview +@Preview(showBackground = true) @Composable fun EnrollSecondScreenPreview() { DATEROADTheme { EnrollSecondScreen( + onPlaceSearchButtonClick = {}, onAddPlaceButtonClick = {}, onSelectedPlaceCourseTimeClick = {}, - onPlaceTitleValueChange = {}, onPlaceEditButtonClick = {}, onPlaceCardDeleteButtonClick = {}, - onPlaceCardDragAndDrop = {} + onPlaceSearchBottomSheetDismiss = {}, + onKeywordChanged = {}, + onPlaceCardDragAndDrop = {}, + onPlaceSelected = {} ) } } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollViewModel.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollViewModel.kt index e06298f..7e9b247 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollViewModel.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollViewModel.kt @@ -6,8 +6,10 @@ import javax.inject.Inject import kotlinx.coroutines.launch import org.sopt.teamdateroad.data.dataremote.util.Date.NEAREST_DATE_START_OUTPUT_FORMAT import org.sopt.teamdateroad.data.mapper.toEntity.toEnroll +import org.sopt.teamdateroad.domain.model.PlaceSearchResult import org.sopt.teamdateroad.domain.type.RegionType import org.sopt.teamdateroad.domain.usecase.GetCourseDetailUseCase +import org.sopt.teamdateroad.domain.usecase.GetPlaceSearchResultUseCase import org.sopt.teamdateroad.domain.usecase.GetTimelineDetailUseCase import org.sopt.teamdateroad.domain.usecase.PostCourseUseCase import org.sopt.teamdateroad.domain.usecase.PostTimelineUseCase @@ -25,7 +27,8 @@ class EnrollViewModel @Inject constructor( private val getCourseDetailUseCase: GetCourseDetailUseCase, private val getTimelineDetailUseCase: GetTimelineDetailUseCase, private val postCourseUseCase: PostCourseUseCase, - private val postTimelineUseCase: PostTimelineUseCase + private val postTimelineUseCase: PostTimelineUseCase, + private val getPlaceSearchResultUseCase: GetPlaceSearchResultUseCase ) : BaseViewModel() { override fun createInitialState(): EnrollContract.EnrollUiState = EnrollContract.EnrollUiState() @@ -74,6 +77,15 @@ class EnrollViewModel @Inject constructor( is EnrollContract.EnrollEvent.OnDateTextFieldClick -> setState { copy(isDatePickerBottomSheetOpen = true) } is EnrollContract.EnrollEvent.OnTimeTextFieldClick -> setState { copy(isTimePickerBottomSheetOpen = true) } is EnrollContract.EnrollEvent.OnRegionTextFieldClick -> setState { copy(isRegionBottomSheetOpen = true, onRegionBottomSheetRegionSelected = RegionType.SEOUL, onRegionBottomSheetAreaSelected = null) } + is EnrollContract.EnrollEvent.OnPlaceSearchButtonClick -> setState { copy(isPlaceSearchBottomSheetOpen = true) } + is EnrollContract.EnrollEvent.OnKeywordChanged -> { + setState { + copy(keyword = event.keyword) + } + getPlaceSearchResult() + } + + is EnrollContract.EnrollEvent.OnPlaceSearchBottomSheetDismiss -> setState { copy(isPlaceSearchBottomSheetOpen = false, keyword = "", placeSearchResult = PlaceSearchResult(emptyList())) } is EnrollContract.EnrollEvent.OnSelectedPlaceCourseTimeClick -> setState { copy(isDurationBottomSheetOpen = true) } is EnrollContract.EnrollEvent.OnDatePickerBottomSheetDismissRequest -> setState { copy(isDatePickerBottomSheetOpen = false) } is EnrollContract.EnrollEvent.OnTimePickerBottomSheetDismissRequest -> setState { copy(isTimePickerBottomSheetOpen = false) } @@ -93,6 +105,9 @@ class EnrollViewModel @Inject constructor( ) } is EnrollContract.EnrollEvent.OnTitleValueChange -> setState { copy(enroll = currentState.enroll.copy(title = event.title)) } + is EnrollContract.EnrollEvent.OnPlaceSelected -> { + setState { copy(keyword = "", placeSearchResult = PlaceSearchResult(emptyList()), place = currentState.place.copy(title = event.placeInfo.placeName), placeInfos = currentState.placeInfos + event.placeInfo, isPlaceSearchBottomSheetOpen = false) } + } is EnrollContract.EnrollEvent.OnDatePickerBottomSheetButtonClick -> setState { copy(enroll = currentState.enroll.copy(date = event.date), isDatePickerBottomSheetOpen = false) } @@ -175,4 +190,17 @@ class EnrollViewModel @Inject constructor( } } } + + private fun getPlaceSearchResult() { + viewModelScope.launch { + getPlaceSearchResultUseCase(keyword = currentState.keyword).fold( + onSuccess = { placeSearchResult -> + setState { copy(placeSearchResult = placeSearchResult) } + }, + onFailure = { + setEvent(EnrollContract.EnrollEvent.Enroll(loadState = LoadState.Error)) + } + ) + } + } } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/component/EnrollPlaceInsertBar.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/component/EnrollPlaceInsertBar.kt index 86200b3..921a288 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/component/EnrollPlaceInsertBar.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/component/EnrollPlaceInsertBar.kt @@ -2,10 +2,17 @@ package org.sopt.teamdateroad.presentation.ui.enroll.component import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -15,10 +22,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -26,7 +30,6 @@ import org.sopt.teamdateroad.R import org.sopt.teamdateroad.presentation.ui.component.bottomsheet.DateRoadPickerBottomSheet import org.sopt.teamdateroad.presentation.ui.component.bottomsheet.model.Picker import org.sopt.teamdateroad.presentation.ui.component.button.DateRoadImageButton -import org.sopt.teamdateroad.presentation.ui.component.textfield.DateRoadBasicTextField import org.sopt.teamdateroad.presentation.util.modifier.noRippleClickable import org.sopt.teamdateroad.ui.theme.DATEROADTheme import org.sopt.teamdateroad.ui.theme.DateRoadTheme @@ -34,57 +37,70 @@ import org.sopt.teamdateroad.ui.theme.DateRoadTheme @Composable fun EnrollPlaceInsertBar( modifier: Modifier = Modifier, - title: String = "", + placeName: String = "", duration: String = "", + onPlaceSearchButtonClick: () -> Unit = {}, onSelectedCourseTimeClick: () -> Unit = {}, - onTitleChange: (String) -> Unit = {}, onAddCourseButtonClick: () -> Unit = {} ) { - var textFieldHeight by remember { mutableStateOf(0) } - val density = LocalDensity.current - Row( modifier = modifier, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - DateRoadBasicTextField( + Button( modifier = Modifier - .weight(196f) - .onGloballyPositioned { coordinates -> - textFieldHeight = maxOf(textFieldHeight, coordinates.size.height) - }, - placeholder = stringResource(id = R.string.enroll_place_insert_bar_enter_place_placeholder), - value = title, - onValueChange = onTitleChange - ) - Text( + .weight(1f) + .height(44.dp), + colors = ButtonDefaults.buttonColors( + containerColor = DateRoadTheme.colors.gray100 + ), + shape = RoundedCornerShape(14.dp), + contentPadding = PaddingValues(0.dp), + onClick = onPlaceSearchButtonClick + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(21.dp) + .padding(horizontal = 14.dp) + ) { + Text( + modifier = Modifier.align(Alignment.CenterStart), + text = if (placeName.isEmpty()) stringResource(id = R.string.enroll_place_insert_bar_enter_place_placeholder) else placeName, + color = if (placeName.isEmpty()) DateRoadTheme.colors.gray300 else DateRoadTheme.colors.black, + style = DateRoadTheme.typography.bodySemi13, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + + Box( modifier = Modifier - .weight(72f) + .width(77.dp) + .height(44.dp) .background(color = DateRoadTheme.colors.gray100, shape = RoundedCornerShape(14.dp)) - .padding(vertical = 16.dp) - .onGloballyPositioned { coordinates -> - textFieldHeight = maxOf(textFieldHeight, coordinates.size.height) - } - .padding(horizontal = 15.dp) .noRippleClickable(onClick = onSelectedCourseTimeClick), - text = duration.ifEmpty { stringResource(id = R.string.enroll_place_insert_bar_select_course_time_placeholder) }, - color = if (duration.isEmpty()) DateRoadTheme.colors.gray300 else DateRoadTheme.colors.black, - style = DateRoadTheme.typography.bodySemi13, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - textAlign = TextAlign.Center - ) + contentAlignment = Alignment.Center + ) { + Text( + text = duration.ifEmpty { stringResource(id = R.string.enroll_place_insert_bar_select_course_time_placeholder) }, + color = if (duration.isEmpty()) DateRoadTheme.colors.gray300 else DateRoadTheme.colors.black, + style = DateRoadTheme.typography.bodySemi13, + maxLines = 1 + ) + } DateRoadImageButton( modifier = Modifier - .size(with(density) { textFieldHeight.toDp() }), - isEnabled = duration.isNotEmpty() && title.isNotEmpty(), + .size(44.dp), + isEnabled = duration.isNotEmpty() && placeName.isNotEmpty(), cornerRadius = 14.dp, - paddingHorizontal = 15.dp, - paddingVertical = 15.dp, + paddingHorizontal = 0.dp, + paddingVertical = 0.dp, disabledBackgroundColor = DateRoadTheme.colors.gray100, disabledContentColor = DateRoadTheme.colors.gray300, - onClick = { if (duration.isNotEmpty() && title.isNotEmpty()) onAddCourseButtonClick() else Unit } + onClick = { if (duration.isNotEmpty() && placeName.isNotEmpty()) onAddCourseButtonClick() else Unit } ) } } @@ -93,7 +109,6 @@ fun EnrollPlaceInsertBar( @Composable fun EnrollPlaceInsertBarPreview() { DATEROADTheme { - var title by remember { mutableStateOf("") } var duration by remember { mutableStateOf("") } var isBottomSheetOpen by rememberSaveable { mutableStateOf(false) } val pickerItems by remember { @@ -101,11 +116,8 @@ fun EnrollPlaceInsertBarPreview() { } EnrollPlaceInsertBar( - title = title, + placeName = "", duration = duration, - onTitleChange = { titleValue -> - title = titleValue - }, onSelectedCourseTimeClick = { isBottomSheetOpen = true } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/component/EnrollPlaceSearchItem.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/component/EnrollPlaceSearchItem.kt new file mode 100644 index 0000000..5a44b7c --- /dev/null +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/component/EnrollPlaceSearchItem.kt @@ -0,0 +1,115 @@ +package org.sopt.teamdateroad.presentation.ui.enroll.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.teamdateroad.domain.model.PlaceInfo +import org.sopt.teamdateroad.presentation.util.modifier.noRippleClickable +import org.sopt.teamdateroad.ui.theme.DATEROADTheme +import org.sopt.teamdateroad.ui.theme.DateRoadTheme + +@Composable +fun EnrollPlaceSearchItem( + keyword: String, + placeInfo: PlaceInfo, + onClick: (PlaceInfo) -> Unit +) { + Column( + modifier = Modifier + .fillMaxWidth() + .height(75.dp) + .noRippleClickable { + onClick(placeInfo) + } + .padding(horizontal = 25.dp), + verticalArrangement = Arrangement.Center + ) { + Spacer(modifier = Modifier.height(11.dp)) + Text( + text = getHighlightedPlaceName(keyword, placeInfo.placeName), + color = DateRoadTheme.colors.black, + style = DateRoadTheme.typography.bodySemi15, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.height(6.dp)) + Text( + text = placeInfo.addressName, + color = DateRoadTheme.colors.gray300, + style = DateRoadTheme.typography.bodySemi13, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + Spacer(modifier = Modifier.height(8.dp)) + } +} + +@Composable +fun getHighlightedPlaceName(keyword: String, placeName: String) = buildAnnotatedString { + val keywordIndex = findKeywordIndex(keyword, placeName) + + if (keywordIndex != null) { + append(placeName.substring(0, keywordIndex.first)) + withStyle(style = SpanStyle(color = DateRoadTheme.colors.purple600)) { + append(placeName.substring(keywordIndex.first, keywordIndex.second + 1)) + } + append(placeName.substring(keywordIndex.second + 1, placeName.length)) + } else { + append(placeName) + } +} + +private fun findKeywordIndex(keyword: String, placeName: String): Pair? { + val normalizedKeyword = keyword.replace(" ", "") + val normalizedPlaceName = placeName.replace(" ", "") + + val startIndex = normalizedPlaceName.indexOf(normalizedKeyword) + if (startIndex == -1) return null + + var actualStartIndex = 0 + var nonSpaceCount = 0 + for ((i, ch) in placeName.withIndex()) { + if (ch != ' ') nonSpaceCount++ + if (nonSpaceCount == startIndex + 1) { + actualStartIndex = i + break + } + } + + var actualEndIndex = actualStartIndex + var matchedCount = 0 + for (i in actualStartIndex until placeName.length) { + if (placeName[i] != ' ') matchedCount++ + if (matchedCount == normalizedKeyword.length) { + actualEndIndex = i + break + } + } + + return actualStartIndex to actualEndIndex +} + +@Preview(showBackground = true) +@Composable +fun EnrollPlacerSearchItemPreview() { + DATEROADTheme { + EnrollPlaceSearchItem( + keyword = "์นดํŽ˜", + placeInfo = PlaceInfo("์˜์™• ์นดํŽ˜ ๋‚˜๋ž‘", "๊ฒฝ๊ธฐ ์˜์™•์‹œ ์ฒญ๊ณ„๋กœ 217"), + onClick = {} + ) + } +} diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/navigator/component/MainNavHost.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/navigator/component/MainNavHost.kt index 5d1b9a4..4b897f4 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/navigator/component/MainNavHost.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/navigator/component/MainNavHost.kt @@ -42,7 +42,6 @@ fun MainNavHost( NavHost( navController = navigator.navHostController, startDestination = navigator.startDestination -// startDestination = navigator.startDestination ) { advertisementGraph( popBackStack = navigator::popBackStackIfNotHome diff --git a/app/src/main/res/drawable/img_place_search_no_match.png b/app/src/main/res/drawable/img_place_search_no_match.png new file mode 100644 index 0000000000000000000000000000000000000000..149a3b17ca9a9c807218c36a62de6b3b9780b6f1 GIT binary patch literal 7952 zcmV+rAMfCaP)TCCy0YYMj~=#}>g~AP8W0d%pVv1VQ}RA9sHN`6DW-&m1l>1`I!GR2F=vaPUK@8n|59+3+4_rKp{3u#_YK$QK^6h*qp-{(GyRim8L z6Nx}mD9j$1>ugoma-pUhp#sf@FptB-j@43$ZY?YM!ZhEbY@%@_ zVC}Wn&aYEWst7I6Pe0vU;_mV;KU68>a8%*ug$s+FFuwTm|CqYMp0iXP=tQ zCIvv{$i!{FzeG8S;Msy=Oop);3G~(r1%>A4=PW_>qtR&5S6_WI@H`Oa?Ci94*KZ683wv~)-d#AleNe9l8iTgM^z9%2h)Ic@I2kI?hYvT)BD;1lGc#q~ z!+C)t(+$LlZW+KT_5vcu*cHPAh_jc7Ax>Ao=;FiDY#Ns-Csu|Ev{=-I|0u3g)M%Wv zco65@+^qGuTHWnyJx`soEi6Rd>zHD(K;`nO_Az3Ru24>_cLsBjX_Rd0kIlD3VEg-Z_O$0L$H=v!UeD<|Q!eg!Gm0*~8^cgE%83mRPLlC4i*%Vh z0Pz%yMJomgL3WA~B7}8=FkL68Gqi+b(KWks=*PZfLAJW{ZlSx*mK*cz7ZVl9>@4M# zOJZcGfBxvv=CUkXSxiHD>7{SCogNoAc>}S?UKS1(wp^ZVk4qsic;~nk3KNz%yEBw< zPKXir5Vt!m3SdoJt)_L4i=q>E{HrJmZEx@T7Bf4Hl?xZn<@50|;wgai$tRy}dGY4a zWg~Qi%p=Tw*Xwmp-7bU!k(El5J=5hNG+5StLU-TaJ-!_T#;g7V_|m(ty|%DUIkDl% ziLRy?KKUA%~@8`2t$ zDX^YA7G9^E*zq-q`S|hX0}eSY)7X)ZJ(I%Mx_GuE@4}kU7{SAawOpLZ;X4fkrEb`Y zK8RyHnl$XB-Ff}>^DC5-7``iuur=9W&-fB58PjO9F*BoNwHVsS5IB);lr3p)InzVn z+Dx#diXvScwJ|BioVhsDL+DgwEUGIMj8&djze*Vf7n-h1@4oRy_UhPgZhpCp#B!dJ ze$#$Ns_MG&F;93@78bs%QHBwgE6-j23ePxQ7rkpoT<~z^*6Xh?W@^j{Vep|T&o#3C=H zFT3LH=$edYrYTnq1G~qOHa$(6m#zyd1Xz65u!Dx;Z?mr;;&frha^>vV@7@`F{x2Sg z*n^6#_Ci*?`qT(CZhQFiM zuFcKOuG9FScK#vKIkHhIlp$j6;&*osI54k2bY@t*z}7>$y}MgW`5d_^a_X=QtLgs1 z7w9qxg3q;{wXCMt1iH6(aK$i;2ac8Wg<+WQu{C{2DTRaVLbHT)U0=q%c;{#$Q{6iV z+{I%IsVFtX4pD*mES}@=Mb$Kok9A$C%+F70N5>v;P#ilWhw7`AU`bD%noiw*?yyQ6 z27(n85?|zYRB;dI$P`zwZzGH8cD@A)u@wsDRm0d{;qKS^LEJ_(tp^f&q9~_WtLS>+ z+WdTRgZ~a>IlF-+<y}jC1j3FVEVS+>B%5w7| z3sva;o-FnoJ3ITAVZkXypiCtRLwXa=5s8DXdov>uk>hw4=L!czE}{gK z5Il!3dVHsnAo#a?))^Vd5<|TfSSJX8vO^-VQ=|);=HwQo8gqk(XScGn+HCo;yStoGaRMYg-(`pDc3fjr{=CeDaX6H^6SeQwr(PIf+eiR~g9F9!J8#}Pp zK0SN(rN|uAzzh?pMx%)ul}Kqs+KeY<*4dJZoXrZvkTK{;?@Jk#NCi%LCJS5A)ucr_ zWDGhwjok-`^zPQymaut!XEngc0Fgd@`o(*c5W~Fqk!tKdEGf!l*lHbZbba)OMXZAx zDD_AQFi@b}*yA8jSc%Kq+chCpLUi(STHrRin}l~i#7SyuG&Yzwcs#mHAsG=*z87UX z1hpA!n&Uu$&d<-Ip*0#4cszPr#CUXsheoPlOvdUo4NbS>;O$s+^A7AZ6+?IRVw0EC z1z&vc#W@}wDp1qR-u!Y%u7?dhWq8Wd6R05-S6A=Bf{ury)QO^mtHBvT@^adUt*aj@ zP^{3TF$0QkOQ31daGn7kYWzK|mwi+Z)7W9KLV+lmH@i;lV{-$A5S z)RRCZ$9I(vr%%t!Qvg2u>#VBe& zd9t8c_T)eO@K^Lt|M<^z|Nbur9{1bdKBRyBmw%)8-~Uj+qd}tl_$tRy2d)_b_D-;2qOW#P6nTatK3YZy&UCr$4ufL)9|Kn%$hd=z0 z?%(?*|NV8}_xueEk1f&*3~cpDgP^SGT6txirvDPSv+x|N7UK;GRBx zO24@Oi@xu{F&}*Jfpy%^fA(|hInz0hB*|Bzm|gNL(6lEn>$*v`T7!0X4{2}jkal+J z+-MWWj+vgG_H?dBLvNo4_lJkAKh+dW{_!}E9{r9UKmL?{{Nul; zAN=4I>p8jq$C zMqzg;CDiM6I&3t1zV_&MAM*uXqo4fbCvB1Lv3L&->QpQiX?Av++d-k<{LgRb^Upu0 zzy0A4sa!s8Fei(}%7qIHmq;L<1-iAhwZ%+=ll%LJ?QXpn{Jpt>C#}b=mQG)N z^-cSDy!M^%yg(BZ1xw_OMyuyJ`2E-B=Vxz{4@B+`7nVk|X^CwRlaoa%P1^0Wy}e7D z+UM=pip2@~n^(?RB83Gu%o7_7P=Q~YomJ*VR?_oPA2glw%dnn<#0l_vt7SK+t{*=h zk9ED(K36~Z{cn>`(v-a1>|;5P9n(CzIAJ|IX*lnP(0hp!w76|Wck`YFn%3myVd6YG z&Tdp#iMhEM%VHJ^vh^5Un(Gh^cz>Y8h_sADb;B5X4BE(TgO^_V4!tm6wuU#bo-dzy z$+Ditkn=Q~P2U&tAbgboNAfcN2UN;9b91xS%@3`XX@#5omLEs*GJCkolo2Fn&l`yG z``JyE?KG^7 zKtUO7(_pD}eIjUB?gYLBn#QmR1PXBtVq!w})cBK=%D~rk-SAyZ4SU&B3Yc`9pFzM| z9|j^GgT#&@f^O(PiBE|EQlM|lKQ!Ey&TR->$T_*o%lmZ~EK_bO# z#R+fiG(9~Ovs0y8?Qs|` z^bHpKo%83vx8CbK0k%RM&*WtnMQH?Zl*TWg7HgJJGKa;vLj5mJ^u)_3o{NjPSLKo* zdv%Uw^0G^?qP}CdbUO&DUAg4=LOkCB<#E|q3zTD;XMMh9oU-e~!Xjgge^QA0l4Zu4l)KEw|Y(8sN|DzStl=$ zFkSpu)@wROkd>a}=*f#9panH5G}e>?OjP8aybszJb0C50`ovgU16ej!{O(tud-C34 z19;1Q97v$!TCb^t>#|&X=x2My+>`f=wy552CowBxnv-`?=ZORZ5n>@6)&dQs^SA0c9wJqcj#r!gDAbOMY2=n6zdije39JKb?I8QT1CS@pa?RV znpmY-KODG@DhNosnov~;WnnJ!6^nK8D#e5?NW;_0&0*My z%Y`_Fs#UAI`2H#h2CFY?2L}`-5GazEBetlTBo*EnRMW}RuZ4ZqSs*{9m=cvC#)E^V zm4FO^iZBv52Lz9E!>YzG?kX3H6J>}=q?L2Kb(5kUUtw)gi^sKCgA|2Gd9vr0EI0mv zJ$7x-aRWcuscMJH7_lWNyCrPQ2nV=m@R^xnEY1N0wX?HtU1%bxu{hDa#|7GGW_nuW zc)r5tTw7T9ZpF*NeSaZGoRN&5`T^^C)4SPePoaUT?@?>VL zp1RARx=2ycS+gl>5EO?c4TG6v;F)kiA5z>L6R2HcQG7tz3t_`ZL5b6f#yoK#99U5! z=8NQ5i}94*bxH_PUansZBfif~%amdm;*21Gwm^4xcb6%lwcE)~nI>nwvxpJ1A!!TL zFr>taF(f^hOACwN+`qE;l*188OC`Thi|-e*|pjs3a*WW^$db(8lh@= zwFQcT4D8LWQ353GMF)-kC>}}{+KJWKcaB_S*eV^M5IaLx6v`T z%oo)~mf!fbmG7&$piu};{4(?Nr&h;Jr2eyXwsY|l@#MJg|_g+N0N8<%Q)+$~8mD-@6<$7c{{m^8BU=g+Jl`F6-*mM|sh zaG}?_n=Ik85_9O4zkRb8oYjddc#K`j_+t?Tkfic-IK?%cviIG}}s_k&Ms$ z6b&7AHK(GOAp}-REUhsJes-X9;jmk1@7-mr1nP=2Y}d|X$PgkkGb2!lvoz`dVwPp) zCJ7=H0`)u980<5|h|@;85~K{cL*V_?<6JTlRGdsDOoDPZ!ra(Gp{4Td69oZ96dDbR z4D8ogDhVxyRaTVZjjgSNHMiU%D*jDOG`S5LOSYGe;%X)b>m^Osrz@Dyz)osm79}cj z^lSKW5kQ>Sb2Hfc4WZMx!UW0D<}odGiaLqOii^@ zLp!aUWFR%g36Q|q5mfsaR`J7bWZ4>*MKQ*Vn&;1VLX9P{D6f9# zufj6Mm<1H3ElxyvA!}p{(UVWY-+!Hjy4G)K5c*rPCAf|86$u6zPWDpXqjKM9R7H_8 zE6Psx_G+Q4-Fc}|FG~<5>OxKU&E<>}O_B~TI$K5_BalE5QYjSbY{HEj6k~DQQ39(y zMXmHiHDr{eF?4%p55$#``3_OaDIfB5P&s$*%%$!phkkq1rst`vE6(hRYqnL&2}Ql$ zV1Y(kXFh9i4|%D3Oxo8;vMb%u4Yrb*%w5dM9)_V`=DSMgb3F%g*far~Q^md2%y^L( zEbB&+Dd`v zrJTq?cqqww^I4)TP(?8ktFAn5j6xa4h&T}kZ3~pU_GQY61r!v^>510;geA}<%YTm> z$V3{YVqzjep^NV4EP*Cj{(IcWt>+-56Kk2AEdV+Sk9XI4%wmO$7<**s?fig_7^xy}DCP1lbuV(vXv+;i`O zi?iI7Rv9_``R^_)d~dz)>+S<*8}3a?A+{)CC1=+CTF9{2#d~R4QRrKnbBk3nGlJ}RK_|6y5@aFIqc6hhVMVHKy3B`oug@g84EHz)-p zDaw8vSyyEVW7$Tdwanj9MOTjOBrGRR$YP?J^F6nkvtL0D!xk)N8qK$z8OP8T){8j7 ziH!Q~?Y(uBcA*3~>b_@hrbU#4z{*9_Y~7{!XW`H`a0dATmNrCHlJkOHov(D+5o@3! zA;0h1;^LW`UXJa#@5}-LR^L@RVcB`0^larH^larH^larH^larH^larGZJ((#UISkY+ zEtTq4mQAZ=2gXfPckbP7VXVBz)5mutkyQ7Q^L&p?9kVQGVWHOJIV4?o1?-P@j>g1? z``F!4T9Op^zN#pCwO%h(M|DCZwE{gjI8b$6DH}#pg+NSGQjf%CiG`nQ&t$2TL-yJ- z$=aLA`CEhdk*I8o8?VXLLV>iYsVR*zKq3M?5+63ba;HrtS8x!YN;xrL30ku)xV5pt zGYAQin4FX20fe~jQ3wKVO@?XsO|{W6*rLDk6od+Uy+Wsf_5m|lLE)-xEKbZ zQaotG4shq+yPmZ~)cK4nDEIFpVOf7wk6tS{&u3%){{I1h7+d5^s89|70000์ด ๋น„์šฉ์„ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š” ๋ฐ์ดํŠธ ์˜ˆ์ƒ ์ด ๋น„์šฉ์„ ์ˆซ์ž๋กœ๋งŒ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š” ์™„๋ฃŒ + ์žฅ์†Œ ๊ฒ€์ƒ‰ํ•˜๊ธฐ + ์ผ์น˜ํ•˜๋Š” ์žฅ์†Œ๊ฐ€ ์—†์–ด์š”! ๋Œ€ํ‘œ diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7fbd2c4..1fbd01b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -50,6 +50,7 @@ timber = "5.0.1" kakao = "2.20.3" amplitude = "1.+" lottieCompose = "6.1.0" +bottomSheet = "1.3.2" [libraries] # Test @@ -110,6 +111,7 @@ kakao-navi = { group = "com.kakao.sdk", name = "v2-navi", version.ref = "kakao" kakao-cert = { group = "com.kakao.sdk", name = "v2-cert", version.ref = "kakao" } amplitude = { group = "com.amplitude", name = "analytics-android", version.ref = "amplitude" } lottie-compose = { module = "com.airbnb.android:lottie-compose", version.ref = "lottieCompose" } +bottom-sheet = { group = "com.holix.android", name = "bottomsheetdialog-compose", version.ref = "bottomSheet" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } From b8f51481ccc6f14244605f1f07392d3d20a1153b Mon Sep 17 00:00:00 2001 From: Yaechan Park <102402485+dpcks0509@users.noreply.github.com> Date: Mon, 31 Mar 2025 23:21:31 +0900 Subject: [PATCH 13/49] =?UTF-8?q?[feat]=20=EC=9E=A5=EC=86=8C=EC=97=90=20?= =?UTF-8?q?=EB=8F=84=EB=A1=9C=EB=AA=85=20=EC=A3=BC=EC=86=8C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: DateRoadPlaceCard UI ์ˆ˜์ • ๋ฐ Place ๋„๋ฉ”์ธ์— address ํ•„๋“œ ์ถ”๊ฐ€ * refactor: ๋ฐ์ดํŠธ ์ฝ”์Šค ๋“ฑ๋ก API ํ•„๋“œ ์ˆ˜์ • * refactor: ๋ฐ์ดํŠธ ์ฝ”์Šค ์ƒ์„ธ ์กฐํšŒ API ํ•„๋“œ ์ˆ˜์ • * refactor: DateRoadPlaceCard ๋„๋กœ๋ช… ์ฃผ์†Œ start padding ์ˆ˜์ • * refactor: ์ƒˆ๋กœ์šด API version ๋ณ€๊ฒฝ * refactor: getPlaceSearchResult onSuccess, onFailure ๊ธฐ์กด ์ฝ”๋“œ ํฌ๋งท ํ†ต์ผ * refactor: DateRoadPlaceCard top padding ์ˆ˜์ • * refactor: ktlint ์ฒดํฌ * refactor: address nullabel ์ œ๊ฑฐ --- .../model/request/RequestCourseDto.kt | 4 +- .../model/request/RequestPlaceDto.kt | 2 + .../model/response/ResponsePlaceDto.kt | 2 + .../data/dataremote/service/CourseService.kt | 5 +- .../dataremote/service/TimelineService.kt | 5 +- .../data/dataremote/util/Constraints.kt | 6 +- .../data/mapper/todata/EnrollMapper.kt | 3 +- .../data/mapper/todata/PlaceMapper.kt | 1 + .../mapper/todomain/ResponsePlaceDtoMapper.kt | 1 + .../sopt/teamdateroad/domain/model/Enroll.kt | 3 +- .../sopt/teamdateroad/domain/model/Place.kt | 1 + .../ui/component/card/DateRoadPlaceCard.kt | 118 +++++++++++------- .../ui/coursedetail/CourseDetailScreen.kt | 2 + .../ui/enroll/EnrollSecondScreen.kt | 4 +- .../presentation/ui/enroll/EnrollViewModel.kt | 19 ++- .../ui/timelinedetail/TimelineDetailScreen.kt | 2 + 16 files changed, 116 insertions(+), 62 deletions(-) diff --git a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/model/request/RequestCourseDto.kt b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/model/request/RequestCourseDto.kt index ce85823..057c8af 100644 --- a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/model/request/RequestCourseDto.kt +++ b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/model/request/RequestCourseDto.kt @@ -18,5 +18,7 @@ data class RequestCourseDto( @SerialName("description") val description: String, @SerialName("cost") - val cost: Int + val cost: Int, + @SerialName("thumbnailIndex") + val thumbnailIndex: Int ) diff --git a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/model/request/RequestPlaceDto.kt b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/model/request/RequestPlaceDto.kt index 4c57c7a..9de72e5 100644 --- a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/model/request/RequestPlaceDto.kt +++ b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/model/request/RequestPlaceDto.kt @@ -9,6 +9,8 @@ data class RequestPlaceDto( val sequence: Int, @SerialName("title") val title: String, + @SerialName("address") + val address: String, @SerialName("duration") val duration: Float ) diff --git a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/model/response/ResponsePlaceDto.kt b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/model/response/ResponsePlaceDto.kt index 5a9e437..c029520 100644 --- a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/model/response/ResponsePlaceDto.kt +++ b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/model/response/ResponsePlaceDto.kt @@ -9,6 +9,8 @@ data class ResponsePlaceDto( val sequence: Int, @SerialName("title") val title: String, + @SerialName("address") + val address: String, @SerialName("duration") val duration: Float ) diff --git a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/service/CourseService.kt b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/service/CourseService.kt index f6379e7..02759d6 100644 --- a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/service/CourseService.kt +++ b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/service/CourseService.kt @@ -18,6 +18,7 @@ import org.sopt.teamdateroad.data.dataremote.util.ApiConstraints.SORT import org.sopt.teamdateroad.data.dataremote.util.ApiConstraints.SORT_BY import org.sopt.teamdateroad.data.dataremote.util.ApiConstraints.TAGS import org.sopt.teamdateroad.data.dataremote.util.ApiConstraints.VERSION +import org.sopt.teamdateroad.data.dataremote.util.ApiConstraints.VERSION2 import retrofit2.http.DELETE import retrofit2.http.GET import retrofit2.http.Multipart @@ -37,7 +38,7 @@ interface CourseService { @Path(COURSE_ID) courseId: Int ) - @GET("$API/$VERSION/$COURSES/{$COURSE_ID}") + @GET("$API/$VERSION2/$COURSES/{$COURSE_ID}") suspend fun getCourseDetail( @Path(COURSE_ID) courseId: Int ): ResponseCourseDetailDto @@ -55,7 +56,7 @@ interface CourseService { ): ResponseCoursesDto @Multipart - @POST("$API/$VERSION/$COURSES") + @POST("$API/$VERSION2/$COURSES") suspend fun postCourse( @Part images: List, @Part(COURSE) course: RequestBody, diff --git a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/service/TimelineService.kt b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/service/TimelineService.kt index 904283c..6dfae2b 100644 --- a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/service/TimelineService.kt +++ b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/service/TimelineService.kt @@ -11,6 +11,7 @@ import org.sopt.teamdateroad.data.dataremote.util.ApiConstraints.DATE_ID import org.sopt.teamdateroad.data.dataremote.util.ApiConstraints.NEAREST import org.sopt.teamdateroad.data.dataremote.util.ApiConstraints.TIME import org.sopt.teamdateroad.data.dataremote.util.ApiConstraints.VERSION +import org.sopt.teamdateroad.data.dataremote.util.ApiConstraints.VERSION2 import retrofit2.http.Body import retrofit2.http.DELETE import retrofit2.http.GET @@ -24,7 +25,7 @@ interface TimelineService { @Path(DATE_ID) timelineId: Int ) - @GET("$API/$VERSION/$DATES/{$DATE_ID}") + @GET("$API/$VERSION2/$DATES/{$DATE_ID}") suspend fun getTimelineDetail( @Path(DATE_ID) timelineId: Int ): ResponseTimelineDetailDto @@ -37,7 +38,7 @@ interface TimelineService { @GET("$API/$VERSION/$DATES/$NEAREST") suspend fun getNearestTimeline(): ResponseNearestTimelineDto - @POST("$API/$VERSION/$DATES") + @POST("$API/$VERSION2/$DATES") suspend fun postTimeline( @Body requestTimelineDto: RequestTimelineDto ): ResponseEnrollTimelineDto diff --git a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/util/Constraints.kt b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/util/Constraints.kt index aaedeb7..7aac26b 100644 --- a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/util/Constraints.kt +++ b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/util/Constraints.kt @@ -1,5 +1,7 @@ package org.sopt.teamdateroad.data.dataremote.util +import org.sopt.teamdateroad.data.dataremote.util.ApiConstraints.VERSION2 + object ApiConstraints { const val PROFILE_FORM_DATA_IMAGE = "image" const val COURSE_FORM_DATA_IMAGE = "images" @@ -7,6 +9,7 @@ object ApiConstraints { const val HTTPS = "https://" const val API = "api" const val VERSION = "v1" + const val VERSION2 = "v2" const val COURSES = "courses" const val DATE_ACCESS = "date-access" const val USERS = "users" @@ -77,6 +80,5 @@ object TotalCostZero { } object PlaceSearch { - private const val VERSION = "v2" - const val LOCAL_SEARCH_KEYWORD_JSON = "$VERSION/local/search/keyword.json" + const val LOCAL_SEARCH_KEYWORD_JSON = "$VERSION2/local/search/keyword.json" } diff --git a/app/src/main/java/org/sopt/teamdateroad/data/mapper/todata/EnrollMapper.kt b/app/src/main/java/org/sopt/teamdateroad/data/mapper/todata/EnrollMapper.kt index e5577fe..c8e41c2 100644 --- a/app/src/main/java/org/sopt/teamdateroad/data/mapper/todata/EnrollMapper.kt +++ b/app/src/main/java/org/sopt/teamdateroad/data/mapper/todata/EnrollMapper.kt @@ -22,5 +22,6 @@ fun Enroll.toCourseData(): RequestCourseDto = RequestCourseDto( country = country?.title.orEmpty(), city = this.city.toAreaTitle(), description = this.description, - cost = this.cost.toInt() + cost = this.cost.toInt(), + thumbnailIndex = this.thumbnailIndex ) diff --git a/app/src/main/java/org/sopt/teamdateroad/data/mapper/todata/PlaceMapper.kt b/app/src/main/java/org/sopt/teamdateroad/data/mapper/todata/PlaceMapper.kt index 01b7635..d59a9b1 100644 --- a/app/src/main/java/org/sopt/teamdateroad/data/mapper/todata/PlaceMapper.kt +++ b/app/src/main/java/org/sopt/teamdateroad/data/mapper/todata/PlaceMapper.kt @@ -7,5 +7,6 @@ import org.sopt.teamdateroad.domain.model.Place fun Place.toData(sequence: Int): RequestPlaceDto = RequestPlaceDto( sequence = sequence, title = this.title, + address = this.address, duration = duration.substringBefore(DURATION).toFloat() ) diff --git a/app/src/main/java/org/sopt/teamdateroad/data/mapper/todomain/ResponsePlaceDtoMapper.kt b/app/src/main/java/org/sopt/teamdateroad/data/mapper/todomain/ResponsePlaceDtoMapper.kt index 5f4bac9..61385f1 100644 --- a/app/src/main/java/org/sopt/teamdateroad/data/mapper/todomain/ResponsePlaceDtoMapper.kt +++ b/app/src/main/java/org/sopt/teamdateroad/data/mapper/todomain/ResponsePlaceDtoMapper.kt @@ -6,5 +6,6 @@ import org.sopt.teamdateroad.domain.model.Place fun ResponsePlaceDto.toDomain() = Place( title = this.title, + address = this.address, duration = this.duration.toDuration() ) diff --git a/app/src/main/java/org/sopt/teamdateroad/domain/model/Enroll.kt b/app/src/main/java/org/sopt/teamdateroad/domain/model/Enroll.kt index b42ccb2..2af2ea3 100644 --- a/app/src/main/java/org/sopt/teamdateroad/domain/model/Enroll.kt +++ b/app/src/main/java/org/sopt/teamdateroad/domain/model/Enroll.kt @@ -12,5 +12,6 @@ data class Enroll( val city: Any? = null, val places: List = listOf(), val description: String = "", - val cost: String = "" + val cost: String = "", + val thumbnailIndex: Int = 0 ) diff --git a/app/src/main/java/org/sopt/teamdateroad/domain/model/Place.kt b/app/src/main/java/org/sopt/teamdateroad/domain/model/Place.kt index 60f8b9a..acd6d61 100644 --- a/app/src/main/java/org/sopt/teamdateroad/domain/model/Place.kt +++ b/app/src/main/java/org/sopt/teamdateroad/domain/model/Place.kt @@ -2,5 +2,6 @@ package org.sopt.teamdateroad.domain.model data class Place( val title: String = "", + val address: String = "", val duration: String = "" ) diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/card/DateRoadPlaceCard.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/card/DateRoadPlaceCard.kt index 263fc0d..277edc6 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/card/DateRoadPlaceCard.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/card/DateRoadPlaceCard.kt @@ -2,10 +2,11 @@ package org.sopt.teamdateroad.presentation.ui.component.card import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width @@ -35,55 +36,88 @@ fun DateRoadPlaceCard( place: Place, onIconClick: (() -> Unit)? = null ) { - val paddingValues = Modifier.padding(start = placeCardType.startPadding, end = placeCardType.endPadding) + val paddingValues = Modifier.padding(start = placeCardType.startPadding, end = 17.dp) Row( modifier = modifier - .fillMaxWidth() - .clip(RoundedCornerShape(14.dp)) - .background(DateRoadTheme.colors.gray100) - .then(paddingValues) - .padding(vertical = placeCardType.verticalPadding), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically +// .fillMaxWidth() +// .height(76.dp) ) { - if (placeCardType == PlaceCardType.COURSE_NORMAL) { - sequence?.let { + Column( + modifier = Modifier + .weight(1f) + .clip(RoundedCornerShape(14.dp)) + .background(DateRoadTheme.colors.gray100) + .then(paddingValues) + ) { + Spacer(modifier = Modifier.padding(top = 13.dp)) + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + if (placeCardType == PlaceCardType.COURSE_NORMAL) { + sequence?.let { + DateRoadTextTag( + textContent = (sequence + 1).toString(), + tagContentType = TagType.PLACE_CARD_NUMBER + ) + } + Spacer(modifier = Modifier.width(14.dp)) + } + Text( + text = place.title, + modifier = Modifier.weight(1f), + style = DateRoadTheme.typography.bodyBold15, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.width(10.dp)) + DateRoadTextTag( - textContent = (sequence + 1).toString(), - tagContentType = TagType.PLACE_CARD_NUMBER + textContent = place.duration, + tagContentType = TagType.PLACE_CARD_TIME ) } - Spacer(modifier = Modifier.width(14.dp)) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + modifier = Modifier.padding(start = if (placeCardType == PlaceCardType.COURSE_NORMAL) 38.dp else 0.dp), + text = place.address, + color = DateRoadTheme.colors.gray300, + style = DateRoadTheme.typography.bodyMed13, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + Spacer(modifier = Modifier.height(13.dp)) } - Text( - text = place.title, - modifier = Modifier.weight(1f), - style = DateRoadTheme.typography.bodyBold15, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - Spacer(modifier = Modifier.width(10.dp)) - DateRoadTextTag( - textContent = place.duration, - tagContentType = TagType.PLACE_CARD_TIME - ) placeCardType.iconRes?.let { - Icon( - painter = painterResource(id = it), - contentDescription = null, + Spacer(modifier = Modifier.width(8.dp)) + Box( modifier = Modifier - .padding( - start = placeCardType.iconStartPadding, - top = placeCardType.iconTopPadding, - end = placeCardType.iconEndPadding, - bottom = placeCardType.iconBottomPadding - ) - .noRippleClickable { - onIconClick?.invoke() - } - ) + .width(44.dp) + .fillMaxHeight() + .clip(RoundedCornerShape(14.dp)) + .background(DateRoadTheme.colors.gray100) + ) { + Icon( + painter = painterResource(id = it), + contentDescription = null, + modifier = Modifier + .padding( + start = placeCardType.iconStartPadding, + top = placeCardType.iconTopPadding, + end = placeCardType.iconEndPadding, + bottom = placeCardType.iconBottomPadding + ) + .align(Alignment.Center) + .noRippleClickable { + onIconClick?.invoke() + } + ) + } } } } @@ -95,18 +129,18 @@ fun DateRoadPlaceCardPreview() { DateRoadPlaceCard( placeCardType = PlaceCardType.COURSE_NORMAL, sequence = 0, - place = Place(title = "์„ฑ์ˆ˜๋ฏธ์ˆ ๊ด€ ์„ฑ์ˆ˜์ ์„ฑ์ˆ˜๋ฏธ์ˆ ๊ด€ ์„ฑ์ˆ˜์ ์„ฑ์ˆ˜๋ฏธ์ˆ ๊ด€ ์„ฑ์ˆ˜์ ์„ฑ์ˆ˜๋ฏธ์ˆ ๊ด€ ์„ฑ์ˆ˜์ ์„ฑ์ˆ˜๋ฏธ์ˆ ๊ด€ ์„ฑ์ˆ˜์ ", duration = "2.5์‹œ๊ฐ„") + place = Place(title = "์„ฑ์ˆ˜๋ฏธ์ˆ ๊ด€ ์„ฑ์ˆ˜์ ์„ฑ์ˆ˜๋ฏธ์ˆ ๊ด€ ์„ฑ์ˆ˜์ ์„ฑ์ˆ˜๋ฏธ์ˆ ๊ด€ ์„ฑ์ˆ˜์ ์„ฑ์ˆ˜๋ฏธ์ˆ ๊ด€ ์„ฑ์ˆ˜์ ์„ฑ์ˆ˜๋ฏธ์ˆ ๊ด€ ์„ฑ์ˆ˜์ ", address = "์„œ์šธ ๊ด‘์ง„๊ตฌ ์ž์–‘๋™ 704-1", duration = "2.5์‹œ๊ฐ„") ) Spacer(modifier = Modifier.height(8.dp)) DateRoadPlaceCard( placeCardType = PlaceCardType.COURSE_EDIT, - place = Place(title = "์„ฑ์ˆ˜๋ฏธ์ˆ ๊ด€ ์„ฑ์ˆ˜์ ", duration = "1์‹œ๊ฐ„"), + place = Place(title = "์„ฑ์ˆ˜๋ฏธ์ˆ ๊ด€ ์„ฑ์ˆ˜์ ", address = "์„œ์šธ ๊ด‘์ง„๊ตฌ ์ž์–‘๋™ 704-1", duration = "1์‹œ๊ฐ„"), onIconClick = { } ) Spacer(modifier = Modifier.height(8.dp)) DateRoadPlaceCard( placeCardType = PlaceCardType.COURSE_DELETE, - place = Place(title = "์„ฑ์ˆ˜๋ฏธ์ˆ ๊ด€ ์„ฑ์ˆ˜์ ", duration = "0.5์‹œ๊ฐ„"), + place = Place(title = "์„ฑ์ˆ˜๋ฏธ์ˆ ๊ด€ ์„ฑ์ˆ˜์ ", address = "์„œ์šธ ๊ด‘์ง„๊ตฌ ์ž์–‘๋™ 704-1", duration = "0.5์‹œ๊ฐ„"), onIconClick = { } ) } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailScreen.kt index 9a97bb8..c2f9857 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailScreen.kt @@ -422,10 +422,12 @@ fun CourseDetailScreenPreview() { places = listOf( Place( title = "Place 1", + address = "Address 1", duration = "1" ), Place( title = "Place 2", + address = "Address 2", duration = "2" ) ), diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollSecondScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollSecondScreen.kt index 9eebbe9..3ddba55 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollSecondScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollSecondScreen.kt @@ -98,7 +98,7 @@ fun EnrollSecondScreen( onPlaceSearchButtonClick = onPlaceSearchButtonClick, onSelectedCourseTimeClick = onSelectedPlaceCourseTimeClick, onAddCourseButtonClick = { - onAddPlaceButtonClick(Place(title = enrollUiState.place.title, duration = enrollUiState.place.duration + Time.TIME)) + onAddPlaceButtonClick(Place(title = enrollUiState.place.title, address = enrollUiState.place.address, duration = enrollUiState.place.duration + Time.TIME)) } ) Spacer(modifier = Modifier.height(22.dp)) @@ -162,6 +162,8 @@ fun EnrollSecondScreen( items(enrollUiState.enroll.places.size) { index -> DateRoadPlaceCard( modifier = Modifier + .fillMaxWidth() + .height(76.dp) .zIndex(if (index == dragDropListState.currentIndexOfDraggedItem) 1f else 0f) .graphicsLayer( scaleX = animateFloatAsState(if (dragDropListState.currentIndexOfDraggedItem == index) 1.1f else 1.0f, label = "").value, diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollViewModel.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollViewModel.kt index 7e9b247..b04dfb2 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollViewModel.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollViewModel.kt @@ -104,9 +104,10 @@ class EnrollViewModel @Inject constructor( thumbnailIndex = if (event.moveThumbnail) (thumbnailIndex - 1).coerceAtLeast(0) else thumbnailIndex ) } + is EnrollContract.EnrollEvent.OnTitleValueChange -> setState { copy(enroll = currentState.enroll.copy(title = event.title)) } is EnrollContract.EnrollEvent.OnPlaceSelected -> { - setState { copy(keyword = "", placeSearchResult = PlaceSearchResult(emptyList()), place = currentState.place.copy(title = event.placeInfo.placeName), placeInfos = currentState.placeInfos + event.placeInfo, isPlaceSearchBottomSheetOpen = false) } + setState { copy(keyword = "", placeSearchResult = PlaceSearchResult(emptyList()), place = currentState.place.copy(title = event.placeInfo.placeName, address = event.placeInfo.addressName), placeInfos = currentState.placeInfos + event.placeInfo, isPlaceSearchBottomSheetOpen = false) } } is EnrollContract.EnrollEvent.OnDatePickerBottomSheetButtonClick -> setState { copy(enroll = currentState.enroll.copy(date = event.date), isDatePickerBottomSheetOpen = false) } @@ -150,8 +151,9 @@ class EnrollViewModel @Inject constructor( setEvent(EnrollContract.EnrollEvent.FetchCourseDetail(fetchEnrollState = LoadState.Loading, courseDetail = null)) getCourseDetailUseCase(courseId = courseId).onSuccess { courseDetail -> setEvent(EnrollContract.EnrollEvent.FetchCourseDetail(fetchEnrollState = LoadState.Success, courseDetail = courseDetail.copy(startAt = courseDetail.startAt.substringBefore(NEAREST_DATE_START_OUTPUT_FORMAT)))) + }.onFailure { + setEvent(EnrollContract.EnrollEvent.FetchCourseDetail(fetchEnrollState = LoadState.Error, courseDetail = null)) } - setEvent(EnrollContract.EnrollEvent.FetchCourseDetail(fetchEnrollState = LoadState.Error, courseDetail = null)) } } @@ -193,14 +195,11 @@ class EnrollViewModel @Inject constructor( private fun getPlaceSearchResult() { viewModelScope.launch { - getPlaceSearchResultUseCase(keyword = currentState.keyword).fold( - onSuccess = { placeSearchResult -> - setState { copy(placeSearchResult = placeSearchResult) } - }, - onFailure = { - setEvent(EnrollContract.EnrollEvent.Enroll(loadState = LoadState.Error)) - } - ) + getPlaceSearchResultUseCase(keyword = currentState.keyword).onSuccess { placeSearchResult -> + setState { copy(placeSearchResult = placeSearchResult) } + }.onFailure { + setEvent(EnrollContract.EnrollEvent.Enroll(loadState = LoadState.Error)) + } } } } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/timelinedetail/TimelineDetailScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/timelinedetail/TimelineDetailScreen.kt index c05bf89..05727c4 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/timelinedetail/TimelineDetailScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/timelinedetail/TimelineDetailScreen.kt @@ -384,10 +384,12 @@ fun TimelineDetailScreenPreview() { places = listOf( Place( title = "๋„๋‹น๊ณ ๋“ฑํ•™๊ต 2-7 ๋ฐ์ดํŠธ", + address = "์„œ์šธํŠน๋ณ„์‹œ ๊ฐ•๋‚จ๊ตฌ ๋„์‚ฐ๋Œ€๋กœ 104", duration = "1.5" ), Place( title = "2๋ฒˆ ๋ฐ์ดํŠธ", + address = "์„œ์šธํŠน๋ณ„์‹œ ๊ฐ•๋‚จ๊ตฌ ๋„์‚ฐ๋Œ€๋กœ 104", duration = "2.5" ) ), From 611d1b6bbf434c0e14ee5b0f8a0c60a656c7b17e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=84=EC=9A=B0?= <85734140+jinuemong@users.noreply.github.com> Date: Thu, 3 Apr 2025 22:55:30 +0900 Subject: [PATCH 14/49] =?UTF-8?q?[feat]=20=EC=8B=9C=EC=B2=AD=ED=98=95=20?= =?UTF-8?q?=EA=B4=91=EA=B3=A0=20=EA=B5=AC=ED=98=84=20(#26)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: ๊ด‘๊ณ  ํฌ์ธํŠธ ์ƒ์„ฑ remote ์—ฐ๊ฒฐ ๋ฐ usecase ์ƒ์„ฑ * feat: viewModel ์ ์šฉ * chore: ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ ์šฉ ๋ฐ ads ์ถ”๊ฐ€ * chore: ci/cd ์ถ”๊ฐ€ * feat: ๊ด‘๊ณ  UI ๋กœ์ง ์ ์šฉ * fix: ํ‚ค ์˜ค๋ฅ˜ ์ˆ˜์ • * feat: ๊ด‘๊ณ  ์‹œ์ฒญ 5ํšŒ ์ œํ•œ ๋กœ์ง ๊ตฌํ˜„ * style: ์ฝ”๋“œ ํฌ๋งท ์ˆ˜์ • & ๋ฆฐํŠธ ์ฒดํฌ * fix : ๋นผ๋จน์€ ๋กœ์ง ์ถ”๊ฐ€ --- .github/workflows/android_cd.yml | 4 ++ .github/workflows/android_ci.yml | 4 ++ app/build.gradle.kts | 4 +- app/src/main/AndroidManifest.xml | 4 ++ .../java/org/sopt/teamdateroad/DateRoadApp.kt | 6 ++ .../datasource/UserPointRemoteDataSource.kt | 2 + .../UserPointRemoteDataSourceImpl.kt | 2 + .../dataremote/service/UserPointService.kt | 3 + .../repositoryimpl/UserPointRepositoryImpl.kt | 4 ++ .../domain/repository/UserPointRepository.kt | 2 + .../domain/usecase/PostAdsPointUseCase.kt | 12 ++++ .../OneButtonDialogWithDescriptionType.kt | 5 ++ .../ui/coursedetail/CourseDetailContract.kt | 7 ++- .../ui/coursedetail/CourseDetailScreen.kt | 54 ++++++++++++++++-- .../ui/coursedetail/CourseDetailViewModel.kt | 18 +++++- .../ui/pointhistory/PointHistoryContract.kt | 7 ++- .../ui/pointhistory/PointHistoryScreen.kt | 57 +++++++++++++++++-- .../ui/pointhistory/PointHistoryViewModel.kt | 18 +++++- app/src/main/res/values/strings.xml | 2 + gradle/libs.versions.toml | 4 +- 20 files changed, 204 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/org/sopt/teamdateroad/domain/usecase/PostAdsPointUseCase.kt diff --git a/.github/workflows/android_cd.yml b/.github/workflows/android_cd.yml index 7dd3ff4..a5910a1 100644 --- a/.github/workflows/android_cd.yml +++ b/.github/workflows/android_cd.yml @@ -51,11 +51,15 @@ jobs: KAKAO_NATIVE_APP_KEY_MANIFEST: ${{ secrets.KAKAO_NATIVE_APP_KEY }} KAKAO_NATIVE_APP_KEY: ${{ secrets.KAKAO_NATIVE_APP_KEY }} AMPLITUDE_API_KEY: ${{ secrets.AMPLITUDE_API_KEY }} + GOOGLE_ADS_API_ID: ${{ secrets.GOOGLE_ADS_API_ID }} + GOOGLE_ADS_API_ID_MANIFEST: ${{ secrets.GOOGLE_ADS_API_ID }} run: | echo "dev.base.url=\"$BASE_URL\"" >> local.properties echo "kakao.native.app.key.manifest=\"$KAKAO_NATIVE_APP_KEY_MANIFEST\"" >> local.properties echo "kakao.native.app.key=\"$KAKAO_NATIVE_APP_KEY\"" >> local.properties echo "amplitude.dev.api.key=\"$AMPLITUDE_API_KEY\"" >> local.properties + echo "google.ads.api.id=\"GOOGLE_ADS_API_ID\"" >> local.properties + echo "google.ads.api.id.manifest=\"GOOGLE_ADS_API_ID_MANIFEST\"" >> local.properties - name: Build Release APK run: | diff --git a/.github/workflows/android_ci.yml b/.github/workflows/android_ci.yml index 4c764ee..acd2f8a 100644 --- a/.github/workflows/android_ci.yml +++ b/.github/workflows/android_ci.yml @@ -54,6 +54,8 @@ jobs: KAKAO_NATIVE_APP_KEY: ${{ secrets.KAKAO_NATIVE_APP_KEY }} KAKAO_REST_API_KEY: ${{ secrets.KAKAO_REST_API_KEY }} AMPLITUDE_API_KEY: ${{ secrets.AMPLITUDE_API_KEY }} + GOOGLE_ADS_API_ID: ${{ secrets.GOOGLE_ADS_API_ID }} + GOOGLE_ADS_API_ID_MANIFEST: ${{ secrets.GOOGLE_ADS_API_ID }} run: | echo "dev.base.url=\"$BASE_URL\"" >> local.properties echo "kakao.base.url=\"$KAKAO_BASE_URL\"" >> local.properties @@ -61,6 +63,8 @@ jobs: echo "kakao.native.app.key=\"$KAKAO_NATIVE_APP_KEY\"" >> local.properties echo "kakao.rest.api.key=\"$KAKAO_REST_API_KEY\"" >> local.properties echo "amplitude.dev.api.key=\"$AMPLITUDE_API_KEY\"" >> local.properties + echo "google.ads.api.id=\"GOOGLE_ADS_API_ID\"" >> local.properties + echo "google.ads.api.id.manifest=\"GOOGLE_ADS_API_ID_MANIFEST\"" >> local.properties - name: Lint Check run: ./gradlew ktlintCheck -PcompileSdkVersion=34 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 98a5785..75eca75 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -33,8 +33,9 @@ android { buildConfigField("String", "KAKAO_NATIVE_APP_KEY", properties["kakao.native.app.key"].toString()) buildConfigField("String", "KAKAO_REST_API_KEY", properties["kakao.rest.api.key"].toString()) - + buildConfigField("String", "GOOGLE_ADS_API_ID", properties["google.ads.api.id"].toString()) manifestPlaceholders["KAKAO_NATIVE_APP_KEY_MANIFEST"] = properties["kakao.native.app.key.manifest"] as String + manifestPlaceholders["GOOGLE_ADS_API_ID_MANIFEST"] = properties["google.ads.api.id.manifest"] as String } buildTypes { @@ -96,6 +97,7 @@ dependencies { implementation(platform(libs.google.firebase.bom)) implementation(libs.google.firebase.crashlytics) implementation(libs.firebase.crashlytics.buildtools) + implementation(libs.google.gms.ads) // Network implementation(platform(libs.okhttp.bom)) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e75d9c2..96ffc14 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -18,6 +18,10 @@ android:usesCleartextTraffic="true" tools:targetApi="31"> + + = runCatching { userPointRemoteDataSource.postUsePoint(courseId = courseId, requestUsePointDto = usePoint.toData()).toDomain() } + + override suspend fun postAdsPoint(): Result = runCatching { + userPointRemoteDataSource.postAdsPoint() + } } diff --git a/app/src/main/java/org/sopt/teamdateroad/domain/repository/UserPointRepository.kt b/app/src/main/java/org/sopt/teamdateroad/domain/repository/UserPointRepository.kt index b95e069..d96f679 100644 --- a/app/src/main/java/org/sopt/teamdateroad/domain/repository/UserPointRepository.kt +++ b/app/src/main/java/org/sopt/teamdateroad/domain/repository/UserPointRepository.kt @@ -11,4 +11,6 @@ interface UserPointRepository { suspend fun getPointHistory(): Result suspend fun postUsePoint(courseId: Int, usePoint: UsePoint): Result + + suspend fun postAdsPoint(): Result } diff --git a/app/src/main/java/org/sopt/teamdateroad/domain/usecase/PostAdsPointUseCase.kt b/app/src/main/java/org/sopt/teamdateroad/domain/usecase/PostAdsPointUseCase.kt new file mode 100644 index 0000000..3643692 --- /dev/null +++ b/app/src/main/java/org/sopt/teamdateroad/domain/usecase/PostAdsPointUseCase.kt @@ -0,0 +1,12 @@ +package org.sopt.teamdateroad.domain.usecase + +import javax.inject.Inject +import javax.inject.Singleton +import org.sopt.teamdateroad.domain.repository.UserPointRepository + +@Singleton +class PostAdsPointUseCase @Inject constructor( + private val userPointRepository: UserPointRepository +) { + suspend operator fun invoke() = userPointRepository.postAdsPoint() +} diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/type/OneButtonDialogWithDescriptionType.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/type/OneButtonDialogWithDescriptionType.kt index bf97509..a793a3f 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/type/OneButtonDialogWithDescriptionType.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/type/OneButtonDialogWithDescriptionType.kt @@ -17,5 +17,10 @@ enum class OneButtonDialogWithDescriptionType( titleRes = R.string.one_button_dialog_with_description_cannot_enroll_course_title, descriptionRes = R.string.one_button_dialog_with_description_cannot_enroll_course_description, buttonTextRes = R.string.dialog_checked + ), + FULL_ADS( + titleRes = R.string.one_button_dialog_with_description_full_ads_title, + descriptionRes = R.string.one_button_dialog_with_description_full_ads_description, + buttonTextRes = R.string.dialog_checked ) } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailContract.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailContract.kt index 529de20..baf3590 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailContract.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailContract.kt @@ -24,12 +24,14 @@ class CourseDetailContract { val usePointLoadState: LoadState = LoadState.Idle, val deleteLoadState: LoadState = LoadState.Idle, var isWebViewOpened: Boolean = false, - var hasPointReadDialogOpened: Boolean = false + var hasPointReadDialogOpened: Boolean = false, + val isFullAdsDialogOpen: Boolean = false ) : UiState sealed interface CourseDetailSideEffect : UiSideEffect { data class NavigateToEnroll(val enrollType: EnrollType, val viewPath: String, val id: Int?) : CourseDetailSideEffect data object PopBackStack : CourseDetailSideEffect + data object NavigateToAds : CourseDetailSideEffect } sealed class CourseDetailEvent : UiEvent { @@ -55,5 +57,8 @@ class CourseDetailContract { data class DeleteCourse(val deleteLoadState: LoadState) : CourseDetailEvent() data object OnReportWebViewClicked : CourseDetailEvent() data object DismissReportWebView : CourseDetailEvent() + data object FailLoadAdsPoint : CourseDetailEvent() + data object FullAds : CourseDetailEvent() + data object DismissFullAdsDialog : CourseDetailEvent() } } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailScreen.kt index c2f9857..abacfb2 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailScreen.kt @@ -17,6 +17,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -26,16 +27,23 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.PagerState +import com.google.android.gms.ads.AdRequest +import com.google.android.gms.ads.LoadAdError +import com.google.android.gms.ads.rewarded.RewardedAd +import com.google.android.gms.ads.rewarded.RewardedAdLoadCallback +import org.sopt.teamdateroad.BuildConfig import org.sopt.teamdateroad.R import org.sopt.teamdateroad.domain.model.CourseDetail import org.sopt.teamdateroad.domain.model.Place import org.sopt.teamdateroad.presentation.type.CourseDetailUnopenedDetailType import org.sopt.teamdateroad.presentation.type.DateTagType.Companion.getDateTagTypeByName import org.sopt.teamdateroad.presentation.type.EnrollType +import org.sopt.teamdateroad.presentation.type.OneButtonDialogWithDescriptionType import org.sopt.teamdateroad.presentation.type.TwoButtonDialogWithDescriptionType import org.sopt.teamdateroad.presentation.ui.component.bottomsheet.DateRoadBasicBottomSheet import org.sopt.teamdateroad.presentation.ui.component.bottomsheet.DateRoadPointBottomSheet import org.sopt.teamdateroad.presentation.ui.component.bottomsheet.model.collect.DateRoadCollectPointType +import org.sopt.teamdateroad.presentation.ui.component.dialog.DateRoadOneButtonDialogWithDescription import org.sopt.teamdateroad.presentation.ui.component.dialog.DateRoadTwoButtonDialogWithDescription import org.sopt.teamdateroad.presentation.ui.component.pager.DateRoadImagePager import org.sopt.teamdateroad.presentation.ui.component.topbar.DateRoadScrollResponsiveTopBar @@ -70,6 +78,8 @@ fun CourseDetailRoute( ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val lifecycleOwner = LocalLifecycleOwner.current + val context = LocalContext.current + val adRequest = remember { AdRequest.Builder().build() } LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { viewModel.sideEffect.flowWithLifecycle(lifecycle = lifecycleOwner.lifecycle) @@ -77,6 +87,25 @@ fun CourseDetailRoute( when (courseDetailSideEffect) { is CourseDetailContract.CourseDetailSideEffect.NavigateToEnroll -> navigateToEnroll(courseDetailSideEffect.enrollType, courseDetailSideEffect.viewPath, courseDetailSideEffect.id) is CourseDetailContract.CourseDetailSideEffect.PopBackStack -> popBackStack() + CourseDetailContract.CourseDetailSideEffect.NavigateToAds -> { + RewardedAd.load( + context, + BuildConfig.GOOGLE_ADS_API_ID, + adRequest, + object : RewardedAdLoadCallback() { + override fun onAdLoaded(ad: RewardedAd) { + viewModel.postAdsPoint() + } + + override fun onAdFailedToLoad(error: LoadAdError) { + when (error.code) { + AdRequest.ERROR_CODE_NO_FILL -> viewModel.setEvent(CourseDetailContract.CourseDetailEvent.FullAds) + else -> viewModel.setEvent(CourseDetailContract.CourseDetailEvent.FailLoadAdsPoint) + } + } + } + ) + } } } } @@ -150,6 +179,12 @@ fun CourseDetailRoute( }, onDismissCollectPoint = { viewModel.setEvent(CourseDetailContract.CourseDetailEvent.DismissDialogPointLack) + }, + onSelectAds = { + viewModel.setSideEffect(CourseDetailContract.CourseDetailSideEffect.NavigateToAds) + }, + onDismissFullAdsDialog = { + viewModel.setEvent(CourseDetailContract.CourseDetailEvent.DismissFullAdsDialog) } ) } @@ -201,7 +236,9 @@ fun CourseDetailScreen( dismissReportCourseBottomSheet: () -> Unit, enrollSchedule: () -> Unit, onTopBarIconClicked: () -> Unit, - openCourseDetail: () -> Unit + openCourseDetail: () -> Unit, + onSelectAds: () -> Unit, + onDismissFullAdsDialog: () -> Unit ) { var imageHeight by remember { mutableIntStateOf(0) } @@ -350,13 +387,20 @@ fun CourseDetailScreen( ) } + if (courseDetailUiState.isFullAdsDialogOpen) { + DateRoadOneButtonDialogWithDescription( + oneButtonDialogWithDescriptionType = OneButtonDialogWithDescriptionType.FULL_ADS, + onDismissRequest = onDismissFullAdsDialog, + onClickConfirm = onDismissFullAdsDialog + ) + } + DateRoadPointBottomSheet( isBottomSheetOpen = courseDetailUiState.isPointCollectBottomSheetOpen, title = stringResource(R.string.point_box_lack_point_button_text), onClick = { dateRoadCollectPointType -> when (dateRoadCollectPointType) { - // TODO : add ADS - DateRoadCollectPointType.WATCH_ADS -> Unit + DateRoadCollectPointType.WATCH_ADS -> onSelectAds() DateRoadCollectPointType.COURSE_REGISTRATION -> onSelectEnroll() } onDismissCollectPoint() @@ -463,7 +507,9 @@ fun CourseDetailScreenPreview() { dismissDialogDeleteCourse = {}, onDismissCollectPoint = {}, onSelectEnroll = {}, - dismissDialogReportCourse = {} + dismissDialogReportCourse = {}, + onSelectAds = {}, + onDismissFullAdsDialog = {} ) } } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailViewModel.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailViewModel.kt index 3bd782e..4462196 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailViewModel.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailViewModel.kt @@ -8,6 +8,7 @@ import org.sopt.teamdateroad.domain.model.UsePoint import org.sopt.teamdateroad.domain.usecase.DeleteCourseLikeUseCase import org.sopt.teamdateroad.domain.usecase.DeleteCourseUseCase import org.sopt.teamdateroad.domain.usecase.GetCourseDetailUseCase +import org.sopt.teamdateroad.domain.usecase.PostAdsPointUseCase import org.sopt.teamdateroad.domain.usecase.PostCourseLikeUseCase import org.sopt.teamdateroad.domain.usecase.PostUsePointUseCase import org.sopt.teamdateroad.presentation.util.CourseDetailAmplitude.CLICK_COURSE_LIKES @@ -26,7 +27,8 @@ class CourseDetailViewModel @Inject constructor( private val deleteCourseLikeUseCase: DeleteCourseLikeUseCase, private val getCourseDetailUseCase: GetCourseDetailUseCase, private val postCourseLikeUseCase: PostCourseLikeUseCase, - private val postUsePointUseCase: PostUsePointUseCase + private val postUsePointUseCase: PostUsePointUseCase, + private val postAdsPointUseCase: PostAdsPointUseCase ) : BaseViewModel() { override fun createInitialState(): CourseDetailContract.CourseDetailUiState = CourseDetailContract.CourseDetailUiState() @@ -54,6 +56,9 @@ class CourseDetailViewModel @Inject constructor( is CourseDetailContract.CourseDetailEvent.PostUsePoint -> setState { copy(usePointLoadState = event.usePointLoadState, courseDetail = courseDetail.copy(isAccess = event.isAccess)) } is CourseDetailContract.CourseDetailEvent.OnReportWebViewClicked -> setState { copy(isWebViewOpened = true) } is CourseDetailContract.CourseDetailEvent.DismissReportWebView -> setState { copy(isWebViewOpened = false) } + CourseDetailContract.CourseDetailEvent.FailLoadAdsPoint -> setState { copy(loadState = LoadState.Loading) } + CourseDetailContract.CourseDetailEvent.DismissFullAdsDialog -> setState { copy(isFullAdsDialogOpen = false) } + CourseDetailContract.CourseDetailEvent.FullAds -> setState { copy(isFullAdsDialogOpen = true) } } } @@ -124,4 +129,15 @@ class CourseDetailViewModel @Inject constructor( } } } + + fun postAdsPoint() { + setState { copy(loadState = LoadState.Loading) } + viewModelScope.launch { + postAdsPointUseCase().onSuccess { + setState { copy(loadState = LoadState.Success) } + }.onFailure { + setState { copy(loadState = LoadState.Loading) } + } + } + } } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryContract.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryContract.kt index 79dc391..05f548d 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryContract.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryContract.kt @@ -15,11 +15,13 @@ class PointHistoryContract { val userPoint: UserPoint = UserPoint(), val isPointCollectBottomSheetOpen: Boolean = false, val pointHistoryTabType: PointHistoryTabType = PointHistoryTabType.GAINED_HISTORY, - val pointHistory: PointHistory = PointHistory() + val pointHistory: PointHistory = PointHistory(), + val isFullAdsDialogOpen: Boolean = false ) : UiState sealed interface PointHistorySideEffect : UiSideEffect { data object PopBackStack : PointHistorySideEffect + data object NavigateToAds : PointHistorySideEffect data class NavigateToEnroll(val enrollType: EnrollType, val viewPath: String, val id: Int?) : PointHistorySideEffect } @@ -27,7 +29,10 @@ class PointHistoryContract { data class FetchPointHistory(val loadState: LoadState, val pointHistory: PointHistory) : PointHistoryEvent() data class FetchUserPoint(val loadState: LoadState, val userPoint: UserPoint) : PointHistoryEvent() data class OnTabBarClicked(val pointHistoryTabType: PointHistoryTabType) : PointHistoryEvent() + data object FailLoadAdsPoint : PointHistoryEvent() data object OnPointCollectBottomSheetClick : PointHistoryEvent() data object OnPointCollectBottomSheetDismiss : PointHistoryEvent() + data object FullAds : PointHistoryEvent() + data object DismissFullAdsDialog : PointHistoryEvent() } } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryScreen.kt index 140953a..20cdb86 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryScreen.kt @@ -13,7 +13,9 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -21,15 +23,22 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle +import com.google.android.gms.ads.AdRequest +import com.google.android.gms.ads.LoadAdError +import com.google.android.gms.ads.rewarded.RewardedAd +import com.google.android.gms.ads.rewarded.RewardedAdLoadCallback +import org.sopt.teamdateroad.BuildConfig import org.sopt.teamdateroad.R import org.sopt.teamdateroad.domain.model.Point import org.sopt.teamdateroad.domain.model.PointHistory import org.sopt.teamdateroad.domain.model.UserPoint import org.sopt.teamdateroad.presentation.type.EmptyViewType import org.sopt.teamdateroad.presentation.type.EnrollType +import org.sopt.teamdateroad.presentation.type.OneButtonDialogWithDescriptionType import org.sopt.teamdateroad.presentation.type.PointHistoryTabType import org.sopt.teamdateroad.presentation.ui.component.bottomsheet.DateRoadPointBottomSheet import org.sopt.teamdateroad.presentation.ui.component.bottomsheet.model.collect.DateRoadCollectPointType +import org.sopt.teamdateroad.presentation.ui.component.dialog.DateRoadOneButtonDialogWithDescription import org.sopt.teamdateroad.presentation.ui.component.tabbar.DateRoadTabBar import org.sopt.teamdateroad.presentation.ui.component.tabbar.DateRoadTabTitle import org.sopt.teamdateroad.presentation.ui.component.topbar.DateRoadBasicTopBar @@ -53,6 +62,8 @@ fun PointHistoryRoute( ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val lifecycleOwner = LocalLifecycleOwner.current + val context = LocalContext.current + val adRequest = remember { AdRequest.Builder().build() } LaunchedEffect(Unit) { viewModel.fetchPointHistory() @@ -69,6 +80,27 @@ fun PointHistoryRoute( pointHistorySideEffect.viewPath, pointHistorySideEffect.id ) + + PointHistoryContract.PointHistorySideEffect.NavigateToAds -> { + RewardedAd.load( + context, + BuildConfig.GOOGLE_ADS_API_ID, + adRequest, + object : RewardedAdLoadCallback() { + override fun onAdLoaded(ad: RewardedAd) { + viewModel.postAdsPoint() + } + + override fun onAdFailedToLoad(error: LoadAdError) { + when (error.code) { + AdRequest.ERROR_CODE_NO_FILL -> viewModel.setEvent(PointHistoryContract.PointHistoryEvent.FullAds) + + else -> viewModel.setEvent(PointHistoryContract.PointHistoryEvent.FailLoadAdsPoint) + } + } + } + ) + } } } } @@ -98,6 +130,12 @@ fun PointHistoryRoute( id = null ) ) + }, + onSelectAds = { + viewModel.setSideEffect(PointHistoryContract.PointHistorySideEffect.NavigateToAds) + }, + onDismissFullAdsDialog = { + viewModel.setEvent(PointHistoryContract.PointHistoryEvent.DismissFullAdsDialog) } ) } @@ -114,7 +152,9 @@ fun PointHistoryScreen( onTopBarIconClicked: () -> Unit, onClickCollectPoint: () -> Unit, onDisMissCollectPoint: () -> Unit, - onSelectEnroll: () -> Unit + onSelectEnroll: () -> Unit, + onSelectAds: () -> Unit, + onDismissFullAdsDialog: () -> Unit ) { Column( modifier = Modifier @@ -179,13 +219,20 @@ fun PointHistoryScreen( } } + if (pointHistoryUiState.isFullAdsDialogOpen) { + DateRoadOneButtonDialogWithDescription( + oneButtonDialogWithDescriptionType = OneButtonDialogWithDescriptionType.FULL_ADS, + onDismissRequest = onDismissFullAdsDialog, + onClickConfirm = onDismissFullAdsDialog + ) + } + DateRoadPointBottomSheet( isBottomSheetOpen = pointHistoryUiState.isPointCollectBottomSheetOpen, title = stringResource(R.string.point_box_get_point_button_text), onClick = { dateRoadCollectPointType -> when (dateRoadCollectPointType) { - // TODO : add ADS - DateRoadCollectPointType.WATCH_ADS -> Unit + DateRoadCollectPointType.WATCH_ADS -> onSelectAds() DateRoadCollectPointType.COURSE_REGISTRATION -> onSelectEnroll() } onDisMissCollectPoint() @@ -218,7 +265,9 @@ fun PointHistoryPreview() { onTopBarIconClicked = {}, onClickCollectPoint = {}, onDisMissCollectPoint = {}, - onSelectEnroll = {} + onSelectEnroll = {}, + onSelectAds = {}, + onDismissFullAdsDialog = {} ) } } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryViewModel.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryViewModel.kt index 2772fcb..990cac5 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryViewModel.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryViewModel.kt @@ -6,13 +6,15 @@ import javax.inject.Inject import kotlinx.coroutines.launch import org.sopt.teamdateroad.domain.usecase.GetPointHistoryUseCase import org.sopt.teamdateroad.domain.usecase.GetUserPointUseCase +import org.sopt.teamdateroad.domain.usecase.PostAdsPointUseCase import org.sopt.teamdateroad.presentation.util.base.BaseViewModel import org.sopt.teamdateroad.presentation.util.view.LoadState @HiltViewModel class PointHistoryViewModel @Inject constructor( private val getPointHistoryUseCase: GetPointHistoryUseCase, - private val getUserPointUseCase: GetUserPointUseCase + private val getUserPointUseCase: GetUserPointUseCase, + private val postAdsPointUseCase: PostAdsPointUseCase ) : BaseViewModel() { override fun createInitialState(): PointHistoryContract.PointHistoryUiState = PointHistoryContract.PointHistoryUiState() @@ -24,6 +26,9 @@ class PointHistoryViewModel @Inject constructor( is PointHistoryContract.PointHistoryEvent.FetchUserPoint -> setState { copy(loadState = event.loadState, userPoint = event.userPoint) } PointHistoryContract.PointHistoryEvent.OnPointCollectBottomSheetClick -> setState { copy(isPointCollectBottomSheetOpen = true) } PointHistoryContract.PointHistoryEvent.OnPointCollectBottomSheetDismiss -> setState { copy(isPointCollectBottomSheetOpen = false) } + PointHistoryContract.PointHistoryEvent.FailLoadAdsPoint -> setState { copy(loadState = LoadState.Loading) } + PointHistoryContract.PointHistoryEvent.DismissFullAdsDialog -> setState { copy(isFullAdsDialogOpen = false) } + PointHistoryContract.PointHistoryEvent.FullAds -> setState { copy(isFullAdsDialogOpen = true) } } } @@ -54,4 +59,15 @@ class PointHistoryViewModel @Inject constructor( } } } + + fun postAdsPoint() { + viewModelScope.launch { + postAdsPointUseCase().onSuccess { + fetchPointHistory() + fetchUserPoint() + }.onFailure { + setState { copy(loadState = LoadState.Loading) } + } + } + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 06a287d..3def010 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -112,6 +112,8 @@ ์กฐ๊ธˆ๋งŒ ๊ธฐ๋‹ค๋ ค์ฃผ์„ธ์š” :) ๋ฐ์ดํŠธ๋ฅผ ๋” ์ด์ƒ ๋“ฑ๋กํ•  ์ˆ˜ ์—†์–ด์š”! ๋ฐ์ดํŠธ๋Š” ์ตœ๋Œ€ 5๊ฐœ๊นŒ์ง€๋งŒ ๋“ฑ๋ก ๊ฐ€๋Šฅํ•ด์š” + ๊ด‘๊ณ ๋Š” ํ•˜๋ฃจ 5ํšŒ๊นŒ์ง€๋งŒ ์‹œ์ฒญํ•  ์ˆ˜ ์žˆ์–ด์š” + ๊ด‘๊ณ  ์‹œ์ฒญ ํ•œ๋„๋Š” ๋งค์ผ ์ž์ •์— ์ดˆ๊ธฐํ™”๋ผ์š” ํš๋“ ๋‚ด์—ญ diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1fbd01b..def392c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,7 +32,7 @@ googleServices = "4.4.2" firebaseCrashlytics = "3.0.2" firebaseBom = "33.3.0" firebaseCrashlyticsBuildtools = "3.0.2" - +googleAds = "23.0.0" # Accompanist accompanistPager = "0.25.0" accompanistWebview = "0.24.13-rc" @@ -84,7 +84,7 @@ google-firebase-crashlytics-gradle = { group = "com.google.firebase", name = "fi google-firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" } google-firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics-ktx" } firebase-crashlytics-buildtools = { group = "com.google.firebase", name = "firebase-crashlytics-buildtools", version.ref = "firebaseCrashlyticsBuildtools" } - +google-gms-ads = {group = "com.google.android.gms",name = "play-services-ads", version.ref = "googleAds"} # Accompanist accompanist-pager = { module = "com.google.accompanist:accompanist-pager", version.ref = "accompanistPager" } accompanist-pager-indicators = { module = "com.google.accompanist:accompanist-pager-indicators", version.ref = "accompanistPager" } From 5b1f9c5438015c37309a2af7700ae04673cf0008 Mon Sep 17 00:00:00 2001 From: Yaechan Park <102402485+dpcks0509@users.noreply.github.com> Date: Sat, 5 Apr 2025 21:47:31 +0900 Subject: [PATCH 15/49] =?UTF-8?q?[feat]=20=EC=9E=A5=EC=86=8C=EA=B2=80?= =?UTF-8?q?=EC=83=89=20Paging=20=EB=AC=B4=ED=95=9C=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A1=A4=20&=20Debounce=20=EC=A0=81=EC=9A=A9=20(#28)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: paging-runtime ์˜์กด์„ฑ ์ถ”๊ฐ€ * feat: PlaceSearchResultPagingSource ๊ตฌํ˜„ * feat: Paging ์ดˆ๊ธฐ ๊ตฌํ˜„ * refactor: DateRoadPlaceCard ์ฃผ์„ ์ œ๊ฑฐ * feat: ์žฅ์†Œ ๊ฒ€์ƒ‰ paging ๊ตฌํ˜„ * feat: Paging MAX_SIZE ์ง€์ • * refactor: paging ๋ฐฉ์‹ ๋ฆฌํŒฉํ† ๋ง * refactor: UseCase์— @Singleton ์ถ”๊ฐ€ * refactor: ์žฅ์†Œ ๊ฒ€์ƒ‰ debounce ์ ์šฉ * [feat] ์‹œ์ฒญํ˜• ๊ด‘๊ณ  ๊ตฌํ˜„ (#26) * feat: ๊ด‘๊ณ  ํฌ์ธํŠธ ์ƒ์„ฑ remote ์—ฐ๊ฒฐ ๋ฐ usecase ์ƒ์„ฑ * feat: viewModel ์ ์šฉ * chore: ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ ์šฉ ๋ฐ ads ์ถ”๊ฐ€ * chore: ci/cd ์ถ”๊ฐ€ * feat: ๊ด‘๊ณ  UI ๋กœ์ง ์ ์šฉ * fix: ํ‚ค ์˜ค๋ฅ˜ ์ˆ˜์ • * feat: ๊ด‘๊ณ  ์‹œ์ฒญ 5ํšŒ ์ œํ•œ ๋กœ์ง ๊ตฌํ˜„ * style: ์ฝ”๋“œ ํฌ๋งท ์ˆ˜์ • & ๋ฆฐํŠธ ์ฒดํฌ * fix : ๋นผ๋จน์€ ๋กœ์ง ์ถ”๊ฐ€ * remove: PlaceSearchUiState ์ œ๊ฑฐ * refactor: PagingData Result ๋ž˜ํผ ์ œ๊ฑฐ ๋ฐ searchPlaceInfos flatMapLatest ํ™œ์šฉ * refactor: DEBOUNCE_TIME ๋‹จ์œ„ ์ถ”๊ฐ€ * refactor: PlaceSearchPagingSource Data Layer๋กœ ์ด๋™ --------- Co-authored-by: ๊น€์ง„์šฐ <85734140+jinuemong@users.noreply.github.com> --- .../datasource/PlaceSearchDataSource.kt | 6 +- .../PlaceSearchDataSourceImpl.kt | 59 ++++++++++++++++- .../model/response/ResponseMetaDto.kt | 10 +++ .../response/ResponsePlaceSearchResultDto.kt | 3 + .../dataremote/service/PlaceSearchService.kt | 4 +- .../todomain/ResponsePlaceInfosMapper.kt | 7 +++ .../todomain/ResponsePlaceSearchDtoMapper.kt | 3 +- .../PlaceSearchRepositoryImpl.kt | 14 +++-- .../domain/model/PlaceSearchResult.kt | 3 +- .../repository/PlaceSearchRepository.kt | 6 +- .../usecase/GetNearestTimelineUseCase.kt | 2 + .../usecase/GetPlaceSearchResultUseCase.kt | 8 ++- .../usecase/GetTimelineDetailUseCase.kt | 2 + .../domain/usecase/GetTimelinesUseCase.kt | 2 + .../domain/usecase/GetUserPointUseCase.kt | 2 + .../ui/component/card/DateRoadPlaceCard.kt | 5 +- .../presentation/ui/enroll/EnrollContract.kt | 5 +- .../presentation/ui/enroll/EnrollScreen.kt | 15 +++++ .../ui/enroll/EnrollSecondScreen.kt | 15 +++-- .../presentation/ui/enroll/EnrollViewModel.kt | 50 ++++++++++----- .../PlaceSearchBottomSheet.kt} | 63 ++++++++++--------- gradle/libs.versions.toml | 9 ++- 22 files changed, 220 insertions(+), 73 deletions(-) create mode 100644 app/src/main/java/org/sopt/teamdateroad/data/dataremote/model/response/ResponseMetaDto.kt create mode 100644 app/src/main/java/org/sopt/teamdateroad/data/mapper/todomain/ResponsePlaceInfosMapper.kt rename app/src/main/java/org/sopt/teamdateroad/presentation/ui/{component/bottomsheet/DateRoadPlaceSearchBottomSheet.kt => enroll/PlaceSearchBottomSheet.kt} (86%) diff --git a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/datasource/PlaceSearchDataSource.kt b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/datasource/PlaceSearchDataSource.kt index 30831a0..400197f 100644 --- a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/datasource/PlaceSearchDataSource.kt +++ b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/datasource/PlaceSearchDataSource.kt @@ -1,9 +1,11 @@ package org.sopt.teamdateroad.data.dataremote.datasource -import org.sopt.teamdateroad.data.dataremote.model.response.ResponsePlaceSearchResultDto +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import org.sopt.teamdateroad.data.dataremote.model.response.ResponsePlaceInfoDto interface PlaceSearchDataSource { suspend fun getPlaceSearchResult( keyword: String - ): Result + ): Flow> } diff --git a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/datasourceimpl/PlaceSearchDataSourceImpl.kt b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/datasourceimpl/PlaceSearchDataSourceImpl.kt index b21d37c..06ba3f9 100644 --- a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/datasourceimpl/PlaceSearchDataSourceImpl.kt +++ b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/datasourceimpl/PlaceSearchDataSourceImpl.kt @@ -1,12 +1,65 @@ package org.sopt.teamdateroad.data.dataremote.datasourceimpl +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.PagingSource +import androidx.paging.PagingState import javax.inject.Inject +import kotlinx.coroutines.flow.Flow import org.sopt.teamdateroad.data.dataremote.datasource.PlaceSearchDataSource -import org.sopt.teamdateroad.data.dataremote.model.response.ResponsePlaceSearchResultDto +import org.sopt.teamdateroad.data.dataremote.model.response.ResponsePlaceInfoDto import org.sopt.teamdateroad.data.dataremote.service.PlaceSearchService +import org.sopt.teamdateroad.presentation.ui.enroll.MAX_SIZE +import org.sopt.teamdateroad.presentation.ui.enroll.PAGE_SIZE class PlaceSearchDataSourceImpl @Inject constructor(private val placeSearchService: PlaceSearchService) : PlaceSearchDataSource { - override suspend fun getPlaceSearchResult(keyword: String): Result { - return runCatching { placeSearchService.getPlaceSearchResult(keyword) } + override suspend fun getPlaceSearchResult(keyword: String): Flow> { + return Pager( + config = PagingConfig(pageSize = PAGE_SIZE, maxSize = MAX_SIZE), + pagingSourceFactory = { + PlaceSearchPagingSource( + keyword = keyword, + placeSearchService = placeSearchService + ) + } + ).flow + } +} + +class PlaceSearchPagingSource( + private val keyword: String, + private val placeSearchService: PlaceSearchService +) : PagingSource() { + override suspend fun load(params: LoadParams): LoadResult { + val page = params.key ?: INITIAL_PAGE + + return runCatching { + val placeSearchResult = placeSearchService.getPlaceSearchResult( + keyword = keyword, + page = page, + size = PAGE_SIZE + ) + + LoadResult.Page( + data = placeSearchResult.placeInfos, + prevKey = if (page == INITIAL_PAGE) null else page - PAGE_OFFSET, + nextKey = if (placeSearchResult.meta.isEnd) null else page + PAGE_OFFSET + ) + }.getOrElse { throwable -> + LoadResult.Error(throwable) + } + } + + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition?.let { position -> + state.closestPageToPosition(position)?.prevKey?.plus(PAGE_OFFSET) + ?: state.closestPageToPosition(position)?.nextKey?.minus(PAGE_OFFSET) + } + } + + companion object { + private const val INITIAL_PAGE = 1 + private const val PAGE_OFFSET = 1 } } diff --git a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/model/response/ResponseMetaDto.kt b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/model/response/ResponseMetaDto.kt new file mode 100644 index 0000000..d560d5d --- /dev/null +++ b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/model/response/ResponseMetaDto.kt @@ -0,0 +1,10 @@ +package org.sopt.teamdateroad.data.dataremote.model.response + +import com.google.gson.annotations.SerializedName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseMetaDto( + @SerializedName("is_end") + val isEnd: Boolean +) diff --git a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/model/response/ResponsePlaceSearchResultDto.kt b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/model/response/ResponsePlaceSearchResultDto.kt index 105bc53..4517400 100644 --- a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/model/response/ResponsePlaceSearchResultDto.kt +++ b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/model/response/ResponsePlaceSearchResultDto.kt @@ -5,6 +5,9 @@ import kotlinx.serialization.Serializable @Serializable data class ResponsePlaceSearchResultDto( + @SerializedName("meta") + val meta: ResponseMetaDto, + @SerializedName("documents") val placeInfos: List ) diff --git a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/service/PlaceSearchService.kt b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/service/PlaceSearchService.kt index 7c7d3a9..60a05c9 100644 --- a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/service/PlaceSearchService.kt +++ b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/service/PlaceSearchService.kt @@ -8,6 +8,8 @@ import retrofit2.http.Query interface PlaceSearchService { @GET(LOCAL_SEARCH_KEYWORD_JSON) suspend fun getPlaceSearchResult( - @Query("query") keyword: String + @Query("query") keyword: String, + @Query("page") page: Int, + @Query("size") size: Int ): ResponsePlaceSearchResultDto } diff --git a/app/src/main/java/org/sopt/teamdateroad/data/mapper/todomain/ResponsePlaceInfosMapper.kt b/app/src/main/java/org/sopt/teamdateroad/data/mapper/todomain/ResponsePlaceInfosMapper.kt new file mode 100644 index 0000000..ab90e39 --- /dev/null +++ b/app/src/main/java/org/sopt/teamdateroad/data/mapper/todomain/ResponsePlaceInfosMapper.kt @@ -0,0 +1,7 @@ +package org.sopt.teamdateroad.data.mapper.todomain + +import org.sopt.teamdateroad.data.dataremote.model.response.ResponsePlaceInfoDto +import org.sopt.teamdateroad.domain.model.PlaceInfo + +fun List.toDomain(): List = + this.map { responsePlaceInfoDto -> responsePlaceInfoDto.toDomain() } diff --git a/app/src/main/java/org/sopt/teamdateroad/data/mapper/todomain/ResponsePlaceSearchDtoMapper.kt b/app/src/main/java/org/sopt/teamdateroad/data/mapper/todomain/ResponsePlaceSearchDtoMapper.kt index 28a0e74..da6f95c 100644 --- a/app/src/main/java/org/sopt/teamdateroad/data/mapper/todomain/ResponsePlaceSearchDtoMapper.kt +++ b/app/src/main/java/org/sopt/teamdateroad/data/mapper/todomain/ResponsePlaceSearchDtoMapper.kt @@ -4,5 +4,6 @@ import org.sopt.teamdateroad.data.dataremote.model.response.ResponsePlaceSearchR import org.sopt.teamdateroad.domain.model.PlaceSearchResult fun ResponsePlaceSearchResultDto.toDomain(): PlaceSearchResult = PlaceSearchResult( - placeInfos = this.placeInfos.map { responsePlaceInfoDto -> responsePlaceInfoDto.toDomain() } + placeInfos = this.placeInfos.map { responsePlaceInfoDto -> responsePlaceInfoDto.toDomain() }, + isEnd = this.meta.isEnd ) diff --git a/app/src/main/java/org/sopt/teamdateroad/data/repositoryimpl/PlaceSearchRepositoryImpl.kt b/app/src/main/java/org/sopt/teamdateroad/data/repositoryimpl/PlaceSearchRepositoryImpl.kt index 611f426..caaccdd 100644 --- a/app/src/main/java/org/sopt/teamdateroad/data/repositoryimpl/PlaceSearchRepositoryImpl.kt +++ b/app/src/main/java/org/sopt/teamdateroad/data/repositoryimpl/PlaceSearchRepositoryImpl.kt @@ -1,15 +1,21 @@ package org.sopt.teamdateroad.data.repositoryimpl +import androidx.paging.PagingData +import androidx.paging.map import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map import org.sopt.teamdateroad.data.dataremote.datasource.PlaceSearchDataSource import org.sopt.teamdateroad.data.mapper.todomain.toDomain -import org.sopt.teamdateroad.domain.model.PlaceSearchResult +import org.sopt.teamdateroad.domain.model.PlaceInfo import org.sopt.teamdateroad.domain.repository.PlaceSearchRepository class PlaceSearchRepositoryImpl @Inject constructor(private val placeSearchDataSource: PlaceSearchDataSource) : PlaceSearchRepository { - override suspend fun getPlaceSearchResult(keyword: String): Result { - return placeSearchDataSource.getPlaceSearchResult(keyword).map { responsePlaceSearchResultDto -> - responsePlaceSearchResultDto.toDomain() + override suspend fun getPlaceSearchResult(keyword: String): Flow> { + return placeSearchDataSource.getPlaceSearchResult(keyword).map { pagingData -> + pagingData.map { responsePlaceInfoDto -> + responsePlaceInfoDto.toDomain() + } } } } diff --git a/app/src/main/java/org/sopt/teamdateroad/domain/model/PlaceSearchResult.kt b/app/src/main/java/org/sopt/teamdateroad/domain/model/PlaceSearchResult.kt index 78a45ee..0005188 100644 --- a/app/src/main/java/org/sopt/teamdateroad/domain/model/PlaceSearchResult.kt +++ b/app/src/main/java/org/sopt/teamdateroad/domain/model/PlaceSearchResult.kt @@ -1,5 +1,6 @@ package org.sopt.teamdateroad.domain.model data class PlaceSearchResult( - val placeInfos: List + val placeInfos: List, + val isEnd: Boolean ) diff --git a/app/src/main/java/org/sopt/teamdateroad/domain/repository/PlaceSearchRepository.kt b/app/src/main/java/org/sopt/teamdateroad/domain/repository/PlaceSearchRepository.kt index 0506f04..482b66c 100644 --- a/app/src/main/java/org/sopt/teamdateroad/domain/repository/PlaceSearchRepository.kt +++ b/app/src/main/java/org/sopt/teamdateroad/domain/repository/PlaceSearchRepository.kt @@ -1,9 +1,11 @@ package org.sopt.teamdateroad.domain.repository -import org.sopt.teamdateroad.domain.model.PlaceSearchResult +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import org.sopt.teamdateroad.domain.model.PlaceInfo interface PlaceSearchRepository { suspend fun getPlaceSearchResult( keyword: String - ): Result + ): Flow> } diff --git a/app/src/main/java/org/sopt/teamdateroad/domain/usecase/GetNearestTimelineUseCase.kt b/app/src/main/java/org/sopt/teamdateroad/domain/usecase/GetNearestTimelineUseCase.kt index a141ac9..edb4106 100644 --- a/app/src/main/java/org/sopt/teamdateroad/domain/usecase/GetNearestTimelineUseCase.kt +++ b/app/src/main/java/org/sopt/teamdateroad/domain/usecase/GetNearestTimelineUseCase.kt @@ -1,9 +1,11 @@ package org.sopt.teamdateroad.domain.usecase import javax.inject.Inject +import javax.inject.Singleton import org.sopt.teamdateroad.domain.model.NearestTimeline import org.sopt.teamdateroad.domain.repository.TimelineRepository +@Singleton class GetNearestTimelineUseCase @Inject constructor( private val timelineRepository: TimelineRepository ) { diff --git a/app/src/main/java/org/sopt/teamdateroad/domain/usecase/GetPlaceSearchResultUseCase.kt b/app/src/main/java/org/sopt/teamdateroad/domain/usecase/GetPlaceSearchResultUseCase.kt index 4866486..6614016 100644 --- a/app/src/main/java/org/sopt/teamdateroad/domain/usecase/GetPlaceSearchResultUseCase.kt +++ b/app/src/main/java/org/sopt/teamdateroad/domain/usecase/GetPlaceSearchResultUseCase.kt @@ -1,11 +1,15 @@ package org.sopt.teamdateroad.domain.usecase +import androidx.paging.PagingData import javax.inject.Inject -import org.sopt.teamdateroad.domain.model.PlaceSearchResult +import javax.inject.Singleton +import kotlinx.coroutines.flow.Flow +import org.sopt.teamdateroad.domain.model.PlaceInfo import org.sopt.teamdateroad.domain.repository.PlaceSearchRepository +@Singleton class GetPlaceSearchResultUseCase @Inject constructor( private val placeSearchRepository: PlaceSearchRepository ) { - suspend operator fun invoke(keyword: String): Result = placeSearchRepository.getPlaceSearchResult(keyword) + suspend operator fun invoke(keyword: String): Flow> = placeSearchRepository.getPlaceSearchResult(keyword) } diff --git a/app/src/main/java/org/sopt/teamdateroad/domain/usecase/GetTimelineDetailUseCase.kt b/app/src/main/java/org/sopt/teamdateroad/domain/usecase/GetTimelineDetailUseCase.kt index 26c389c..e096636 100644 --- a/app/src/main/java/org/sopt/teamdateroad/domain/usecase/GetTimelineDetailUseCase.kt +++ b/app/src/main/java/org/sopt/teamdateroad/domain/usecase/GetTimelineDetailUseCase.kt @@ -1,9 +1,11 @@ package org.sopt.teamdateroad.domain.usecase import javax.inject.Inject +import javax.inject.Singleton import org.sopt.teamdateroad.domain.model.TimelineDetail import org.sopt.teamdateroad.domain.repository.TimelineRepository +@Singleton class GetTimelineDetailUseCase @Inject constructor( private val timelineRepository: TimelineRepository ) { diff --git a/app/src/main/java/org/sopt/teamdateroad/domain/usecase/GetTimelinesUseCase.kt b/app/src/main/java/org/sopt/teamdateroad/domain/usecase/GetTimelinesUseCase.kt index 6ad2502..c7e6ea6 100644 --- a/app/src/main/java/org/sopt/teamdateroad/domain/usecase/GetTimelinesUseCase.kt +++ b/app/src/main/java/org/sopt/teamdateroad/domain/usecase/GetTimelinesUseCase.kt @@ -1,10 +1,12 @@ package org.sopt.teamdateroad.domain.usecase import javax.inject.Inject +import javax.inject.Singleton import org.sopt.teamdateroad.domain.model.Timeline import org.sopt.teamdateroad.domain.repository.TimelineRepository import org.sopt.teamdateroad.domain.type.TimelineTimeType +@Singleton class GetTimelinesUseCase @Inject constructor( private val timelineRepository: TimelineRepository ) { diff --git a/app/src/main/java/org/sopt/teamdateroad/domain/usecase/GetUserPointUseCase.kt b/app/src/main/java/org/sopt/teamdateroad/domain/usecase/GetUserPointUseCase.kt index 7898a84..0a73c6b 100644 --- a/app/src/main/java/org/sopt/teamdateroad/domain/usecase/GetUserPointUseCase.kt +++ b/app/src/main/java/org/sopt/teamdateroad/domain/usecase/GetUserPointUseCase.kt @@ -1,9 +1,11 @@ package org.sopt.teamdateroad.domain.usecase import javax.inject.Inject +import javax.inject.Singleton import org.sopt.teamdateroad.domain.model.UserPoint import org.sopt.teamdateroad.domain.repository.UserPointRepository +@Singleton class GetUserPointUseCase @Inject constructor( private val userPointRepository: UserPointRepository ) { diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/card/DateRoadPlaceCard.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/card/DateRoadPlaceCard.kt index 277edc6..58456d4 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/card/DateRoadPlaceCard.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/card/DateRoadPlaceCard.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width @@ -40,8 +41,8 @@ fun DateRoadPlaceCard( Row( modifier = modifier -// .fillMaxWidth() -// .height(76.dp) + .fillMaxWidth() + .height(76.dp) ) { Column( modifier = Modifier diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollContract.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollContract.kt index 3e5b010..28fc395 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollContract.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollContract.kt @@ -4,7 +4,6 @@ import org.sopt.teamdateroad.domain.model.CourseDetail import org.sopt.teamdateroad.domain.model.Enroll import org.sopt.teamdateroad.domain.model.Place import org.sopt.teamdateroad.domain.model.PlaceInfo -import org.sopt.teamdateroad.domain.model.PlaceSearchResult import org.sopt.teamdateroad.domain.model.TimelineDetail import org.sopt.teamdateroad.domain.type.RegionType import org.sopt.teamdateroad.presentation.type.EnrollScreenType @@ -57,10 +56,8 @@ class EnrollContract { val onRegionBottomSheetRegionSelected: RegionType? = RegionType.SEOUL, val onRegionBottomSheetAreaSelected: Any? = null, val isPlaceSearchBottomSheetOpen: Boolean = false, - val keyword: String = "", val place: Place = Place(), - val placeSearchResult: PlaceSearchResult = PlaceSearchResult(placeInfos = emptyList()), - val placeInfos: List = emptyList(), + val selectedPlaceInfos: List = emptyList(), val isPlaceEditable: Boolean = true, val isDurationBottomSheetOpen: Boolean = false, val durationPicker: List = listOf(Picker(items = (DURATION_START..DURATION_END).map { (it * 0.5).toString() })), diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt index 9bf5968..ddcba6c 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt @@ -29,8 +29,12 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle +import androidx.paging.PagingData +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems import java.time.LocalDate import java.time.format.DateTimeFormatter +import kotlinx.coroutines.flow.flowOf import org.sopt.teamdateroad.R import org.sopt.teamdateroad.domain.model.Place import org.sopt.teamdateroad.domain.model.PlaceInfo @@ -108,6 +112,9 @@ fun EnrollRoute( timelineId: Int? ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val searchKeyword by viewModel.searchKeyword.collectAsStateWithLifecycle("") + val searchPlaceInfos = viewModel.searchPlaceInfos.collectAsLazyPagingItems() + val lifecycleOwner = LocalLifecycleOwner.current val getGalleryLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? -> @@ -206,6 +213,8 @@ fun EnrollRoute( EnrollScreen( padding = padding, enrollUiState = uiState, + searchKeyword = searchKeyword, + searchPlaceInfos = searchPlaceInfos, onTopBarBackButtonClick = { viewModel.setEvent(EnrollContract.EnrollEvent.OnTopBarBackButtonClick) @@ -335,6 +344,8 @@ fun EnrollRoute( fun EnrollScreen( padding: PaddingValues, enrollUiState: EnrollContract.EnrollUiState = EnrollContract.EnrollUiState(), + searchKeyword: String, + searchPlaceInfos: LazyPagingItems, onTopBarBackButtonClick: () -> Unit, onTopBarLoadButtonClick: () -> Unit, onEnrollButtonClick: () -> Unit, @@ -445,6 +456,8 @@ fun EnrollScreen( EnrollScreenType.SECOND -> EnrollSecondScreen( enrollUiState = enrollUiState, + searchKeyword = searchKeyword, + searchPlaceInfos = searchPlaceInfos, onPlaceSearchButtonClick = onPlaceSearchButtonClick, onKeywordChanged = onKeywordChanged, onPlaceSelected = onPlaceSelected, @@ -572,6 +585,8 @@ fun EnrollScreenPreview() { enrollUiState = EnrollContract.EnrollUiState( loadState = LoadState.Success ), + searchKeyword = "", + searchPlaceInfos = flowOf(PagingData.empty()).collectAsLazyPagingItems(), onTopBarBackButtonClick = {}, onTopBarLoadButtonClick = {}, onEnrollButtonClick = {}, diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollSecondScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollSecondScreen.kt index 3ddba55..2ba0e2f 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollSecondScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollSecondScreen.kt @@ -31,13 +31,16 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex +import androidx.paging.PagingData +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch import org.sopt.teamdateroad.R import org.sopt.teamdateroad.domain.model.Place import org.sopt.teamdateroad.domain.model.PlaceInfo import org.sopt.teamdateroad.presentation.type.PlaceCardType -import org.sopt.teamdateroad.presentation.ui.component.bottomsheet.DateRoadPlaceSearchBottomSheet import org.sopt.teamdateroad.presentation.ui.component.button.DateRoadTextButton import org.sopt.teamdateroad.presentation.ui.component.card.DateRoadPlaceCard import org.sopt.teamdateroad.presentation.ui.enroll.component.EnrollPlaceInsertBar @@ -51,6 +54,8 @@ import org.sopt.teamdateroad.ui.theme.DateRoadTheme @Composable fun EnrollSecondScreen( enrollUiState: EnrollContract.EnrollUiState = EnrollContract.EnrollUiState(), + searchKeyword: String, + searchPlaceInfos: LazyPagingItems, onPlaceSearchButtonClick: () -> Unit, onKeywordChanged: (String) -> Unit, onPlaceSelected: (PlaceInfo) -> Unit, @@ -178,10 +183,10 @@ fun EnrollSecondScreen( Spacer(modifier = Modifier.height(16.dp)) } - DateRoadPlaceSearchBottomSheet( + PlaceSearchBottomSheet( isBottomSheetOpen = enrollUiState.isPlaceSearchBottomSheetOpen, - keyword = enrollUiState.keyword, - placeSearchResult = enrollUiState.placeSearchResult, + searchKeyword = searchKeyword, + searchPlaceInfos = searchPlaceInfos, onKeywordChanged = onKeywordChanged, onPlaceSelected = onPlaceSelected, onDismissRequest = onPlaceSearchBottomSheetDismiss @@ -193,6 +198,8 @@ fun EnrollSecondScreen( fun EnrollSecondScreenPreview() { DATEROADTheme { EnrollSecondScreen( + searchKeyword = "", + searchPlaceInfos = flowOf(PagingData.empty()).collectAsLazyPagingItems(), onPlaceSearchButtonClick = {}, onAddPlaceButtonClick = {}, onSelectedPlaceCourseTimeClick = {}, diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollViewModel.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollViewModel.kt index b04dfb2..49d4c05 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollViewModel.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollViewModel.kt @@ -1,12 +1,23 @@ package org.sopt.teamdateroad.presentation.ui.enroll import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData +import androidx.paging.cachedIn import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.launch import org.sopt.teamdateroad.data.dataremote.util.Date.NEAREST_DATE_START_OUTPUT_FORMAT import org.sopt.teamdateroad.data.mapper.toEntity.toEnroll -import org.sopt.teamdateroad.domain.model.PlaceSearchResult +import org.sopt.teamdateroad.domain.model.PlaceInfo import org.sopt.teamdateroad.domain.type.RegionType import org.sopt.teamdateroad.domain.usecase.GetCourseDetailUseCase import org.sopt.teamdateroad.domain.usecase.GetPlaceSearchResultUseCase @@ -32,6 +43,18 @@ class EnrollViewModel @Inject constructor( ) : BaseViewModel() { override fun createInitialState(): EnrollContract.EnrollUiState = EnrollContract.EnrollUiState() + private val _searchKeyword: MutableStateFlow = MutableStateFlow("") + val searchKeyword: StateFlow get() = _searchKeyword.asStateFlow() + + @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) + val searchPlaceInfos: Flow> = searchKeyword + .debounce(DEBOUNCE_TIME_MILLS) + .distinctUntilChanged() + .flatMapLatest { query -> + getPlaceSearchResultUseCase(query) + } + .cachedIn(viewModelScope) + override suspend fun handleEvent(event: EnrollContract.EnrollEvent) { when (event) { is EnrollContract.EnrollEvent.OnTopBarBackButtonClick -> { @@ -78,14 +101,13 @@ class EnrollViewModel @Inject constructor( is EnrollContract.EnrollEvent.OnTimeTextFieldClick -> setState { copy(isTimePickerBottomSheetOpen = true) } is EnrollContract.EnrollEvent.OnRegionTextFieldClick -> setState { copy(isRegionBottomSheetOpen = true, onRegionBottomSheetRegionSelected = RegionType.SEOUL, onRegionBottomSheetAreaSelected = null) } is EnrollContract.EnrollEvent.OnPlaceSearchButtonClick -> setState { copy(isPlaceSearchBottomSheetOpen = true) } - is EnrollContract.EnrollEvent.OnKeywordChanged -> { - setState { - copy(keyword = event.keyword) - } - getPlaceSearchResult() + is EnrollContract.EnrollEvent.OnKeywordChanged -> _searchKeyword.value = event.keyword + + is EnrollContract.EnrollEvent.OnPlaceSearchBottomSheetDismiss -> { + setState { copy(isPlaceSearchBottomSheetOpen = false) } + _searchKeyword.value = "" } - is EnrollContract.EnrollEvent.OnPlaceSearchBottomSheetDismiss -> setState { copy(isPlaceSearchBottomSheetOpen = false, keyword = "", placeSearchResult = PlaceSearchResult(emptyList())) } is EnrollContract.EnrollEvent.OnSelectedPlaceCourseTimeClick -> setState { copy(isDurationBottomSheetOpen = true) } is EnrollContract.EnrollEvent.OnDatePickerBottomSheetDismissRequest -> setState { copy(isDatePickerBottomSheetOpen = false) } is EnrollContract.EnrollEvent.OnTimePickerBottomSheetDismissRequest -> setState { copy(isTimePickerBottomSheetOpen = false) } @@ -107,11 +129,11 @@ class EnrollViewModel @Inject constructor( is EnrollContract.EnrollEvent.OnTitleValueChange -> setState { copy(enroll = currentState.enroll.copy(title = event.title)) } is EnrollContract.EnrollEvent.OnPlaceSelected -> { - setState { copy(keyword = "", placeSearchResult = PlaceSearchResult(emptyList()), place = currentState.place.copy(title = event.placeInfo.placeName, address = event.placeInfo.addressName), placeInfos = currentState.placeInfos + event.placeInfo, isPlaceSearchBottomSheetOpen = false) } + setState { copy(place = currentState.place.copy(title = event.placeInfo.placeName, address = event.placeInfo.addressName), selectedPlaceInfos = currentState.selectedPlaceInfos + event.placeInfo, isPlaceSearchBottomSheetOpen = false) } + _searchKeyword.value = "" } is EnrollContract.EnrollEvent.OnDatePickerBottomSheetButtonClick -> setState { copy(enroll = currentState.enroll.copy(date = event.date), isDatePickerBottomSheetOpen = false) } - is EnrollContract.EnrollEvent.OnTimePickerBottomSheetButtonClick -> setState { copy(enroll = currentState.enroll.copy(startAt = event.startAt), isTimePickerBottomSheetOpen = false) } is EnrollContract.EnrollEvent.OnDateChipClicked -> setState { copy( @@ -193,13 +215,7 @@ class EnrollViewModel @Inject constructor( } } - private fun getPlaceSearchResult() { - viewModelScope.launch { - getPlaceSearchResultUseCase(keyword = currentState.keyword).onSuccess { placeSearchResult -> - setState { copy(placeSearchResult = placeSearchResult) } - }.onFailure { - setEvent(EnrollContract.EnrollEvent.Enroll(loadState = LoadState.Error)) - } - } + companion object { + private const val DEBOUNCE_TIME_MILLS = 300L } } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadPlaceSearchBottomSheet.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/PlaceSearchBottomSheet.kt similarity index 86% rename from app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadPlaceSearchBottomSheet.kt rename to app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/PlaceSearchBottomSheet.kt index 5e22de8..8ce441b 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadPlaceSearchBottomSheet.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/PlaceSearchBottomSheet.kt @@ -1,4 +1,4 @@ -package org.sopt.teamdateroad.presentation.ui.component.bottomsheet +package org.sopt.teamdateroad.presentation.ui.enroll import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Button @@ -38,25 +37,28 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.paging.PagingData +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems import com.holix.android.bottomsheetdialog.compose.BottomSheetBehaviorProperties import com.holix.android.bottomsheetdialog.compose.BottomSheetDialog import com.holix.android.bottomsheetdialog.compose.BottomSheetDialogProperties +import kotlinx.coroutines.flow.flowOf import org.sopt.teamdateroad.R import org.sopt.teamdateroad.domain.model.PlaceInfo -import org.sopt.teamdateroad.domain.model.PlaceSearchResult import org.sopt.teamdateroad.presentation.ui.enroll.component.EnrollPlaceSearchItem import org.sopt.teamdateroad.presentation.util.modifier.noRippleClickable import org.sopt.teamdateroad.ui.theme.DATEROADTheme import org.sopt.teamdateroad.ui.theme.DateRoadTheme @Composable -fun DateRoadPlaceSearchBottomSheet( +fun PlaceSearchBottomSheet( isBottomSheetOpen: Boolean, - keyword: String, - placeSearchResult: PlaceSearchResult, + searchKeyword: String, + searchPlaceInfos: LazyPagingItems, onKeywordChanged: (String) -> Unit, onPlaceSelected: (PlaceInfo) -> Unit, - onDismissRequest: () -> Unit = {} + onDismissRequest: () -> Unit ) { if (isBottomSheetOpen) { BottomSheetDialog( @@ -105,7 +107,7 @@ fun DateRoadPlaceSearchBottomSheet( Spacer(modifier = Modifier.height(22.dp)) TextField( - value = keyword, + value = searchKeyword, onValueChange = onKeywordChanged, modifier = Modifier .fillMaxWidth() @@ -151,19 +153,20 @@ fun DateRoadPlaceSearchBottomSheet( Spacer(modifier = Modifier.height(10.dp)) - if (placeSearchResult.placeInfos.isNotEmpty()) { + if (searchPlaceInfos.itemCount > 0) { LazyColumn(modifier = Modifier.weight(1f)) { - itemsIndexed( - items = placeSearchResult.placeInfos, - key = { index, placeInfo -> placeInfo.hashCode() + index } - ) { index, placeInfo -> + items( + searchPlaceInfos.itemCount + ) { index -> + val placeInfo = searchPlaceInfos[index] ?: return@items + EnrollPlaceSearchItem( - keyword = keyword, + keyword = searchKeyword, placeInfo = placeInfo, onClick = { onPlaceSelected(placeInfo) } ) - if (index != placeSearchResult.placeInfos.lastIndex) { + if (index != searchPlaceInfos.itemCount - 1) { HorizontalDivider( modifier = Modifier.fillMaxWidth(), color = DateRoadTheme.colors.gray100, @@ -172,6 +175,7 @@ fun DateRoadPlaceSearchBottomSheet( } } } + Spacer(modifier = Modifier.height(12.dp)) } else { EmptyPlaceSearchResult() } @@ -207,10 +211,18 @@ private fun EmptyPlaceSearchResult() { @Preview @Composable -fun DateRoadPlaceSearchBottomSheetPreview() { +fun PlaceSearchBottomSheetPreview() { DATEROADTheme { var isBottomSheetOpen by rememberSaveable { mutableStateOf(false) } - var text by rememberSaveable { mutableStateOf("") } + var searchKeyword by rememberSaveable { mutableStateOf("") } + + val searchPlaceInfos = flowOf( + PagingData.from( + List(10) { + PlaceInfo("์นดํŽ˜ ๋‚˜๋ž‘", "๊ฒฝ๊ธฐ ์˜์™•์‹œ ์ฒญ๊ณ„๋กœ 217") + } + ) + ).collectAsLazyPagingItems() Button(onClick = { isBottomSheetOpen = true }) { Text( @@ -220,18 +232,11 @@ fun DateRoadPlaceSearchBottomSheetPreview() { ) } - DateRoadPlaceSearchBottomSheet( + PlaceSearchBottomSheet( isBottomSheetOpen = isBottomSheetOpen, - keyword = text, - placeSearchResult = PlaceSearchResult( - List(10) { - PlaceInfo( - "์นดํŽ˜ ๋‚˜๋ž‘", - "๊ฒฝ๊ธฐ ์˜์™•์‹œ ์ฒญ๊ณ„๋กœ 217" - ) - } - ), - onKeywordChanged = { text = it }, + searchKeyword = searchKeyword, + searchPlaceInfos = searchPlaceInfos, + onKeywordChanged = { searchKeyword = it }, onPlaceSelected = {}, onDismissRequest = { isBottomSheetOpen = !isBottomSheetOpen } ) @@ -247,3 +252,5 @@ fun EmptyPlaceSearchResultPreview() { } private const val BOTTOM_SHEET_OPEN_RATIO = 0.8f +const val PAGE_SIZE = 10 +const val MAX_SIZE = 50 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index def392c..9cc9af8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -25,7 +25,7 @@ composeBom = "2024.09.02" navigation = "2.8.1" security = "1.1.0-alpha06" lifecycleRuntimeComposeAndroid = "2.8.6" -pagingCommonAndroid = "3.3.2" +paging = "3.3.6" # Google googleServices = "4.4.2" @@ -76,7 +76,10 @@ androidx-material3 = { group = "androidx.compose.material3", name = "material3" androidx-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" } androidx-security = { group = "androidx.security", name = "security-crypto-ktx", version.ref = "security" } androidx-lifecycle-runtime-compose-android = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose-android", version.ref = "lifecycleRuntimeComposeAndroid" } -androidx-paging-common-android = { group = "androidx.paging", name = "paging-common-android", version.ref = "pagingCommonAndroid" } +androidx-paging-common-android = { group = "androidx.paging", name = "paging-common-android", version.ref = "paging" } +androidx-paging-runtime = { group = "androidx.paging", name = "paging-runtime", version.ref = "paging" } +android-paging-compose = { group = "androidx.paging", name = "paging-compose", version.ref = "paging" } + # Google google-services = { group = "com.google.gms", name = "google-services", version.ref = "googleServices" } @@ -175,6 +178,8 @@ kakao = [ pager = [ "androidx-paging-common-android", + "androidx-paging-runtime", + "android-paging-compose", "accompanist-pager", "accompanist-pager-indicators" ] From 65c816b852fb1bdf79717abe7ea7c4083d6d109b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=84=EC=9A=B0?= <85734140+jinuemong@users.noreply.github.com> Date: Mon, 7 Apr 2025 21:46:29 +0900 Subject: [PATCH 16/49] =?UTF-8?q?[feat]=20=ED=8F=AC=EC=9D=B8=ED=8A=B8=20?= =?UTF-8?q?=EB=A1=9C=EA=B9=85=20=EC=B6=94=EA=B0=80=20(#31)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: ๋กœ๊น… ์ถ”๊ฐ€ * feat: ์ฝ”์Šค ๋กœ๊น… ๋“ฑ๋ก --- .../ui/coursedetail/CourseDetailScreen.kt | 17 ++++++++------- .../ui/pointhistory/PointHistoryScreen.kt | 21 +++++++++++++------ .../presentation/util/Constraints.kt | 7 +++++++ 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailScreen.kt index abacfb2..46b6dd7 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailScreen.kt @@ -55,6 +55,7 @@ import org.sopt.teamdateroad.presentation.ui.coursedetail.component.CourseDetail import org.sopt.teamdateroad.presentation.ui.coursedetail.component.CourseDetailBottomBar import org.sopt.teamdateroad.presentation.ui.coursedetail.component.CourseDetailUnopenedDetail import org.sopt.teamdateroad.presentation.ui.coursedetail.component.courseDetailOpenedDetail +import org.sopt.teamdateroad.presentation.util.AdsAmplitude import org.sopt.teamdateroad.presentation.util.CourseDetail.POINT_LACK import org.sopt.teamdateroad.presentation.util.CourseDetailAmplitude.CLICK_COURSE_BACK import org.sopt.teamdateroad.presentation.util.CourseDetailAmplitude.CLICK_COURSE_PURCHASE @@ -174,13 +175,18 @@ fun CourseDetailRoute( viewModel.setEvent(CourseDetailContract.CourseDetailEvent.OnReportWebViewClicked) }, onReportWebViewClose = { viewModel.setEvent(CourseDetailContract.CourseDetailEvent.DismissReportWebView) }, - onSelectEnroll = { - viewModel.setSideEffect(CourseDetailContract.CourseDetailSideEffect.NavigateToEnroll(enrollType = EnrollType.COURSE, viewPath = COURSE_DETAIL, id = null)) - }, onDismissCollectPoint = { + AmplitudeUtils.trackEvent(eventName = AdsAmplitude.CLICK_COLLECT_POINT_CLOSE) viewModel.setEvent(CourseDetailContract.CourseDetailEvent.DismissDialogPointLack) }, + onSelectEnroll = { + AmplitudeUtils.trackEvent(eventName = AdsAmplitude.CLICK_COURSE) + viewModel.setEvent(CourseDetailContract.CourseDetailEvent.DismissDialogPointLack) + viewModel.setSideEffect(CourseDetailContract.CourseDetailSideEffect.NavigateToEnroll(enrollType = EnrollType.COURSE, viewPath = COURSE_DETAIL, id = null)) + }, onSelectAds = { + AmplitudeUtils.trackEvent(eventName = AdsAmplitude.CLICK_AD) + viewModel.setEvent(CourseDetailContract.CourseDetailEvent.DismissDialogPointLack) viewModel.setSideEffect(CourseDetailContract.CourseDetailSideEffect.NavigateToAds) }, onDismissFullAdsDialog = { @@ -403,11 +409,8 @@ fun CourseDetailScreen( DateRoadCollectPointType.WATCH_ADS -> onSelectAds() DateRoadCollectPointType.COURSE_REGISTRATION -> onSelectEnroll() } - onDismissCollectPoint() }, - onDismissRequest = { - onDismissCollectPoint() - } + onDismissRequest = onDismissCollectPoint ) DateRoadBasicBottomSheet( diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryScreen.kt index 20cdb86..63aaba2 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryScreen.kt @@ -48,7 +48,9 @@ import org.sopt.teamdateroad.presentation.ui.component.view.DateRoadIdleView import org.sopt.teamdateroad.presentation.ui.component.view.DateRoadLoadingView import org.sopt.teamdateroad.presentation.ui.pointhistory.component.PointHistoryCard import org.sopt.teamdateroad.presentation.ui.pointhistory.component.PointHistoryPointBox +import org.sopt.teamdateroad.presentation.util.AdsAmplitude import org.sopt.teamdateroad.presentation.util.ViewPath.POINT_HISTORY +import org.sopt.teamdateroad.presentation.util.amplitude.AmplitudeUtils import org.sopt.teamdateroad.presentation.util.view.LoadState import org.sopt.teamdateroad.ui.theme.DATEROADTheme import org.sopt.teamdateroad.ui.theme.DateRoadTheme @@ -68,6 +70,7 @@ fun PointHistoryRoute( LaunchedEffect(Unit) { viewModel.fetchPointHistory() viewModel.fetchUserPoint() + AmplitudeUtils.trackEvent(eventName = AdsAmplitude.VIEW_POINT) } LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { @@ -120,9 +123,16 @@ fun PointHistoryRoute( ) }, onTopBarIconClicked = { viewModel.setSideEffect(PointHistoryContract.PointHistorySideEffect.PopBackStack) }, - onClickCollectPoint = { viewModel.setEvent(PointHistoryContract.PointHistoryEvent.OnPointCollectBottomSheetClick) }, - onDisMissCollectPoint = { viewModel.setEvent(PointHistoryContract.PointHistoryEvent.OnPointCollectBottomSheetDismiss) }, + onClickCollectPoint = { + viewModel.setEvent(PointHistoryContract.PointHistoryEvent.OnPointCollectBottomSheetClick) + }, + onDisMissCollectPoint = { + AmplitudeUtils.trackEvent(eventName = AdsAmplitude.CLICK_COLLECT_POINT_CLOSE) + viewModel.setEvent(PointHistoryContract.PointHistoryEvent.OnPointCollectBottomSheetDismiss) + }, onSelectEnroll = { + AmplitudeUtils.trackEvent(eventName = AdsAmplitude.CLICK_COURSE) + viewModel.setEvent(PointHistoryContract.PointHistoryEvent.OnPointCollectBottomSheetDismiss) viewModel.setSideEffect( PointHistoryContract.PointHistorySideEffect.NavigateToEnroll( enrollType = EnrollType.COURSE, @@ -132,6 +142,8 @@ fun PointHistoryRoute( ) }, onSelectAds = { + AmplitudeUtils.trackEvent(eventName = AdsAmplitude.CLICK_AD) + viewModel.setEvent(PointHistoryContract.PointHistoryEvent.OnPointCollectBottomSheetDismiss) viewModel.setSideEffect(PointHistoryContract.PointHistorySideEffect.NavigateToAds) }, onDismissFullAdsDialog = { @@ -235,11 +247,8 @@ fun PointHistoryScreen( DateRoadCollectPointType.WATCH_ADS -> onSelectAds() DateRoadCollectPointType.COURSE_REGISTRATION -> onSelectEnroll() } - onDisMissCollectPoint() }, - onDismissRequest = { - onDisMissCollectPoint() - } + onDismissRequest = onDisMissCollectPoint ) } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/util/Constraints.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/util/Constraints.kt index 5fc1fe0..8706600 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/util/Constraints.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/util/Constraints.kt @@ -38,6 +38,13 @@ object DurationPicker { const val DURATION_END = 12 } +object AdsAmplitude { + const val VIEW_POINT = "view_point" + const val CLICK_AD = "click_ad" + const val CLICK_COURSE = "click_course" + const val CLICK_COLLECT_POINT_CLOSE = "click_collect_point_close" +} + object EnrollAmplitude { const val VIEW_ADD_SCHEDULE = "view_add_schedule" const val CLICK_SCHEDULE1_BACK = "click_schedule1_back" From 0584234c9f776e03f3379f6067c1bd9cd9d54941 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=84=EC=9A=B0?= <85734140+jinuemong@users.noreply.github.com> Date: Mon, 7 Apr 2025 21:46:41 +0900 Subject: [PATCH 17/49] =?UTF-8?q?fix=20:=20Date=20=EC=9D=B8=EB=8D=B1?= =?UTF-8?q?=EC=8A=A4=20=EC=88=98=EC=A0=95=20(#32)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/sopt/teamdateroad/presentation/util/Constraints.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/util/Constraints.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/util/Constraints.kt index 8706600..6278e40 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/util/Constraints.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/util/Constraints.kt @@ -18,8 +18,8 @@ object CourseDetailAmplitude { object DatePicker { const val DATE_PATTERN = "yyyy.MM.dd" const val YEAR_START = 2000 - const val YEAR_END = 2024 - const val YEAR_START_INDEX = 24 + const val YEAR_END = 2026 + const val YEAR_START_INDEX = 25 const val MONTH_START = 1 const val MONTH_END = 12 const val DAY_START = 1 From 387214d0b3ab18216ffd45bbe33bb6ab7621fda4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=84=EC=9A=B0?= <85734140+jinuemong@users.noreply.github.com> Date: Mon, 7 Apr 2025 21:47:03 +0900 Subject: [PATCH 18/49] =?UTF-8?q?[refactor]=20=EC=9E=A5=EC=86=8C=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20LazyPaging=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20(#34)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor : collect ๋กœ์ง ๊ฐœ์„  * style: ์ฝ”๋“œ ํฌ๋งท ์ˆ˜์ • & ๋ฆฐํŠธ ์ฒดํฌ --- .../PlaceSearchDataSourceImpl.kt | 4 +-- .../presentation/ui/enroll/EnrollScreen.kt | 9 ++---- .../ui/enroll/EnrollSecondScreen.kt | 9 ++---- .../{ => component}/PlaceSearchBottomSheet.kt | 31 ++++++------------- 4 files changed, 18 insertions(+), 35 deletions(-) rename app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/{ => component}/PlaceSearchBottomSheet.kt (90%) diff --git a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/datasourceimpl/PlaceSearchDataSourceImpl.kt b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/datasourceimpl/PlaceSearchDataSourceImpl.kt index 06ba3f9..f519581 100644 --- a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/datasourceimpl/PlaceSearchDataSourceImpl.kt +++ b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/datasourceimpl/PlaceSearchDataSourceImpl.kt @@ -10,8 +10,8 @@ import kotlinx.coroutines.flow.Flow import org.sopt.teamdateroad.data.dataremote.datasource.PlaceSearchDataSource import org.sopt.teamdateroad.data.dataremote.model.response.ResponsePlaceInfoDto import org.sopt.teamdateroad.data.dataremote.service.PlaceSearchService -import org.sopt.teamdateroad.presentation.ui.enroll.MAX_SIZE -import org.sopt.teamdateroad.presentation.ui.enroll.PAGE_SIZE +import org.sopt.teamdateroad.presentation.ui.enroll.component.MAX_SIZE +import org.sopt.teamdateroad.presentation.ui.enroll.component.PAGE_SIZE class PlaceSearchDataSourceImpl @Inject constructor(private val placeSearchService: PlaceSearchService) : PlaceSearchDataSource { override suspend fun getPlaceSearchResult(keyword: String): Flow> { diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt index ddcba6c..9f16532 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt @@ -29,12 +29,9 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle -import androidx.paging.PagingData -import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems import java.time.LocalDate import java.time.format.DateTimeFormatter -import kotlinx.coroutines.flow.flowOf import org.sopt.teamdateroad.R import org.sopt.teamdateroad.domain.model.Place import org.sopt.teamdateroad.domain.model.PlaceInfo @@ -214,7 +211,7 @@ fun EnrollRoute( padding = padding, enrollUiState = uiState, searchKeyword = searchKeyword, - searchPlaceInfos = searchPlaceInfos, + searchPlaceInfos = searchPlaceInfos.itemSnapshotList.items, onTopBarBackButtonClick = { viewModel.setEvent(EnrollContract.EnrollEvent.OnTopBarBackButtonClick) @@ -345,7 +342,7 @@ fun EnrollScreen( padding: PaddingValues, enrollUiState: EnrollContract.EnrollUiState = EnrollContract.EnrollUiState(), searchKeyword: String, - searchPlaceInfos: LazyPagingItems, + searchPlaceInfos: List, onTopBarBackButtonClick: () -> Unit, onTopBarLoadButtonClick: () -> Unit, onEnrollButtonClick: () -> Unit, @@ -586,7 +583,7 @@ fun EnrollScreenPreview() { loadState = LoadState.Success ), searchKeyword = "", - searchPlaceInfos = flowOf(PagingData.empty()).collectAsLazyPagingItems(), + searchPlaceInfos = listOf(), onTopBarBackButtonClick = {}, onTopBarLoadButtonClick = {}, onEnrollButtonClick = {}, diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollSecondScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollSecondScreen.kt index 2ba0e2f..d31b3c5 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollSecondScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollSecondScreen.kt @@ -31,11 +31,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex -import androidx.paging.PagingData -import androidx.paging.compose.LazyPagingItems -import androidx.paging.compose.collectAsLazyPagingItems import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch import org.sopt.teamdateroad.R import org.sopt.teamdateroad.domain.model.Place @@ -44,6 +40,7 @@ import org.sopt.teamdateroad.presentation.type.PlaceCardType import org.sopt.teamdateroad.presentation.ui.component.button.DateRoadTextButton import org.sopt.teamdateroad.presentation.ui.component.card.DateRoadPlaceCard import org.sopt.teamdateroad.presentation.ui.enroll.component.EnrollPlaceInsertBar +import org.sopt.teamdateroad.presentation.ui.enroll.component.PlaceSearchBottomSheet import org.sopt.teamdateroad.presentation.util.Time import org.sopt.teamdateroad.presentation.util.draganddrop.rememberDragAndDropListState import org.sopt.teamdateroad.presentation.util.mutablelist.move @@ -55,7 +52,7 @@ import org.sopt.teamdateroad.ui.theme.DateRoadTheme fun EnrollSecondScreen( enrollUiState: EnrollContract.EnrollUiState = EnrollContract.EnrollUiState(), searchKeyword: String, - searchPlaceInfos: LazyPagingItems, + searchPlaceInfos: List, onPlaceSearchButtonClick: () -> Unit, onKeywordChanged: (String) -> Unit, onPlaceSelected: (PlaceInfo) -> Unit, @@ -199,7 +196,7 @@ fun EnrollSecondScreenPreview() { DATEROADTheme { EnrollSecondScreen( searchKeyword = "", - searchPlaceInfos = flowOf(PagingData.empty()).collectAsLazyPagingItems(), + searchPlaceInfos = listOf(), onPlaceSearchButtonClick = {}, onAddPlaceButtonClick = {}, onSelectedPlaceCourseTimeClick = {}, diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/PlaceSearchBottomSheet.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/component/PlaceSearchBottomSheet.kt similarity index 90% rename from app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/PlaceSearchBottomSheet.kt rename to app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/component/PlaceSearchBottomSheet.kt index 8ce441b..6dcb28a 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/PlaceSearchBottomSheet.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/component/PlaceSearchBottomSheet.kt @@ -1,4 +1,4 @@ -package org.sopt.teamdateroad.presentation.ui.enroll +package org.sopt.teamdateroad.presentation.ui.enroll.component import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Button @@ -37,16 +38,11 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.paging.PagingData -import androidx.paging.compose.LazyPagingItems -import androidx.paging.compose.collectAsLazyPagingItems import com.holix.android.bottomsheetdialog.compose.BottomSheetBehaviorProperties import com.holix.android.bottomsheetdialog.compose.BottomSheetDialog import com.holix.android.bottomsheetdialog.compose.BottomSheetDialogProperties -import kotlinx.coroutines.flow.flowOf import org.sopt.teamdateroad.R import org.sopt.teamdateroad.domain.model.PlaceInfo -import org.sopt.teamdateroad.presentation.ui.enroll.component.EnrollPlaceSearchItem import org.sopt.teamdateroad.presentation.util.modifier.noRippleClickable import org.sopt.teamdateroad.ui.theme.DATEROADTheme import org.sopt.teamdateroad.ui.theme.DateRoadTheme @@ -55,7 +51,7 @@ import org.sopt.teamdateroad.ui.theme.DateRoadTheme fun PlaceSearchBottomSheet( isBottomSheetOpen: Boolean, searchKeyword: String, - searchPlaceInfos: LazyPagingItems, + searchPlaceInfos: List, onKeywordChanged: (String) -> Unit, onPlaceSelected: (PlaceInfo) -> Unit, onDismissRequest: () -> Unit @@ -153,20 +149,16 @@ fun PlaceSearchBottomSheet( Spacer(modifier = Modifier.height(10.dp)) - if (searchPlaceInfos.itemCount > 0) { + if (searchPlaceInfos.isNotEmpty()) { LazyColumn(modifier = Modifier.weight(1f)) { - items( - searchPlaceInfos.itemCount - ) { index -> - val placeInfo = searchPlaceInfos[index] ?: return@items - + itemsIndexed(searchPlaceInfos) { index: Int, placeInfo: PlaceInfo -> EnrollPlaceSearchItem( keyword = searchKeyword, placeInfo = placeInfo, onClick = { onPlaceSelected(placeInfo) } ) - if (index != searchPlaceInfos.itemCount - 1) { + if (index != searchPlaceInfos.lastIndex) { HorizontalDivider( modifier = Modifier.fillMaxWidth(), color = DateRoadTheme.colors.gray100, @@ -216,13 +208,10 @@ fun PlaceSearchBottomSheetPreview() { var isBottomSheetOpen by rememberSaveable { mutableStateOf(false) } var searchKeyword by rememberSaveable { mutableStateOf("") } - val searchPlaceInfos = flowOf( - PagingData.from( - List(10) { - PlaceInfo("์นดํŽ˜ ๋‚˜๋ž‘", "๊ฒฝ๊ธฐ ์˜์™•์‹œ ์ฒญ๊ณ„๋กœ 217") - } - ) - ).collectAsLazyPagingItems() + val searchPlaceInfos = + List(10) { + PlaceInfo("์นดํŽ˜ ๋‚˜๋ž‘", "๊ฒฝ๊ธฐ ์˜์™•์‹œ ์ฒญ๊ณ„๋กœ 217") + } Button(onClick = { isBottomSheetOpen = true }) { Text( From b630cbd50610e1225149550bd988b306f4b76d3b Mon Sep 17 00:00:00 2001 From: Yaechan Park <102402485+dpcks0509@users.noreply.github.com> Date: Mon, 7 Apr 2025 21:47:14 +0900 Subject: [PATCH 19/49] =?UTF-8?q?fix:=20=EC=9E=A5=EC=86=8C=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=ED=82=A4=EB=B3=B4=EB=93=9C,=20=EB=B0=94=ED=85=80?= =?UTF-8?q?=EC=8B=9C=ED=8A=B8=20=EC=95=A0=EB=8B=88=EB=A9=94=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EA=B2=B9=EC=B9=A8=20=ED=95=B4=EA=B2=B0=20(#36)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/enroll/EnrollScreen.kt | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt index 9f16532..7204ae6 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt @@ -22,6 +22,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -113,6 +114,7 @@ fun EnrollRoute( val searchPlaceInfos = viewModel.searchPlaceInfos.collectAsLazyPagingItems() val lifecycleOwner = LocalLifecycleOwner.current + val keyboardController = LocalSoftwareKeyboardController.current val getGalleryLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? -> if (uri != null) viewModel.setEvent(EnrollContract.EnrollEvent.SetImage(images = listOf(uri.toString()))) @@ -257,9 +259,18 @@ fun EnrollRoute( AmplitudeUtils.trackEvent(eventName = CLICK_BRING_COURSE) }, onEnrollButtonClick = { viewModel.setEvent(EnrollContract.EnrollEvent.OnEnrollButtonClick) }, - onDateTextFieldClick = { viewModel.setEvent(EnrollContract.EnrollEvent.OnDateTextFieldClick) }, - onTimeTextFieldClick = { viewModel.setEvent(EnrollContract.EnrollEvent.OnTimeTextFieldClick) }, - onRegionTextFieldClick = { viewModel.setEvent(EnrollContract.EnrollEvent.OnRegionTextFieldClick) }, + onDateTextFieldClick = { + keyboardController?.hide() + viewModel.setEvent(EnrollContract.EnrollEvent.OnDateTextFieldClick) + }, + onTimeTextFieldClick = { + keyboardController?.hide() + viewModel.setEvent(EnrollContract.EnrollEvent.OnTimeTextFieldClick) + }, + onRegionTextFieldClick = { + keyboardController?.hide() + viewModel.setEvent(EnrollContract.EnrollEvent.OnRegionTextFieldClick) + }, onPlaceSearchButtonClick = { viewModel.setEvent(EnrollContract.EnrollEvent.OnPlaceSearchButtonClick) }, onKeywordChanged = { keyword -> viewModel.setEvent(EnrollContract.EnrollEvent.OnKeywordChanged(keyword = keyword)) }, onPlaceSelected = { placeInfo -> viewModel.setEvent(EnrollContract.EnrollEvent.OnPlaceSelected(placeInfo = placeInfo)) }, From 244332c353f3a8c8fd89838e4bbd26b362be590c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=84=EC=9A=B0?= <85734140+jinuemong@users.noreply.github.com> Date: Mon, 7 Apr 2025 23:14:30 +0900 Subject: [PATCH 20/49] [release] 1.0.1 (#39) * feat : version up * feat : version up --- .github/workflows/android_cd.yml | 8 ++++---- .github/workflows/android_ci.yml | 6 +++--- gradle/libs.versions.toml | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/android_cd.yml b/.github/workflows/android_cd.yml index a5910a1..6cd02cf 100644 --- a/.github/workflows/android_cd.yml +++ b/.github/workflows/android_cd.yml @@ -16,10 +16,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Gradle cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ~/.gradle/caches @@ -29,7 +29,7 @@ jobs: ${{ runner.os }}-gradle- - name: set up JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 17 distribution: 'temurin' @@ -66,7 +66,7 @@ jobs: ./gradlew :app:assembleRelease - name: Upload Release APK - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: release path: ./app/build/outputs/apk/release/app-release-unsigned.apk diff --git a/.github/workflows/android_ci.yml b/.github/workflows/android_ci.yml index acd2f8a..b24c778 100644 --- a/.github/workflows/android_ci.yml +++ b/.github/workflows/android_ci.yml @@ -14,10 +14,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Gradle cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ~/.gradle/caches @@ -27,7 +27,7 @@ jobs: ${{ runner.os }}-gradle- - name: set up JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 17 distribution: 'temurin' diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9cc9af8..fdb3cce 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ compileSdk = "34" minSdk = "28" targetSdk = "34" versionCode = "3" -versionName = "1.0.0" +versionName = "1.0.1" jvmTarget = "1.8" kotlinCompilerExtensionVersion = "1.5.13" From fc2561522194e530f6d600030226a6e6f9e86340 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=8C=E1=85=B5=E1=86=AB?= =?UTF-8?q?=E1=84=8B=E1=85=AE?= Date: Tue, 8 Apr 2025 23:26:51 +0900 Subject: [PATCH 21/49] =?UTF-8?q?feat=20:=20=EB=B9=8C=EB=93=9C=20=ED=82=A4?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/android_cd.yml | 40 +++++++++++++++++++++++--------- app/build.gradle.kts | 10 ++++++++ 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/.github/workflows/android_cd.yml b/.github/workflows/android_cd.yml index 6cd02cf..01f41b0 100644 --- a/.github/workflows/android_cd.yml +++ b/.github/workflows/android_cd.yml @@ -28,7 +28,7 @@ jobs: restore-keys: | ${{ runner.os }}-gradle- - - name: set up JDK 17 + - name: Set up JDK 17 uses: actions/setup-java@v4 with: java-version: 17 @@ -43,7 +43,7 @@ jobs: - name: Decode google-services.json env: FIREBASE_SECRET: ${{ secrets.FIREBASE_SECRET }} - run: echo $FIREBASE_SECRET > app/google-services.json + run: echo "$FIREBASE_SECRET" > app/google-services.json - name: Access local properties env: @@ -54,22 +54,39 @@ jobs: GOOGLE_ADS_API_ID: ${{ secrets.GOOGLE_ADS_API_ID }} GOOGLE_ADS_API_ID_MANIFEST: ${{ secrets.GOOGLE_ADS_API_ID }} run: | - echo "dev.base.url=\"$BASE_URL\"" >> local.properties + echo "dev.base.url=\"$HFM_BASE_URL\"" >> local.properties echo "kakao.native.app.key.manifest=\"$KAKAO_NATIVE_APP_KEY_MANIFEST\"" >> local.properties echo "kakao.native.app.key=\"$KAKAO_NATIVE_APP_KEY\"" >> local.properties echo "amplitude.dev.api.key=\"$AMPLITUDE_API_KEY\"" >> local.properties - echo "google.ads.api.id=\"GOOGLE_ADS_API_ID\"" >> local.properties - echo "google.ads.api.id.manifest=\"GOOGLE_ADS_API_ID_MANIFEST\"" >> local.properties + echo "google.ads.api.id=\"$GOOGLE_ADS_API_ID\"" >> local.properties + echo "google.ads.api.id.manifest=\"$GOOGLE_ADS_API_ID_MANIFEST\"" >> local.properties - - name: Build Release APK + - name: Decode Keystore + run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > dateroad.keystore + + - name: Export signing environment run: | - ./gradlew :app:assembleRelease + echo "STORE_PASSWORD=${{ secrets.KEYSTORE_PASSWORD }}" >> $GITHUB_ENV + echo "KEY_ALIAS=${{ secrets.KEY_ALIAS }}" >> $GITHUB_ENV + echo "KEY_PASSWORD=${{ secrets.KEY_PASSWORD }}" >> $GITHUB_ENV + + - name: Build Signed Release APK + run: ./gradlew :app:assembleRelease + + - name: Upload Signed APK + uses: actions/upload-artifact@v4 + with: + name: release-apk + path: ./app/build/outputs/apk/release/app-release.apk + + - name: Build Signed Release AAB + run: ./gradlew :app:bundleRelease - - name: Upload Release APK + - name: Upload Signed AAB uses: actions/upload-artifact@v4 with: - name: release - path: ./app/build/outputs/apk/release/app-release-unsigned.apk + name: release-aab + path: ./app/build/outputs/bundle/release/app-release.aab - name: Discord Notify - Success if: ${{ success() }} @@ -81,7 +98,8 @@ jobs: username: DATEROAD-ANDROID ๐Ÿซ content: | Release Test๊ฐ€ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค! - [โ‡๏ธ APK๋ฅผ ๋‹ค์šด๋กœ๋“œํ•ด ๋ณด์„ธ์š”! โ‡๏ธ](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) + [โ‡๏ธ APK ๋‹ค์šด๋กœ๋“œ](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) + [๐Ÿ“ฆ AAB ๋‹ค์šด๋กœ๋“œ](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) - name: Discord Notify - Failure if: ${{ failure() }} diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 75eca75..264b651 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -37,6 +37,14 @@ android { manifestPlaceholders["KAKAO_NATIVE_APP_KEY_MANIFEST"] = properties["kakao.native.app.key.manifest"] as String manifestPlaceholders["GOOGLE_ADS_API_ID_MANIFEST"] = properties["google.ads.api.id.manifest"] as String } + signingConfigs { + create("release") { + storeFile = file("dateroad.keystore") + storePassword = System.getenv("STORE_PASSWORD") + keyAlias = System.getenv("KEY_ALIAS") + keyPassword = System.getenv("KEY_PASSWORD") + } + } buildTypes { debug { @@ -47,6 +55,7 @@ android { } release { + signingConfig = signingConfigs.getByName("release") isMinifyEnabled = true isShrinkResources = true buildConfigField("String", "BASE_URL", properties["prod.base.url"].toString()) @@ -58,6 +67,7 @@ android { ) } } + compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 From c98c937844722e7e7ff0b8d9c8480878ce981053 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=8C=E1=85=B5=E1=86=AB?= =?UTF-8?q?=E1=84=8B=E1=85=AE?= Date: Sun, 23 Mar 2025 20:04:17 +0900 Subject: [PATCH 22/49] _ --- .github/workflows/android_cd.yml | 12 ++++-------- .github/workflows/android_ci.yml | 14 +++----------- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/.github/workflows/android_cd.yml b/.github/workflows/android_cd.yml index 6cd02cf..7dd3ff4 100644 --- a/.github/workflows/android_cd.yml +++ b/.github/workflows/android_cd.yml @@ -16,10 +16,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v3 - name: Gradle cache - uses: actions/cache@v4 + uses: actions/cache@v3 with: path: | ~/.gradle/caches @@ -29,7 +29,7 @@ jobs: ${{ runner.os }}-gradle- - name: set up JDK 17 - uses: actions/setup-java@v4 + uses: actions/setup-java@v3 with: java-version: 17 distribution: 'temurin' @@ -51,22 +51,18 @@ jobs: KAKAO_NATIVE_APP_KEY_MANIFEST: ${{ secrets.KAKAO_NATIVE_APP_KEY }} KAKAO_NATIVE_APP_KEY: ${{ secrets.KAKAO_NATIVE_APP_KEY }} AMPLITUDE_API_KEY: ${{ secrets.AMPLITUDE_API_KEY }} - GOOGLE_ADS_API_ID: ${{ secrets.GOOGLE_ADS_API_ID }} - GOOGLE_ADS_API_ID_MANIFEST: ${{ secrets.GOOGLE_ADS_API_ID }} run: | echo "dev.base.url=\"$BASE_URL\"" >> local.properties echo "kakao.native.app.key.manifest=\"$KAKAO_NATIVE_APP_KEY_MANIFEST\"" >> local.properties echo "kakao.native.app.key=\"$KAKAO_NATIVE_APP_KEY\"" >> local.properties echo "amplitude.dev.api.key=\"$AMPLITUDE_API_KEY\"" >> local.properties - echo "google.ads.api.id=\"GOOGLE_ADS_API_ID\"" >> local.properties - echo "google.ads.api.id.manifest=\"GOOGLE_ADS_API_ID_MANIFEST\"" >> local.properties - name: Build Release APK run: | ./gradlew :app:assembleRelease - name: Upload Release APK - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v3 with: name: release path: ./app/build/outputs/apk/release/app-release-unsigned.apk diff --git a/.github/workflows/android_ci.yml b/.github/workflows/android_ci.yml index b24c778..4a6a626 100644 --- a/.github/workflows/android_ci.yml +++ b/.github/workflows/android_ci.yml @@ -14,10 +14,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v3 - name: Gradle cache - uses: actions/cache@v4 + uses: actions/cache@v3 with: path: | ~/.gradle/caches @@ -27,7 +27,7 @@ jobs: ${{ runner.os }}-gradle- - name: set up JDK 17 - uses: actions/setup-java@v4 + uses: actions/setup-java@v3 with: java-version: 17 distribution: 'temurin' @@ -49,22 +49,14 @@ jobs: - name: Access local properties env: HFM_BASE_URL: ${{ secrets.BASE_URL }} - KAKAO_BASE_URL: ${{ secrets.KAKAO_BASE_URL }} KAKAO_NATIVE_APP_KEY_MANIFEST: ${{ secrets.KAKAO_NATIVE_APP_KEY }} KAKAO_NATIVE_APP_KEY: ${{ secrets.KAKAO_NATIVE_APP_KEY }} - KAKAO_REST_API_KEY: ${{ secrets.KAKAO_REST_API_KEY }} AMPLITUDE_API_KEY: ${{ secrets.AMPLITUDE_API_KEY }} - GOOGLE_ADS_API_ID: ${{ secrets.GOOGLE_ADS_API_ID }} - GOOGLE_ADS_API_ID_MANIFEST: ${{ secrets.GOOGLE_ADS_API_ID }} run: | echo "dev.base.url=\"$BASE_URL\"" >> local.properties - echo "kakao.base.url=\"$KAKAO_BASE_URL\"" >> local.properties echo "kakao.native.app.key.manifest=\"$KAKAO_NATIVE_APP_KEY_MANIFEST\"" >> local.properties echo "kakao.native.app.key=\"$KAKAO_NATIVE_APP_KEY\"" >> local.properties - echo "kakao.rest.api.key=\"$KAKAO_REST_API_KEY\"" >> local.properties echo "amplitude.dev.api.key=\"$AMPLITUDE_API_KEY\"" >> local.properties - echo "google.ads.api.id=\"GOOGLE_ADS_API_ID\"" >> local.properties - echo "google.ads.api.id.manifest=\"GOOGLE_ADS_API_ID_MANIFEST\"" >> local.properties - name: Lint Check run: ./gradlew ktlintCheck -PcompileSdkVersion=34 From 2f08725e9454d6c977cc228cf9b6b5450f655384 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=8C=E1=85=B5=E1=86=AB?= =?UTF-8?q?=E1=84=8B=E1=85=AE?= Date: Sun, 23 Mar 2025 20:07:14 +0900 Subject: [PATCH 23/49] _ --- app/build.gradle.kts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 75eca75..66b1ff2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -9,6 +9,7 @@ plugins { alias(libs.plugins.google.firebase.crashlytics) alias(libs.plugins.dagger.hilt) alias(libs.plugins.ktlint) + alias(libs.plugins.sentry) } val properties = Properties().apply { @@ -32,17 +33,14 @@ android { } buildConfigField("String", "KAKAO_NATIVE_APP_KEY", properties["kakao.native.app.key"].toString()) - buildConfigField("String", "KAKAO_REST_API_KEY", properties["kakao.rest.api.key"].toString()) - buildConfigField("String", "GOOGLE_ADS_API_ID", properties["google.ads.api.id"].toString()) + manifestPlaceholders["KAKAO_NATIVE_APP_KEY_MANIFEST"] = properties["kakao.native.app.key.manifest"] as String - manifestPlaceholders["GOOGLE_ADS_API_ID_MANIFEST"] = properties["google.ads.api.id.manifest"] as String } buildTypes { debug { isMinifyEnabled = false buildConfigField("String", "BASE_URL", properties["dev.base.url"].toString()) - buildConfigField("String", "KAKAO_BASE_URL", properties["kakao.base.url"].toString()) buildConfigField("String", "AMPLITUDE_API_KEY", properties["amplitude.dev.api.key"].toString()) } @@ -50,7 +48,6 @@ android { isMinifyEnabled = true isShrinkResources = true buildConfigField("String", "BASE_URL", properties["prod.base.url"].toString()) - buildConfigField("String", "KAKAO_BASE_URL", properties["kakao.base.url"].toString()) buildConfigField("String", "AMPLITUDE_API_KEY", properties["amplitude.prod.api.key"].toString()) proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), @@ -97,7 +94,6 @@ dependencies { implementation(platform(libs.google.firebase.bom)) implementation(libs.google.firebase.crashlytics) implementation(libs.firebase.crashlytics.buildtools) - implementation(libs.google.gms.ads) // Network implementation(platform(libs.okhttp.bom)) @@ -129,9 +125,6 @@ dependencies { // Lottie implementation(libs.lottie.compose) - - // BottomSheet - implementation(libs.bottom.sheet) } ktlint { From e0d266812ffc6f1f117302dfef40099db591ff10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=8C=E1=85=B5=E1=86=AB?= =?UTF-8?q?=E1=84=8B=E1=85=AE?= Date: Sun, 23 Mar 2025 20:10:47 +0900 Subject: [PATCH 24/49] _ --- app/src/main/AndroidManifest.xml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 96ffc14..e75d9c2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -18,10 +18,6 @@ android:usesCleartextTraffic="true" tools:targetApi="31"> - - Date: Sun, 23 Mar 2025 20:35:50 +0900 Subject: [PATCH 25/49] test: ci/cd --- app/build.gradle.kts | 1 - 1 file changed, 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 66b1ff2..dbfe070 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -9,7 +9,6 @@ plugins { alias(libs.plugins.google.firebase.crashlytics) alias(libs.plugins.dagger.hilt) alias(libs.plugins.ktlint) - alias(libs.plugins.sentry) } val properties = Properties().apply { From 579b25947023f213575ad873c5ff7fa46717a529 Mon Sep 17 00:00:00 2001 From: Yaechan Park <102402485+dpcks0509@users.noreply.github.com> Date: Tue, 25 Mar 2025 23:34:58 +0900 Subject: [PATCH 26/49] =?UTF-8?q?[feat]=20KaKao=20=EC=9E=A5=EC=86=8C=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20API=20=EC=97=B0=EA=B2=B0=20(#9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: PlaceSearchResult, PlaceInfo์˜ Model, Response, Mapper ์ƒ์„ฑ * feat: ์นด์นด์˜ค ์žฅ์†Œ๊ฒ€์ƒ‰ API ์ดˆ๊ธฐ ์„ธํŒ… * fix: ์นด์นด์˜ค ์žฅ์†Œ ๊ฒ€์ƒ‰ ์˜ค๋ฅ˜ ์ˆ˜์ • * refactor: ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜ํ˜• Result๋กœ ๊ฐ์‹ธ๊ธฐ * feat: GetPlaceSearchResultUseCase ๊ตฌํ˜„ * refactor: PlaceSearch Constraints ๋ณ€์ˆ˜๋ช… ์ˆ˜์ • * refactor: ktlint ์ ์šฉ * refactor: GetPlaceSearchResultUseCase ๊ตฌํ˜„ * feat: ci ํŒŒ์ผ์— kako_base_url, kakao_rest_api_key ์ถ”๊ฐ€ * refactor: GetPlaceSearchResultUseCase ๊ตฌํ˜„ * feat: buildConfig KAKAO_BASE_URL ์ถ”๊ฐ€ * refactor: providesPlaceSearchOkHttpClient ์‹ฑ๊ธ€ํ†ค ์œ ์ง€๋˜๊ฒŒ ์ˆ˜์ • * refactor: ๋ณ€์ˆ˜๋ช… address_name -> addressName์œผ๋กœ ์ˆ˜์ • * refactor: ktlint ์ˆ˜์ • * refactor: OkHttpClient Qualifier ์ถ”๊ฐ€ --- .github/workflows/android_ci.yml | 4 ++ app/build.gradle.kts | 3 + .../datasource/PlaceSearchDataSource.kt | 6 +- .../PlaceSearchDataSourceImpl.kt | 59 +------------------ .../response/ResponsePlaceSearchResultDto.kt | 3 - .../dataremote/service/PlaceSearchService.kt | 4 +- .../data/dataremote/util/Constraints.kt | 6 +- .../todomain/ResponsePlaceSearchDtoMapper.kt | 3 +- .../PlaceSearchRepositoryImpl.kt | 14 ++--- .../domain/model/PlaceSearchResult.kt | 3 +- .../repository/PlaceSearchRepository.kt | 6 +- .../usecase/GetPlaceSearchResultUseCase.kt | 8 +-- 12 files changed, 25 insertions(+), 94 deletions(-) diff --git a/.github/workflows/android_ci.yml b/.github/workflows/android_ci.yml index 4a6a626..4c764ee 100644 --- a/.github/workflows/android_ci.yml +++ b/.github/workflows/android_ci.yml @@ -49,13 +49,17 @@ jobs: - name: Access local properties env: HFM_BASE_URL: ${{ secrets.BASE_URL }} + KAKAO_BASE_URL: ${{ secrets.KAKAO_BASE_URL }} KAKAO_NATIVE_APP_KEY_MANIFEST: ${{ secrets.KAKAO_NATIVE_APP_KEY }} KAKAO_NATIVE_APP_KEY: ${{ secrets.KAKAO_NATIVE_APP_KEY }} + KAKAO_REST_API_KEY: ${{ secrets.KAKAO_REST_API_KEY }} AMPLITUDE_API_KEY: ${{ secrets.AMPLITUDE_API_KEY }} run: | echo "dev.base.url=\"$BASE_URL\"" >> local.properties + echo "kakao.base.url=\"$KAKAO_BASE_URL\"" >> local.properties echo "kakao.native.app.key.manifest=\"$KAKAO_NATIVE_APP_KEY_MANIFEST\"" >> local.properties echo "kakao.native.app.key=\"$KAKAO_NATIVE_APP_KEY\"" >> local.properties + echo "kakao.rest.api.key=\"$KAKAO_REST_API_KEY\"" >> local.properties echo "amplitude.dev.api.key=\"$AMPLITUDE_API_KEY\"" >> local.properties - name: Lint Check diff --git a/app/build.gradle.kts b/app/build.gradle.kts index dbfe070..2832b3f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -32,6 +32,7 @@ android { } buildConfigField("String", "KAKAO_NATIVE_APP_KEY", properties["kakao.native.app.key"].toString()) + buildConfigField("String", "KAKAO_REST_API_KEY", properties["kakao.rest.api.key"].toString()) manifestPlaceholders["KAKAO_NATIVE_APP_KEY_MANIFEST"] = properties["kakao.native.app.key.manifest"] as String } @@ -40,6 +41,7 @@ android { debug { isMinifyEnabled = false buildConfigField("String", "BASE_URL", properties["dev.base.url"].toString()) + buildConfigField("String", "KAKAO_BASE_URL", properties["kakao.base.url"].toString()) buildConfigField("String", "AMPLITUDE_API_KEY", properties["amplitude.dev.api.key"].toString()) } @@ -47,6 +49,7 @@ android { isMinifyEnabled = true isShrinkResources = true buildConfigField("String", "BASE_URL", properties["prod.base.url"].toString()) + buildConfigField("String", "KAKAO_BASE_URL", properties["kakao.base.url"].toString()) buildConfigField("String", "AMPLITUDE_API_KEY", properties["amplitude.prod.api.key"].toString()) proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), diff --git a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/datasource/PlaceSearchDataSource.kt b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/datasource/PlaceSearchDataSource.kt index 400197f..30831a0 100644 --- a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/datasource/PlaceSearchDataSource.kt +++ b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/datasource/PlaceSearchDataSource.kt @@ -1,11 +1,9 @@ package org.sopt.teamdateroad.data.dataremote.datasource -import androidx.paging.PagingData -import kotlinx.coroutines.flow.Flow -import org.sopt.teamdateroad.data.dataremote.model.response.ResponsePlaceInfoDto +import org.sopt.teamdateroad.data.dataremote.model.response.ResponsePlaceSearchResultDto interface PlaceSearchDataSource { suspend fun getPlaceSearchResult( keyword: String - ): Flow> + ): Result } diff --git a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/datasourceimpl/PlaceSearchDataSourceImpl.kt b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/datasourceimpl/PlaceSearchDataSourceImpl.kt index f519581..b21d37c 100644 --- a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/datasourceimpl/PlaceSearchDataSourceImpl.kt +++ b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/datasourceimpl/PlaceSearchDataSourceImpl.kt @@ -1,65 +1,12 @@ package org.sopt.teamdateroad.data.dataremote.datasourceimpl -import androidx.paging.Pager -import androidx.paging.PagingConfig -import androidx.paging.PagingData -import androidx.paging.PagingSource -import androidx.paging.PagingState import javax.inject.Inject -import kotlinx.coroutines.flow.Flow import org.sopt.teamdateroad.data.dataremote.datasource.PlaceSearchDataSource -import org.sopt.teamdateroad.data.dataremote.model.response.ResponsePlaceInfoDto +import org.sopt.teamdateroad.data.dataremote.model.response.ResponsePlaceSearchResultDto import org.sopt.teamdateroad.data.dataremote.service.PlaceSearchService -import org.sopt.teamdateroad.presentation.ui.enroll.component.MAX_SIZE -import org.sopt.teamdateroad.presentation.ui.enroll.component.PAGE_SIZE class PlaceSearchDataSourceImpl @Inject constructor(private val placeSearchService: PlaceSearchService) : PlaceSearchDataSource { - override suspend fun getPlaceSearchResult(keyword: String): Flow> { - return Pager( - config = PagingConfig(pageSize = PAGE_SIZE, maxSize = MAX_SIZE), - pagingSourceFactory = { - PlaceSearchPagingSource( - keyword = keyword, - placeSearchService = placeSearchService - ) - } - ).flow - } -} - -class PlaceSearchPagingSource( - private val keyword: String, - private val placeSearchService: PlaceSearchService -) : PagingSource() { - override suspend fun load(params: LoadParams): LoadResult { - val page = params.key ?: INITIAL_PAGE - - return runCatching { - val placeSearchResult = placeSearchService.getPlaceSearchResult( - keyword = keyword, - page = page, - size = PAGE_SIZE - ) - - LoadResult.Page( - data = placeSearchResult.placeInfos, - prevKey = if (page == INITIAL_PAGE) null else page - PAGE_OFFSET, - nextKey = if (placeSearchResult.meta.isEnd) null else page + PAGE_OFFSET - ) - }.getOrElse { throwable -> - LoadResult.Error(throwable) - } - } - - override fun getRefreshKey(state: PagingState): Int? { - return state.anchorPosition?.let { position -> - state.closestPageToPosition(position)?.prevKey?.plus(PAGE_OFFSET) - ?: state.closestPageToPosition(position)?.nextKey?.minus(PAGE_OFFSET) - } - } - - companion object { - private const val INITIAL_PAGE = 1 - private const val PAGE_OFFSET = 1 + override suspend fun getPlaceSearchResult(keyword: String): Result { + return runCatching { placeSearchService.getPlaceSearchResult(keyword) } } } diff --git a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/model/response/ResponsePlaceSearchResultDto.kt b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/model/response/ResponsePlaceSearchResultDto.kt index 4517400..105bc53 100644 --- a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/model/response/ResponsePlaceSearchResultDto.kt +++ b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/model/response/ResponsePlaceSearchResultDto.kt @@ -5,9 +5,6 @@ import kotlinx.serialization.Serializable @Serializable data class ResponsePlaceSearchResultDto( - @SerializedName("meta") - val meta: ResponseMetaDto, - @SerializedName("documents") val placeInfos: List ) diff --git a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/service/PlaceSearchService.kt b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/service/PlaceSearchService.kt index 60a05c9..7c7d3a9 100644 --- a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/service/PlaceSearchService.kt +++ b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/service/PlaceSearchService.kt @@ -8,8 +8,6 @@ import retrofit2.http.Query interface PlaceSearchService { @GET(LOCAL_SEARCH_KEYWORD_JSON) suspend fun getPlaceSearchResult( - @Query("query") keyword: String, - @Query("page") page: Int, - @Query("size") size: Int + @Query("query") keyword: String ): ResponsePlaceSearchResultDto } diff --git a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/util/Constraints.kt b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/util/Constraints.kt index 7aac26b..aaedeb7 100644 --- a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/util/Constraints.kt +++ b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/util/Constraints.kt @@ -1,7 +1,5 @@ package org.sopt.teamdateroad.data.dataremote.util -import org.sopt.teamdateroad.data.dataremote.util.ApiConstraints.VERSION2 - object ApiConstraints { const val PROFILE_FORM_DATA_IMAGE = "image" const val COURSE_FORM_DATA_IMAGE = "images" @@ -9,7 +7,6 @@ object ApiConstraints { const val HTTPS = "https://" const val API = "api" const val VERSION = "v1" - const val VERSION2 = "v2" const val COURSES = "courses" const val DATE_ACCESS = "date-access" const val USERS = "users" @@ -80,5 +77,6 @@ object TotalCostZero { } object PlaceSearch { - const val LOCAL_SEARCH_KEYWORD_JSON = "$VERSION2/local/search/keyword.json" + private const val VERSION = "v2" + const val LOCAL_SEARCH_KEYWORD_JSON = "$VERSION/local/search/keyword.json" } diff --git a/app/src/main/java/org/sopt/teamdateroad/data/mapper/todomain/ResponsePlaceSearchDtoMapper.kt b/app/src/main/java/org/sopt/teamdateroad/data/mapper/todomain/ResponsePlaceSearchDtoMapper.kt index da6f95c..28a0e74 100644 --- a/app/src/main/java/org/sopt/teamdateroad/data/mapper/todomain/ResponsePlaceSearchDtoMapper.kt +++ b/app/src/main/java/org/sopt/teamdateroad/data/mapper/todomain/ResponsePlaceSearchDtoMapper.kt @@ -4,6 +4,5 @@ import org.sopt.teamdateroad.data.dataremote.model.response.ResponsePlaceSearchR import org.sopt.teamdateroad.domain.model.PlaceSearchResult fun ResponsePlaceSearchResultDto.toDomain(): PlaceSearchResult = PlaceSearchResult( - placeInfos = this.placeInfos.map { responsePlaceInfoDto -> responsePlaceInfoDto.toDomain() }, - isEnd = this.meta.isEnd + placeInfos = this.placeInfos.map { responsePlaceInfoDto -> responsePlaceInfoDto.toDomain() } ) diff --git a/app/src/main/java/org/sopt/teamdateroad/data/repositoryimpl/PlaceSearchRepositoryImpl.kt b/app/src/main/java/org/sopt/teamdateroad/data/repositoryimpl/PlaceSearchRepositoryImpl.kt index caaccdd..611f426 100644 --- a/app/src/main/java/org/sopt/teamdateroad/data/repositoryimpl/PlaceSearchRepositoryImpl.kt +++ b/app/src/main/java/org/sopt/teamdateroad/data/repositoryimpl/PlaceSearchRepositoryImpl.kt @@ -1,21 +1,15 @@ package org.sopt.teamdateroad.data.repositoryimpl -import androidx.paging.PagingData -import androidx.paging.map import javax.inject.Inject -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map import org.sopt.teamdateroad.data.dataremote.datasource.PlaceSearchDataSource import org.sopt.teamdateroad.data.mapper.todomain.toDomain -import org.sopt.teamdateroad.domain.model.PlaceInfo +import org.sopt.teamdateroad.domain.model.PlaceSearchResult import org.sopt.teamdateroad.domain.repository.PlaceSearchRepository class PlaceSearchRepositoryImpl @Inject constructor(private val placeSearchDataSource: PlaceSearchDataSource) : PlaceSearchRepository { - override suspend fun getPlaceSearchResult(keyword: String): Flow> { - return placeSearchDataSource.getPlaceSearchResult(keyword).map { pagingData -> - pagingData.map { responsePlaceInfoDto -> - responsePlaceInfoDto.toDomain() - } + override suspend fun getPlaceSearchResult(keyword: String): Result { + return placeSearchDataSource.getPlaceSearchResult(keyword).map { responsePlaceSearchResultDto -> + responsePlaceSearchResultDto.toDomain() } } } diff --git a/app/src/main/java/org/sopt/teamdateroad/domain/model/PlaceSearchResult.kt b/app/src/main/java/org/sopt/teamdateroad/domain/model/PlaceSearchResult.kt index 0005188..78a45ee 100644 --- a/app/src/main/java/org/sopt/teamdateroad/domain/model/PlaceSearchResult.kt +++ b/app/src/main/java/org/sopt/teamdateroad/domain/model/PlaceSearchResult.kt @@ -1,6 +1,5 @@ package org.sopt.teamdateroad.domain.model data class PlaceSearchResult( - val placeInfos: List, - val isEnd: Boolean + val placeInfos: List ) diff --git a/app/src/main/java/org/sopt/teamdateroad/domain/repository/PlaceSearchRepository.kt b/app/src/main/java/org/sopt/teamdateroad/domain/repository/PlaceSearchRepository.kt index 482b66c..0506f04 100644 --- a/app/src/main/java/org/sopt/teamdateroad/domain/repository/PlaceSearchRepository.kt +++ b/app/src/main/java/org/sopt/teamdateroad/domain/repository/PlaceSearchRepository.kt @@ -1,11 +1,9 @@ package org.sopt.teamdateroad.domain.repository -import androidx.paging.PagingData -import kotlinx.coroutines.flow.Flow -import org.sopt.teamdateroad.domain.model.PlaceInfo +import org.sopt.teamdateroad.domain.model.PlaceSearchResult interface PlaceSearchRepository { suspend fun getPlaceSearchResult( keyword: String - ): Flow> + ): Result } diff --git a/app/src/main/java/org/sopt/teamdateroad/domain/usecase/GetPlaceSearchResultUseCase.kt b/app/src/main/java/org/sopt/teamdateroad/domain/usecase/GetPlaceSearchResultUseCase.kt index 6614016..4866486 100644 --- a/app/src/main/java/org/sopt/teamdateroad/domain/usecase/GetPlaceSearchResultUseCase.kt +++ b/app/src/main/java/org/sopt/teamdateroad/domain/usecase/GetPlaceSearchResultUseCase.kt @@ -1,15 +1,11 @@ package org.sopt.teamdateroad.domain.usecase -import androidx.paging.PagingData import javax.inject.Inject -import javax.inject.Singleton -import kotlinx.coroutines.flow.Flow -import org.sopt.teamdateroad.domain.model.PlaceInfo +import org.sopt.teamdateroad.domain.model.PlaceSearchResult import org.sopt.teamdateroad.domain.repository.PlaceSearchRepository -@Singleton class GetPlaceSearchResultUseCase @Inject constructor( private val placeSearchRepository: PlaceSearchRepository ) { - suspend operator fun invoke(keyword: String): Flow> = placeSearchRepository.getPlaceSearchResult(keyword) + suspend operator fun invoke(keyword: String): Result = placeSearchRepository.getPlaceSearchResult(keyword) } From a4b30782c1d2a4220a0bd8dc8395228334dda083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=84=EC=9A=B0?= <85734140+jinuemong@users.noreply.github.com> Date: Wed, 26 Mar 2025 09:04:59 +0900 Subject: [PATCH 27/49] =?UTF-8?q?[feat]=20DefaultButtonSheet=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20&=20Point=20=EC=88=98=EC=A7=91=20UI=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20(#10)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: ํฌ์ธํŠธ ui ๋ณ€๊ฒฝ์‚ฌํ•ญ ์ ์šฉ * feat: ํฌ์ธํŠธ ๋ฐ”ํ…€์‹œํŠธ ๊ตฌํ˜„ * feat: ํฌ์ธํŠธ ๋ฐ”ํ…€์‹œํŠธ ์—ฐ๊ฒฐ * style : ์ฝ”๋“œ ํฌ๋งท ์ˆ˜์ • & ๋ฆฐํŠธ ์ฒดํฌ * refactor : ์ค‘๋ณต ์ฝ”๋“œ ์ œ๊ฑฐ --- .../bottomsheet/DateRoadPointBottomSheet.kt | 20 +-- .../bottomsheet/DefaultDateRoadBottomSheet.kt | 9 -- .../ui/pointhistory/PointHistoryContract.kt | 9 +- .../ui/pointhistory/PointHistoryScreen.kt | 114 +++--------------- .../ui/pointhistory/PointHistoryViewModel.kt | 18 +-- app/src/main/res/values/strings.xml | 10 +- 6 files changed, 26 insertions(+), 154 deletions(-) diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadPointBottomSheet.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadPointBottomSheet.kt index 5a24ef5..f4a0407 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadPointBottomSheet.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadPointBottomSheet.kt @@ -22,7 +22,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.sopt.teamdateroad.R -import org.sopt.teamdateroad.domain.util.PointCollect import org.sopt.teamdateroad.presentation.ui.component.bottomsheet.model.collect.DateRoadCollectPointType import org.sopt.teamdateroad.presentation.util.modifier.noRippleClickable import org.sopt.teamdateroad.ui.theme.DateRoadTheme @@ -31,7 +30,6 @@ import org.sopt.teamdateroad.ui.theme.DateRoadTheme @Composable fun DateRoadPointBottomSheet( isBottomSheetOpen: Boolean, - title: String, onClick: (DateRoadCollectPointType) -> Unit, onDismissRequest: () -> Unit = {} ) { @@ -49,7 +47,7 @@ fun DateRoadPointBottomSheet( ) ) { Text( - text = title, + text = stringResource(R.string.point_box_get_point_button_text), color = DateRoadTheme.colors.black, style = DateRoadTheme.typography.bodyBold17, modifier = Modifier.align(Alignment.CenterStart) @@ -64,11 +62,11 @@ fun DateRoadPointBottomSheet( ) } Spacer(modifier = Modifier.height(17.dp)) - DateRoadBottomSheetContent( + DateRoadBottonSheetContent( dateLoadCollectPoint = DateRoadCollectPointType.WATCH_ADS, onClick = onClick ) - DateRoadBottomSheetContent( + DateRoadBottonSheetContent( dateLoadCollectPoint = DateRoadCollectPointType.COURSE_REGISTRATION, onClick = onClick ) @@ -78,15 +76,10 @@ fun DateRoadPointBottomSheet( } @Composable -fun DateRoadBottomSheetContent( +fun DateRoadBottonSheetContent( dateLoadCollectPoint: DateRoadCollectPointType, onClick: (DateRoadCollectPointType) -> Unit ) { - val pointAmount = when (dateLoadCollectPoint) { - DateRoadCollectPointType.WATCH_ADS -> PointCollect.ADS - DateRoadCollectPointType.COURSE_REGISTRATION -> PointCollect.COURSE - } - Row( modifier = Modifier .fillMaxWidth() @@ -116,7 +109,7 @@ fun DateRoadBottomSheetContent( ) Spacer(modifier = Modifier.height(4.dp)) Text( - text = stringResource(dateLoadCollectPoint.contentRes, pointAmount), + text = stringResource(dateLoadCollectPoint.contentRes), color = DateRoadTheme.colors.gray400, style = DateRoadTheme.typography.bodySemi13 @@ -136,7 +129,6 @@ fun DateRoadBottomSheetContent( fun DateRoadPointBottomSheetPreView() { DateRoadPointBottomSheet( isBottomSheetOpen = true, - onClick = {}, - title = "" + onClick = {} ) } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DefaultDateRoadBottomSheet.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DefaultDateRoadBottomSheet.kt index 1398558..8023a8f 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DefaultDateRoadBottomSheet.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DefaultDateRoadBottomSheet.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.windowInsetsBottomHeight import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.SheetState @@ -69,14 +68,6 @@ fun DefaultDateRoadBottomSheetPreview() { DATEROADTheme { var isBottomSheetOpen by rememberSaveable { mutableStateOf(false) } - Button(onClick = { isBottomSheetOpen = true }) { - Text( - text = "DefaultDateRoadBottomSheet", - color = DateRoadTheme.colors.black, - style = DateRoadTheme.typography.titleExtra20 - ) - } - DefaultDateRoadBottomSheet( modifier = Modifier.padding(top = 20.dp, bottom = 16.dp, start = 16.dp, end = 16.dp), isBottomSheetOpen = isBottomSheetOpen, diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryContract.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryContract.kt index 05f548d..edccc6a 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryContract.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryContract.kt @@ -2,7 +2,6 @@ package org.sopt.teamdateroad.presentation.ui.pointhistory import org.sopt.teamdateroad.domain.model.PointHistory import org.sopt.teamdateroad.domain.model.UserPoint -import org.sopt.teamdateroad.presentation.type.EnrollType import org.sopt.teamdateroad.presentation.type.PointHistoryTabType import org.sopt.teamdateroad.presentation.util.base.UiEvent import org.sopt.teamdateroad.presentation.util.base.UiSideEffect @@ -15,24 +14,18 @@ class PointHistoryContract { val userPoint: UserPoint = UserPoint(), val isPointCollectBottomSheetOpen: Boolean = false, val pointHistoryTabType: PointHistoryTabType = PointHistoryTabType.GAINED_HISTORY, - val pointHistory: PointHistory = PointHistory(), - val isFullAdsDialogOpen: Boolean = false + val pointHistory: PointHistory = PointHistory() ) : UiState sealed interface PointHistorySideEffect : UiSideEffect { data object PopBackStack : PointHistorySideEffect - data object NavigateToAds : PointHistorySideEffect - data class NavigateToEnroll(val enrollType: EnrollType, val viewPath: String, val id: Int?) : PointHistorySideEffect } sealed class PointHistoryEvent : UiEvent { data class FetchPointHistory(val loadState: LoadState, val pointHistory: PointHistory) : PointHistoryEvent() data class FetchUserPoint(val loadState: LoadState, val userPoint: UserPoint) : PointHistoryEvent() data class OnTabBarClicked(val pointHistoryTabType: PointHistoryTabType) : PointHistoryEvent() - data object FailLoadAdsPoint : PointHistoryEvent() data object OnPointCollectBottomSheetClick : PointHistoryEvent() data object OnPointCollectBottomSheetDismiss : PointHistoryEvent() - data object FullAds : PointHistoryEvent() - data object DismissFullAdsDialog : PointHistoryEvent() } } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryScreen.kt index 63aaba2..778bb45 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryScreen.kt @@ -13,9 +13,7 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -23,22 +21,13 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle -import com.google.android.gms.ads.AdRequest -import com.google.android.gms.ads.LoadAdError -import com.google.android.gms.ads.rewarded.RewardedAd -import com.google.android.gms.ads.rewarded.RewardedAdLoadCallback -import org.sopt.teamdateroad.BuildConfig import org.sopt.teamdateroad.R import org.sopt.teamdateroad.domain.model.Point import org.sopt.teamdateroad.domain.model.PointHistory import org.sopt.teamdateroad.domain.model.UserPoint import org.sopt.teamdateroad.presentation.type.EmptyViewType -import org.sopt.teamdateroad.presentation.type.EnrollType -import org.sopt.teamdateroad.presentation.type.OneButtonDialogWithDescriptionType import org.sopt.teamdateroad.presentation.type.PointHistoryTabType import org.sopt.teamdateroad.presentation.ui.component.bottomsheet.DateRoadPointBottomSheet -import org.sopt.teamdateroad.presentation.ui.component.bottomsheet.model.collect.DateRoadCollectPointType -import org.sopt.teamdateroad.presentation.ui.component.dialog.DateRoadOneButtonDialogWithDescription import org.sopt.teamdateroad.presentation.ui.component.tabbar.DateRoadTabBar import org.sopt.teamdateroad.presentation.ui.component.tabbar.DateRoadTabTitle import org.sopt.teamdateroad.presentation.ui.component.topbar.DateRoadBasicTopBar @@ -48,9 +37,6 @@ import org.sopt.teamdateroad.presentation.ui.component.view.DateRoadIdleView import org.sopt.teamdateroad.presentation.ui.component.view.DateRoadLoadingView import org.sopt.teamdateroad.presentation.ui.pointhistory.component.PointHistoryCard import org.sopt.teamdateroad.presentation.ui.pointhistory.component.PointHistoryPointBox -import org.sopt.teamdateroad.presentation.util.AdsAmplitude -import org.sopt.teamdateroad.presentation.util.ViewPath.POINT_HISTORY -import org.sopt.teamdateroad.presentation.util.amplitude.AmplitudeUtils import org.sopt.teamdateroad.presentation.util.view.LoadState import org.sopt.teamdateroad.ui.theme.DATEROADTheme import org.sopt.teamdateroad.ui.theme.DateRoadTheme @@ -59,18 +45,14 @@ import org.sopt.teamdateroad.ui.theme.DateRoadTheme fun PointHistoryRoute( padding: PaddingValues, viewModel: PointHistoryViewModel = hiltViewModel(), - popBackStack: () -> Unit, - navigateToEnroll: (EnrollType, String, Int?) -> Unit + popBackStack: () -> Unit ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val lifecycleOwner = LocalLifecycleOwner.current - val context = LocalContext.current - val adRequest = remember { AdRequest.Builder().build() } LaunchedEffect(Unit) { viewModel.fetchPointHistory() viewModel.fetchUserPoint() - AmplitudeUtils.trackEvent(eventName = AdsAmplitude.VIEW_POINT) } LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { @@ -78,32 +60,6 @@ fun PointHistoryRoute( .collect { pointHistorySideEffect -> when (pointHistorySideEffect) { is PointHistoryContract.PointHistorySideEffect.PopBackStack -> popBackStack() - is PointHistoryContract.PointHistorySideEffect.NavigateToEnroll -> navigateToEnroll( - pointHistorySideEffect.enrollType, - pointHistorySideEffect.viewPath, - pointHistorySideEffect.id - ) - - PointHistoryContract.PointHistorySideEffect.NavigateToAds -> { - RewardedAd.load( - context, - BuildConfig.GOOGLE_ADS_API_ID, - adRequest, - object : RewardedAdLoadCallback() { - override fun onAdLoaded(ad: RewardedAd) { - viewModel.postAdsPoint() - } - - override fun onAdFailedToLoad(error: LoadAdError) { - when (error.code) { - AdRequest.ERROR_CODE_NO_FILL -> viewModel.setEvent(PointHistoryContract.PointHistoryEvent.FullAds) - - else -> viewModel.setEvent(PointHistoryContract.PointHistoryEvent.FailLoadAdsPoint) - } - } - } - ) - } } } } @@ -123,32 +79,8 @@ fun PointHistoryRoute( ) }, onTopBarIconClicked = { viewModel.setSideEffect(PointHistoryContract.PointHistorySideEffect.PopBackStack) }, - onClickCollectPoint = { - viewModel.setEvent(PointHistoryContract.PointHistoryEvent.OnPointCollectBottomSheetClick) - }, - onDisMissCollectPoint = { - AmplitudeUtils.trackEvent(eventName = AdsAmplitude.CLICK_COLLECT_POINT_CLOSE) - viewModel.setEvent(PointHistoryContract.PointHistoryEvent.OnPointCollectBottomSheetDismiss) - }, - onSelectEnroll = { - AmplitudeUtils.trackEvent(eventName = AdsAmplitude.CLICK_COURSE) - viewModel.setEvent(PointHistoryContract.PointHistoryEvent.OnPointCollectBottomSheetDismiss) - viewModel.setSideEffect( - PointHistoryContract.PointHistorySideEffect.NavigateToEnroll( - enrollType = EnrollType.COURSE, - viewPath = POINT_HISTORY, - id = null - ) - ) - }, - onSelectAds = { - AmplitudeUtils.trackEvent(eventName = AdsAmplitude.CLICK_AD) - viewModel.setEvent(PointHistoryContract.PointHistoryEvent.OnPointCollectBottomSheetDismiss) - viewModel.setSideEffect(PointHistoryContract.PointHistorySideEffect.NavigateToAds) - }, - onDismissFullAdsDialog = { - viewModel.setEvent(PointHistoryContract.PointHistoryEvent.DismissFullAdsDialog) - } + onClickCollectPoint = { viewModel.setEvent(PointHistoryContract.PointHistoryEvent.OnPointCollectBottomSheetClick) }, + onDisMissCollectPoint = { viewModel.setEvent(PointHistoryContract.PointHistoryEvent.OnPointCollectBottomSheetDismiss) } ) } @@ -163,10 +95,7 @@ fun PointHistoryScreen( onTabBarClicked: (PointHistoryTabType) -> Unit, onTopBarIconClicked: () -> Unit, onClickCollectPoint: () -> Unit, - onDisMissCollectPoint: () -> Unit, - onSelectEnroll: () -> Unit, - onSelectAds: () -> Unit, - onDismissFullAdsDialog: () -> Unit + onDisMissCollectPoint: () -> Unit ) { Column( modifier = Modifier @@ -223,32 +152,24 @@ fun PointHistoryScreen( } items(pointHistory.size) { index -> PointHistoryCard(point = pointHistory[index]) - HorizontalDivider( - color = DateRoadTheme.colors.gray100, - thickness = 1.dp - ) + if (index < pointHistory.size - 1) { + HorizontalDivider( + color = DateRoadTheme.colors.gray100, + thickness = 1.dp + ) + } } } } - if (pointHistoryUiState.isFullAdsDialogOpen) { - DateRoadOneButtonDialogWithDescription( - oneButtonDialogWithDescriptionType = OneButtonDialogWithDescriptionType.FULL_ADS, - onDismissRequest = onDismissFullAdsDialog, - onClickConfirm = onDismissFullAdsDialog - ) - } - DateRoadPointBottomSheet( isBottomSheetOpen = pointHistoryUiState.isPointCollectBottomSheetOpen, - title = stringResource(R.string.point_box_get_point_button_text), - onClick = { dateRoadCollectPointType -> - when (dateRoadCollectPointType) { - DateRoadCollectPointType.WATCH_ADS -> onSelectAds() - DateRoadCollectPointType.COURSE_REGISTRATION -> onSelectEnroll() - } + onClick = { + onDisMissCollectPoint() }, - onDismissRequest = onDisMissCollectPoint + onDismissRequest = { + onDisMissCollectPoint() + } ) } @@ -273,10 +194,7 @@ fun PointHistoryPreview() { onTabBarClicked = {}, onTopBarIconClicked = {}, onClickCollectPoint = {}, - onDisMissCollectPoint = {}, - onSelectEnroll = {}, - onSelectAds = {}, - onDismissFullAdsDialog = {} + onDisMissCollectPoint = {} ) } } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryViewModel.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryViewModel.kt index 990cac5..2772fcb 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryViewModel.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryViewModel.kt @@ -6,15 +6,13 @@ import javax.inject.Inject import kotlinx.coroutines.launch import org.sopt.teamdateroad.domain.usecase.GetPointHistoryUseCase import org.sopt.teamdateroad.domain.usecase.GetUserPointUseCase -import org.sopt.teamdateroad.domain.usecase.PostAdsPointUseCase import org.sopt.teamdateroad.presentation.util.base.BaseViewModel import org.sopt.teamdateroad.presentation.util.view.LoadState @HiltViewModel class PointHistoryViewModel @Inject constructor( private val getPointHistoryUseCase: GetPointHistoryUseCase, - private val getUserPointUseCase: GetUserPointUseCase, - private val postAdsPointUseCase: PostAdsPointUseCase + private val getUserPointUseCase: GetUserPointUseCase ) : BaseViewModel() { override fun createInitialState(): PointHistoryContract.PointHistoryUiState = PointHistoryContract.PointHistoryUiState() @@ -26,9 +24,6 @@ class PointHistoryViewModel @Inject constructor( is PointHistoryContract.PointHistoryEvent.FetchUserPoint -> setState { copy(loadState = event.loadState, userPoint = event.userPoint) } PointHistoryContract.PointHistoryEvent.OnPointCollectBottomSheetClick -> setState { copy(isPointCollectBottomSheetOpen = true) } PointHistoryContract.PointHistoryEvent.OnPointCollectBottomSheetDismiss -> setState { copy(isPointCollectBottomSheetOpen = false) } - PointHistoryContract.PointHistoryEvent.FailLoadAdsPoint -> setState { copy(loadState = LoadState.Loading) } - PointHistoryContract.PointHistoryEvent.DismissFullAdsDialog -> setState { copy(isFullAdsDialogOpen = false) } - PointHistoryContract.PointHistoryEvent.FullAds -> setState { copy(isFullAdsDialogOpen = true) } } } @@ -59,15 +54,4 @@ class PointHistoryViewModel @Inject constructor( } } } - - fun postAdsPoint() { - viewModelScope.launch { - postAdsPointUseCase().onSuccess { - fetchPointHistory() - fetchUserPoint() - }.onFailure { - setState { copy(loadState = LoadState.Loading) } - } - } - } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3def010..a622fa7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -112,8 +112,6 @@ ์กฐ๊ธˆ๋งŒ ๊ธฐ๋‹ค๋ ค์ฃผ์„ธ์š” :) ๋ฐ์ดํŠธ๋ฅผ ๋” ์ด์ƒ ๋“ฑ๋กํ•  ์ˆ˜ ์—†์–ด์š”! ๋ฐ์ดํŠธ๋Š” ์ตœ๋Œ€ 5๊ฐœ๊นŒ์ง€๋งŒ ๋“ฑ๋ก ๊ฐ€๋Šฅํ•ด์š” - ๊ด‘๊ณ ๋Š” ํ•˜๋ฃจ 5ํšŒ๊นŒ์ง€๋งŒ ์‹œ์ฒญํ•  ์ˆ˜ ์žˆ์–ด์š” - ๊ด‘๊ณ  ์‹œ์ฒญ ํ•œ๋„๋Š” ๋งค์ผ ์ž์ •์— ์ดˆ๊ธฐํ™”๋ผ์š” ํš๋“ ๋‚ด์—ญ @@ -194,7 +192,6 @@ %1$s\๋‹˜์˜ ํฌ์ธํŠธ %1$d P ํฌ์ธํŠธ ๋ชจ์œผ๋Ÿฌ ๊ฐ€๊ธฐ - ํฌ์ธํŠธ๊ฐ€ ๋ถ€์กฑํ•ด์š”! ํฌ์ธํŠธ ๋‚ด์—ญ ๋ณด๊ธฐ @@ -254,9 +251,6 @@ ์ด ๋น„์šฉ์„ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š” ๋ฐ์ดํŠธ ์˜ˆ์ƒ ์ด ๋น„์šฉ์„ ์ˆซ์ž๋กœ๋งŒ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š” ์™„๋ฃŒ - ์žฅ์†Œ ๊ฒ€์ƒ‰ํ•˜๊ธฐ - ์ผ์น˜ํ•˜๋Š” ์žฅ์†Œ๊ฐ€ ์—†์–ด์š”! - ๋Œ€ํ‘œ %s๋‹˜, ์˜ค๋Š˜์€\n์ด๋Ÿฐ ๋ฐ์ดํŠธ ์ฝ”์Šค ์–ด๋– ์„ธ์š”? @@ -292,9 +286,9 @@ ๊ด‘๊ณ  ์‹œ์ฒญํ•˜๊ธฐ - ์งง์€ ๊ด‘๊ณ  ๋ณด๊ณ  ๋ฐ”๋กœ %dP ํš๋“ํ•˜๊ธฐ + ์งง์€ ๊ด‘๊ณ  ๋ณด๊ณ  ๋ฐ”๋กœ ํฌ์ธํŠธ ํš๋“ํ•˜๊ธฐ ์ฝ”์Šค ๋“ฑ๋กํ•˜๊ธฐ - ๋‚˜๋งŒ์˜ ์ฝ”์Šค ๋“ฑ๋กํ•˜๊ณ  %dP ํš๋“ํ•˜๊ธฐ + ๋‚˜๋งŒ์˜ ์ฝ”์Šค ๋“ฑ๋กํ•˜๊ณ  ํฌ์ธํŠธ ํš๋“ํ•˜๊ธฐ ๋‚ด ํ”„๋กœํ•„ From 016ad445ec8a79aee6209395aba0784c038f6784 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=84=EC=9A=B0?= <85734140+jinuemong@users.noreply.github.com> Date: Thu, 27 Mar 2025 18:14:34 +0900 Subject: [PATCH 28/49] =?UTF-8?q?[feat]=20=EC=8D=B8=EB=84=A4=EC=9D=BC=20UI?= =?UTF-8?q?=20&=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84=20(#16)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat : ์ธ๋„ค์ผ ์ง€์ • ๋กœ์ง ๊ตฌํ˜„ * feat : ์ธ๋„ค์ผ ์ง€์ • UI ๊ตฌํ˜„ * feat : ๋…ผ๋ฆฌ ์˜ค๋ฅ˜ ์ˆ˜์ •, ๊ธฐ์กด ์ธ๋„ค์ผ ์‚ญ์ œ ์‹œ ์˜† ์ธ๋„ค์ผ๋กœ ์ง€์ • * style: ์ฝ”๋“œ ํฌ๋งท ์ˆ˜์ • & ๋ฆฐํŠธ ์ฒดํฌ --- .../presentation/ui/enroll/EnrollScreen.kt | 52 +++---------------- .../presentation/ui/enroll/EnrollViewModel.kt | 49 ++--------------- app/src/main/res/values/strings.xml | 1 + 3 files changed, 12 insertions(+), 90 deletions(-) diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt index 7204ae6..5f79966 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt @@ -22,7 +22,6 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -30,12 +29,10 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle -import androidx.paging.compose.collectAsLazyPagingItems import java.time.LocalDate import java.time.format.DateTimeFormatter import org.sopt.teamdateroad.R import org.sopt.teamdateroad.domain.model.Place -import org.sopt.teamdateroad.domain.model.PlaceInfo import org.sopt.teamdateroad.domain.type.RegionType import org.sopt.teamdateroad.presentation.type.DateRoadRegionBottomSheetType import org.sopt.teamdateroad.presentation.type.DateTagType @@ -110,11 +107,7 @@ fun EnrollRoute( timelineId: Int? ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() - val searchKeyword by viewModel.searchKeyword.collectAsStateWithLifecycle("") - val searchPlaceInfos = viewModel.searchPlaceInfos.collectAsLazyPagingItems() - val lifecycleOwner = LocalLifecycleOwner.current - val keyboardController = LocalSoftwareKeyboardController.current val getGalleryLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? -> if (uri != null) viewModel.setEvent(EnrollContract.EnrollEvent.SetImage(images = listOf(uri.toString()))) @@ -212,8 +205,6 @@ fun EnrollRoute( EnrollScreen( padding = padding, enrollUiState = uiState, - searchKeyword = searchKeyword, - searchPlaceInfos = searchPlaceInfos.itemSnapshotList.items, onTopBarBackButtonClick = { viewModel.setEvent(EnrollContract.EnrollEvent.OnTopBarBackButtonClick) @@ -259,22 +250,9 @@ fun EnrollRoute( AmplitudeUtils.trackEvent(eventName = CLICK_BRING_COURSE) }, onEnrollButtonClick = { viewModel.setEvent(EnrollContract.EnrollEvent.OnEnrollButtonClick) }, - onDateTextFieldClick = { - keyboardController?.hide() - viewModel.setEvent(EnrollContract.EnrollEvent.OnDateTextFieldClick) - }, - onTimeTextFieldClick = { - keyboardController?.hide() - viewModel.setEvent(EnrollContract.EnrollEvent.OnTimeTextFieldClick) - }, - onRegionTextFieldClick = { - keyboardController?.hide() - viewModel.setEvent(EnrollContract.EnrollEvent.OnRegionTextFieldClick) - }, - onPlaceSearchButtonClick = { viewModel.setEvent(EnrollContract.EnrollEvent.OnPlaceSearchButtonClick) }, - onKeywordChanged = { keyword -> viewModel.setEvent(EnrollContract.EnrollEvent.OnKeywordChanged(keyword = keyword)) }, - onPlaceSelected = { placeInfo -> viewModel.setEvent(EnrollContract.EnrollEvent.OnPlaceSelected(placeInfo = placeInfo)) }, - onPlaceSearchBottomSheetDismiss = { viewModel.setEvent(EnrollContract.EnrollEvent.OnPlaceSearchBottomSheetDismiss) }, + onDateTextFieldClick = { viewModel.setEvent(EnrollContract.EnrollEvent.OnDateTextFieldClick) }, + onTimeTextFieldClick = { viewModel.setEvent(EnrollContract.EnrollEvent.OnTimeTextFieldClick) }, + onRegionTextFieldClick = { viewModel.setEvent(EnrollContract.EnrollEvent.OnRegionTextFieldClick) }, onSelectedPlaceCourseTimeClick = { viewModel.setEvent(EnrollContract.EnrollEvent.OnSelectedPlaceCourseTimeClick) }, onDatePickerBottomSheetDismissRequest = { viewModel.setEvent(EnrollContract.EnrollEvent.OnDatePickerBottomSheetDismissRequest) }, onTimePickerBottomSheetDismissRequest = { viewModel.setEvent(EnrollContract.EnrollEvent.OnTimePickerBottomSheetDismissRequest) }, @@ -306,6 +284,7 @@ fun EnrollRoute( onRegionBottomSheetButtonClick = { region: RegionType?, area: Any? -> viewModel.setEvent(EnrollContract.EnrollEvent.OnRegionBottomSheetButtonClick(region = region, area = area)) }, onAddPlaceButtonClick = { place -> viewModel.setEvent(EnrollContract.EnrollEvent.OnAddPlaceButtonClick(place = place)) }, onPlaceCardDragAndDrop = { places -> viewModel.setEvent(EnrollContract.EnrollEvent.OnPlaceCardDragAndDrop(places = places)) }, + onPlaceTitleValueChange = { placeTitle -> viewModel.setEvent(EnrollContract.EnrollEvent.OnPlaceTitleValueChange(placeTitle = placeTitle)) }, onDurationBottomSheetButtonClick = { placeDuration -> viewModel.setEvent(EnrollContract.EnrollEvent.OnDurationBottomSheetButtonClick(placeDuration = placeDuration)) }, onPlaceEditButtonClick = { editable -> viewModel.setEvent(EnrollContract.EnrollEvent.OnEditableValueChange(editable = editable)) }, onPlaceCardDeleteButtonClick = { index -> viewModel.setEvent(EnrollContract.EnrollEvent.OnPlaceCardDeleteButtonClick(index = index)) }, @@ -352,18 +331,12 @@ fun EnrollRoute( fun EnrollScreen( padding: PaddingValues, enrollUiState: EnrollContract.EnrollUiState = EnrollContract.EnrollUiState(), - searchKeyword: String, - searchPlaceInfos: List, onTopBarBackButtonClick: () -> Unit, onTopBarLoadButtonClick: () -> Unit, onEnrollButtonClick: () -> Unit, onDateTextFieldClick: () -> Unit, onTimeTextFieldClick: () -> Unit, onRegionTextFieldClick: () -> Unit, - onPlaceSearchButtonClick: () -> Unit, - onKeywordChanged: (String) -> Unit, - onPlaceSelected: (PlaceInfo) -> Unit, - onPlaceSearchBottomSheetDismiss: () -> Unit, onSelectedPlaceCourseTimeClick: () -> Unit, onDatePickerBottomSheetDismissRequest: () -> Unit, onTimePickerBottomSheetDismissRequest: () -> Unit, @@ -380,6 +353,7 @@ fun EnrollScreen( onRegionBottomSheetButtonClick: (RegionType?, Any?) -> Unit, onAddPlaceButtonClick: (Place) -> Unit, onPlaceCardDragAndDrop: (List) -> Unit, + onPlaceTitleValueChange: (String) -> Unit, onDurationBottomSheetButtonClick: (String) -> Unit, onPlaceEditButtonClick: (Boolean) -> Unit, onPlaceCardDeleteButtonClick: (Int) -> Unit, @@ -464,14 +438,9 @@ fun EnrollScreen( EnrollScreenType.SECOND -> EnrollSecondScreen( enrollUiState = enrollUiState, - searchKeyword = searchKeyword, - searchPlaceInfos = searchPlaceInfos, - onPlaceSearchButtonClick = onPlaceSearchButtonClick, - onKeywordChanged = onKeywordChanged, - onPlaceSelected = onPlaceSelected, - onPlaceSearchBottomSheetDismiss = onPlaceSearchBottomSheetDismiss, onSelectedPlaceCourseTimeClick = onSelectedPlaceCourseTimeClick, onAddPlaceButtonClick = onAddPlaceButtonClick, + onPlaceTitleValueChange = onPlaceTitleValueChange, onPlaceEditButtonClick = onPlaceEditButtonClick, onPlaceCardDeleteButtonClick = onPlaceCardDeleteButtonClick, onPlaceCardDragAndDrop = onPlaceCardDragAndDrop @@ -593,15 +562,12 @@ fun EnrollScreenPreview() { enrollUiState = EnrollContract.EnrollUiState( loadState = LoadState.Success ), - searchKeyword = "", - searchPlaceInfos = listOf(), onTopBarBackButtonClick = {}, onTopBarLoadButtonClick = {}, onEnrollButtonClick = {}, onDateTextFieldClick = {}, onTimeTextFieldClick = {}, onRegionTextFieldClick = {}, - onPlaceSearchButtonClick = {}, onSelectedPlaceCourseTimeClick = {}, onDatePickerBottomSheetDismissRequest = {}, onTimePickerBottomSheetDismissRequest = {}, @@ -617,6 +583,7 @@ fun EnrollScreenPreview() { onRegionBottomSheetAreaChipClick = {}, onRegionBottomSheetButtonClick = { _, _ -> }, onAddPlaceButtonClick = {}, + onPlaceTitleValueChange = {}, onDurationBottomSheetButtonClick = {}, onPlaceEditButtonClick = {}, onPlaceCardDeleteButtonClick = {}, @@ -624,10 +591,7 @@ fun EnrollScreenPreview() { onDescriptionValueChange = {}, onCostValueChange = {}, onEnrollSuccessDialogButtonClick = {}, - onSelectThumbnail = {}, - onPlaceSearchBottomSheetDismiss = {}, - onKeywordChanged = { }, - onPlaceSelected = { } + onSelectThumbnail = {} ) } } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollViewModel.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollViewModel.kt index 49d4c05..e06298f 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollViewModel.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollViewModel.kt @@ -1,26 +1,13 @@ package org.sopt.teamdateroad.presentation.ui.enroll import androidx.lifecycle.viewModelScope -import androidx.paging.PagingData -import androidx.paging.cachedIn import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.launch import org.sopt.teamdateroad.data.dataremote.util.Date.NEAREST_DATE_START_OUTPUT_FORMAT import org.sopt.teamdateroad.data.mapper.toEntity.toEnroll -import org.sopt.teamdateroad.domain.model.PlaceInfo import org.sopt.teamdateroad.domain.type.RegionType import org.sopt.teamdateroad.domain.usecase.GetCourseDetailUseCase -import org.sopt.teamdateroad.domain.usecase.GetPlaceSearchResultUseCase import org.sopt.teamdateroad.domain.usecase.GetTimelineDetailUseCase import org.sopt.teamdateroad.domain.usecase.PostCourseUseCase import org.sopt.teamdateroad.domain.usecase.PostTimelineUseCase @@ -38,23 +25,10 @@ class EnrollViewModel @Inject constructor( private val getCourseDetailUseCase: GetCourseDetailUseCase, private val getTimelineDetailUseCase: GetTimelineDetailUseCase, private val postCourseUseCase: PostCourseUseCase, - private val postTimelineUseCase: PostTimelineUseCase, - private val getPlaceSearchResultUseCase: GetPlaceSearchResultUseCase + private val postTimelineUseCase: PostTimelineUseCase ) : BaseViewModel() { override fun createInitialState(): EnrollContract.EnrollUiState = EnrollContract.EnrollUiState() - private val _searchKeyword: MutableStateFlow = MutableStateFlow("") - val searchKeyword: StateFlow get() = _searchKeyword.asStateFlow() - - @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) - val searchPlaceInfos: Flow> = searchKeyword - .debounce(DEBOUNCE_TIME_MILLS) - .distinctUntilChanged() - .flatMapLatest { query -> - getPlaceSearchResultUseCase(query) - } - .cachedIn(viewModelScope) - override suspend fun handleEvent(event: EnrollContract.EnrollEvent) { when (event) { is EnrollContract.EnrollEvent.OnTopBarBackButtonClick -> { @@ -100,14 +74,6 @@ class EnrollViewModel @Inject constructor( is EnrollContract.EnrollEvent.OnDateTextFieldClick -> setState { copy(isDatePickerBottomSheetOpen = true) } is EnrollContract.EnrollEvent.OnTimeTextFieldClick -> setState { copy(isTimePickerBottomSheetOpen = true) } is EnrollContract.EnrollEvent.OnRegionTextFieldClick -> setState { copy(isRegionBottomSheetOpen = true, onRegionBottomSheetRegionSelected = RegionType.SEOUL, onRegionBottomSheetAreaSelected = null) } - is EnrollContract.EnrollEvent.OnPlaceSearchButtonClick -> setState { copy(isPlaceSearchBottomSheetOpen = true) } - is EnrollContract.EnrollEvent.OnKeywordChanged -> _searchKeyword.value = event.keyword - - is EnrollContract.EnrollEvent.OnPlaceSearchBottomSheetDismiss -> { - setState { copy(isPlaceSearchBottomSheetOpen = false) } - _searchKeyword.value = "" - } - is EnrollContract.EnrollEvent.OnSelectedPlaceCourseTimeClick -> setState { copy(isDurationBottomSheetOpen = true) } is EnrollContract.EnrollEvent.OnDatePickerBottomSheetDismissRequest -> setState { copy(isDatePickerBottomSheetOpen = false) } is EnrollContract.EnrollEvent.OnTimePickerBottomSheetDismissRequest -> setState { copy(isTimePickerBottomSheetOpen = false) } @@ -126,14 +92,10 @@ class EnrollViewModel @Inject constructor( thumbnailIndex = if (event.moveThumbnail) (thumbnailIndex - 1).coerceAtLeast(0) else thumbnailIndex ) } - is EnrollContract.EnrollEvent.OnTitleValueChange -> setState { copy(enroll = currentState.enroll.copy(title = event.title)) } - is EnrollContract.EnrollEvent.OnPlaceSelected -> { - setState { copy(place = currentState.place.copy(title = event.placeInfo.placeName, address = event.placeInfo.addressName), selectedPlaceInfos = currentState.selectedPlaceInfos + event.placeInfo, isPlaceSearchBottomSheetOpen = false) } - _searchKeyword.value = "" - } is EnrollContract.EnrollEvent.OnDatePickerBottomSheetButtonClick -> setState { copy(enroll = currentState.enroll.copy(date = event.date), isDatePickerBottomSheetOpen = false) } + is EnrollContract.EnrollEvent.OnTimePickerBottomSheetButtonClick -> setState { copy(enroll = currentState.enroll.copy(startAt = event.startAt), isTimePickerBottomSheetOpen = false) } is EnrollContract.EnrollEvent.OnDateChipClicked -> setState { copy( @@ -173,9 +135,8 @@ class EnrollViewModel @Inject constructor( setEvent(EnrollContract.EnrollEvent.FetchCourseDetail(fetchEnrollState = LoadState.Loading, courseDetail = null)) getCourseDetailUseCase(courseId = courseId).onSuccess { courseDetail -> setEvent(EnrollContract.EnrollEvent.FetchCourseDetail(fetchEnrollState = LoadState.Success, courseDetail = courseDetail.copy(startAt = courseDetail.startAt.substringBefore(NEAREST_DATE_START_OUTPUT_FORMAT)))) - }.onFailure { - setEvent(EnrollContract.EnrollEvent.FetchCourseDetail(fetchEnrollState = LoadState.Error, courseDetail = null)) } + setEvent(EnrollContract.EnrollEvent.FetchCourseDetail(fetchEnrollState = LoadState.Error, courseDetail = null)) } } @@ -214,8 +175,4 @@ class EnrollViewModel @Inject constructor( } } } - - companion object { - private const val DEBOUNCE_TIME_MILLS = 300L - } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a622fa7..dc363f2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -251,6 +251,7 @@ ์ด ๋น„์šฉ์„ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š” ๋ฐ์ดํŠธ ์˜ˆ์ƒ ์ด ๋น„์šฉ์„ ์ˆซ์ž๋กœ๋งŒ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š” ์™„๋ฃŒ + ๋Œ€ํ‘œ %s๋‹˜, ์˜ค๋Š˜์€\n์ด๋Ÿฐ ๋ฐ์ดํŠธ ์ฝ”์Šค ์–ด๋– ์„ธ์š”? From a721c0a80e3cdcab84647ecfa913baeb665f8dd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=84=EC=9A=B0?= <85734140+jinuemong@users.noreply.github.com> Date: Sat, 29 Mar 2025 12:03:49 +0900 Subject: [PATCH 29/49] =?UTF-8?q?[feat]=20=EC=BD=94=EC=8A=A4=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20&=20=EB=94=94=EC=9E=90=EC=9D=B8=20?= =?UTF-8?q?=EC=9A=94=EA=B5=AC=EC=82=AC=ED=95=AD=20=EC=A0=81=EC=9A=A9=20=20?= =?UTF-8?q?(#18)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: ํฌ์ธํŠธ ๋ถ€์กฑ ์‹œ ์ฝ”์Šค ๋“ฑ๋ก ์—ฐ๊ฒฐ * feat: ์ฝ”์Šค ์ƒ์„ธ์—์„œ ํฌ์ธํŠธ ๋ถ€์กฑ ์‹œ ์ฝ”์Šค ๋“ฑ๋ก ์—ฐ๊ฒฐ * feat: ์ค‘๋ณต ๋กœ์ง ์ œ๊ฑฐ * refactor : ํฌ์ธํŠธ ์ˆ˜์ง‘ ui ๋ณ€๊ฒฝ์‚ฌํ•ญ ์ ์šฉ * style: ์ฝ”๋“œ ํฌ๋งท ์ˆ˜์ • & ๋ฆฐํŠธ ์ฒดํฌ --- .../bottomsheet/DateRoadPointBottomSheet.kt | 14 +++- .../ui/coursedetail/CourseDetailScreen.kt | 69 +++---------------- .../ui/pointhistory/PointHistoryContract.kt | 2 + .../ui/pointhistory/PointHistoryScreen.kt | 46 ++++++++++--- app/src/main/res/values/strings.xml | 5 +- 5 files changed, 60 insertions(+), 76 deletions(-) diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadPointBottomSheet.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadPointBottomSheet.kt index f4a0407..f463f91 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadPointBottomSheet.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadPointBottomSheet.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.sopt.teamdateroad.R +import org.sopt.teamdateroad.domain.util.PointCollect import org.sopt.teamdateroad.presentation.ui.component.bottomsheet.model.collect.DateRoadCollectPointType import org.sopt.teamdateroad.presentation.util.modifier.noRippleClickable import org.sopt.teamdateroad.ui.theme.DateRoadTheme @@ -30,6 +31,7 @@ import org.sopt.teamdateroad.ui.theme.DateRoadTheme @Composable fun DateRoadPointBottomSheet( isBottomSheetOpen: Boolean, + title: String, onClick: (DateRoadCollectPointType) -> Unit, onDismissRequest: () -> Unit = {} ) { @@ -47,7 +49,7 @@ fun DateRoadPointBottomSheet( ) ) { Text( - text = stringResource(R.string.point_box_get_point_button_text), + text = title, color = DateRoadTheme.colors.black, style = DateRoadTheme.typography.bodyBold17, modifier = Modifier.align(Alignment.CenterStart) @@ -80,6 +82,11 @@ fun DateRoadBottonSheetContent( dateLoadCollectPoint: DateRoadCollectPointType, onClick: (DateRoadCollectPointType) -> Unit ) { + val pointAmount = when (dateLoadCollectPoint) { + DateRoadCollectPointType.WATCH_ADS -> PointCollect.ADS + DateRoadCollectPointType.COURSE_REGISTRATION -> PointCollect.COURSE + } + Row( modifier = Modifier .fillMaxWidth() @@ -109,7 +116,7 @@ fun DateRoadBottonSheetContent( ) Spacer(modifier = Modifier.height(4.dp)) Text( - text = stringResource(dateLoadCollectPoint.contentRes), + text = stringResource(dateLoadCollectPoint.contentRes, pointAmount), color = DateRoadTheme.colors.gray400, style = DateRoadTheme.typography.bodySemi13 @@ -129,6 +136,7 @@ fun DateRoadBottonSheetContent( fun DateRoadPointBottomSheetPreView() { DateRoadPointBottomSheet( isBottomSheetOpen = true, - onClick = {} + onClick = {}, + title = "" ) } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailScreen.kt index 46b6dd7..9a97bb8 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailScreen.kt @@ -17,7 +17,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -27,23 +26,16 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.PagerState -import com.google.android.gms.ads.AdRequest -import com.google.android.gms.ads.LoadAdError -import com.google.android.gms.ads.rewarded.RewardedAd -import com.google.android.gms.ads.rewarded.RewardedAdLoadCallback -import org.sopt.teamdateroad.BuildConfig import org.sopt.teamdateroad.R import org.sopt.teamdateroad.domain.model.CourseDetail import org.sopt.teamdateroad.domain.model.Place import org.sopt.teamdateroad.presentation.type.CourseDetailUnopenedDetailType import org.sopt.teamdateroad.presentation.type.DateTagType.Companion.getDateTagTypeByName import org.sopt.teamdateroad.presentation.type.EnrollType -import org.sopt.teamdateroad.presentation.type.OneButtonDialogWithDescriptionType import org.sopt.teamdateroad.presentation.type.TwoButtonDialogWithDescriptionType import org.sopt.teamdateroad.presentation.ui.component.bottomsheet.DateRoadBasicBottomSheet import org.sopt.teamdateroad.presentation.ui.component.bottomsheet.DateRoadPointBottomSheet import org.sopt.teamdateroad.presentation.ui.component.bottomsheet.model.collect.DateRoadCollectPointType -import org.sopt.teamdateroad.presentation.ui.component.dialog.DateRoadOneButtonDialogWithDescription import org.sopt.teamdateroad.presentation.ui.component.dialog.DateRoadTwoButtonDialogWithDescription import org.sopt.teamdateroad.presentation.ui.component.pager.DateRoadImagePager import org.sopt.teamdateroad.presentation.ui.component.topbar.DateRoadScrollResponsiveTopBar @@ -55,7 +47,6 @@ import org.sopt.teamdateroad.presentation.ui.coursedetail.component.CourseDetail import org.sopt.teamdateroad.presentation.ui.coursedetail.component.CourseDetailBottomBar import org.sopt.teamdateroad.presentation.ui.coursedetail.component.CourseDetailUnopenedDetail import org.sopt.teamdateroad.presentation.ui.coursedetail.component.courseDetailOpenedDetail -import org.sopt.teamdateroad.presentation.util.AdsAmplitude import org.sopt.teamdateroad.presentation.util.CourseDetail.POINT_LACK import org.sopt.teamdateroad.presentation.util.CourseDetailAmplitude.CLICK_COURSE_BACK import org.sopt.teamdateroad.presentation.util.CourseDetailAmplitude.CLICK_COURSE_PURCHASE @@ -79,8 +70,6 @@ fun CourseDetailRoute( ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val lifecycleOwner = LocalLifecycleOwner.current - val context = LocalContext.current - val adRequest = remember { AdRequest.Builder().build() } LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { viewModel.sideEffect.flowWithLifecycle(lifecycle = lifecycleOwner.lifecycle) @@ -88,25 +77,6 @@ fun CourseDetailRoute( when (courseDetailSideEffect) { is CourseDetailContract.CourseDetailSideEffect.NavigateToEnroll -> navigateToEnroll(courseDetailSideEffect.enrollType, courseDetailSideEffect.viewPath, courseDetailSideEffect.id) is CourseDetailContract.CourseDetailSideEffect.PopBackStack -> popBackStack() - CourseDetailContract.CourseDetailSideEffect.NavigateToAds -> { - RewardedAd.load( - context, - BuildConfig.GOOGLE_ADS_API_ID, - adRequest, - object : RewardedAdLoadCallback() { - override fun onAdLoaded(ad: RewardedAd) { - viewModel.postAdsPoint() - } - - override fun onAdFailedToLoad(error: LoadAdError) { - when (error.code) { - AdRequest.ERROR_CODE_NO_FILL -> viewModel.setEvent(CourseDetailContract.CourseDetailEvent.FullAds) - else -> viewModel.setEvent(CourseDetailContract.CourseDetailEvent.FailLoadAdsPoint) - } - } - } - ) - } } } } @@ -175,22 +145,11 @@ fun CourseDetailRoute( viewModel.setEvent(CourseDetailContract.CourseDetailEvent.OnReportWebViewClicked) }, onReportWebViewClose = { viewModel.setEvent(CourseDetailContract.CourseDetailEvent.DismissReportWebView) }, - onDismissCollectPoint = { - AmplitudeUtils.trackEvent(eventName = AdsAmplitude.CLICK_COLLECT_POINT_CLOSE) - viewModel.setEvent(CourseDetailContract.CourseDetailEvent.DismissDialogPointLack) - }, onSelectEnroll = { - AmplitudeUtils.trackEvent(eventName = AdsAmplitude.CLICK_COURSE) - viewModel.setEvent(CourseDetailContract.CourseDetailEvent.DismissDialogPointLack) viewModel.setSideEffect(CourseDetailContract.CourseDetailSideEffect.NavigateToEnroll(enrollType = EnrollType.COURSE, viewPath = COURSE_DETAIL, id = null)) }, - onSelectAds = { - AmplitudeUtils.trackEvent(eventName = AdsAmplitude.CLICK_AD) + onDismissCollectPoint = { viewModel.setEvent(CourseDetailContract.CourseDetailEvent.DismissDialogPointLack) - viewModel.setSideEffect(CourseDetailContract.CourseDetailSideEffect.NavigateToAds) - }, - onDismissFullAdsDialog = { - viewModel.setEvent(CourseDetailContract.CourseDetailEvent.DismissFullAdsDialog) } ) } @@ -242,9 +201,7 @@ fun CourseDetailScreen( dismissReportCourseBottomSheet: () -> Unit, enrollSchedule: () -> Unit, onTopBarIconClicked: () -> Unit, - openCourseDetail: () -> Unit, - onSelectAds: () -> Unit, - onDismissFullAdsDialog: () -> Unit + openCourseDetail: () -> Unit ) { var imageHeight by remember { mutableIntStateOf(0) } @@ -393,24 +350,20 @@ fun CourseDetailScreen( ) } - if (courseDetailUiState.isFullAdsDialogOpen) { - DateRoadOneButtonDialogWithDescription( - oneButtonDialogWithDescriptionType = OneButtonDialogWithDescriptionType.FULL_ADS, - onDismissRequest = onDismissFullAdsDialog, - onClickConfirm = onDismissFullAdsDialog - ) - } - DateRoadPointBottomSheet( isBottomSheetOpen = courseDetailUiState.isPointCollectBottomSheetOpen, title = stringResource(R.string.point_box_lack_point_button_text), onClick = { dateRoadCollectPointType -> when (dateRoadCollectPointType) { - DateRoadCollectPointType.WATCH_ADS -> onSelectAds() + // TODO : add ADS + DateRoadCollectPointType.WATCH_ADS -> Unit DateRoadCollectPointType.COURSE_REGISTRATION -> onSelectEnroll() } + onDismissCollectPoint() }, - onDismissRequest = onDismissCollectPoint + onDismissRequest = { + onDismissCollectPoint() + } ) DateRoadBasicBottomSheet( @@ -469,12 +422,10 @@ fun CourseDetailScreenPreview() { places = listOf( Place( title = "Place 1", - address = "Address 1", duration = "1" ), Place( title = "Place 2", - address = "Address 2", duration = "2" ) ), @@ -510,9 +461,7 @@ fun CourseDetailScreenPreview() { dismissDialogDeleteCourse = {}, onDismissCollectPoint = {}, onSelectEnroll = {}, - dismissDialogReportCourse = {}, - onSelectAds = {}, - onDismissFullAdsDialog = {} + dismissDialogReportCourse = {} ) } } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryContract.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryContract.kt index edccc6a..79dc391 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryContract.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryContract.kt @@ -2,6 +2,7 @@ package org.sopt.teamdateroad.presentation.ui.pointhistory import org.sopt.teamdateroad.domain.model.PointHistory import org.sopt.teamdateroad.domain.model.UserPoint +import org.sopt.teamdateroad.presentation.type.EnrollType import org.sopt.teamdateroad.presentation.type.PointHistoryTabType import org.sopt.teamdateroad.presentation.util.base.UiEvent import org.sopt.teamdateroad.presentation.util.base.UiSideEffect @@ -19,6 +20,7 @@ class PointHistoryContract { sealed interface PointHistorySideEffect : UiSideEffect { data object PopBackStack : PointHistorySideEffect + data class NavigateToEnroll(val enrollType: EnrollType, val viewPath: String, val id: Int?) : PointHistorySideEffect } sealed class PointHistoryEvent : UiEvent { diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryScreen.kt index 778bb45..140953a 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryScreen.kt @@ -26,8 +26,10 @@ import org.sopt.teamdateroad.domain.model.Point import org.sopt.teamdateroad.domain.model.PointHistory import org.sopt.teamdateroad.domain.model.UserPoint import org.sopt.teamdateroad.presentation.type.EmptyViewType +import org.sopt.teamdateroad.presentation.type.EnrollType import org.sopt.teamdateroad.presentation.type.PointHistoryTabType import org.sopt.teamdateroad.presentation.ui.component.bottomsheet.DateRoadPointBottomSheet +import org.sopt.teamdateroad.presentation.ui.component.bottomsheet.model.collect.DateRoadCollectPointType import org.sopt.teamdateroad.presentation.ui.component.tabbar.DateRoadTabBar import org.sopt.teamdateroad.presentation.ui.component.tabbar.DateRoadTabTitle import org.sopt.teamdateroad.presentation.ui.component.topbar.DateRoadBasicTopBar @@ -37,6 +39,7 @@ import org.sopt.teamdateroad.presentation.ui.component.view.DateRoadIdleView import org.sopt.teamdateroad.presentation.ui.component.view.DateRoadLoadingView import org.sopt.teamdateroad.presentation.ui.pointhistory.component.PointHistoryCard import org.sopt.teamdateroad.presentation.ui.pointhistory.component.PointHistoryPointBox +import org.sopt.teamdateroad.presentation.util.ViewPath.POINT_HISTORY import org.sopt.teamdateroad.presentation.util.view.LoadState import org.sopt.teamdateroad.ui.theme.DATEROADTheme import org.sopt.teamdateroad.ui.theme.DateRoadTheme @@ -45,7 +48,8 @@ import org.sopt.teamdateroad.ui.theme.DateRoadTheme fun PointHistoryRoute( padding: PaddingValues, viewModel: PointHistoryViewModel = hiltViewModel(), - popBackStack: () -> Unit + popBackStack: () -> Unit, + navigateToEnroll: (EnrollType, String, Int?) -> Unit ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val lifecycleOwner = LocalLifecycleOwner.current @@ -60,6 +64,11 @@ fun PointHistoryRoute( .collect { pointHistorySideEffect -> when (pointHistorySideEffect) { is PointHistoryContract.PointHistorySideEffect.PopBackStack -> popBackStack() + is PointHistoryContract.PointHistorySideEffect.NavigateToEnroll -> navigateToEnroll( + pointHistorySideEffect.enrollType, + pointHistorySideEffect.viewPath, + pointHistorySideEffect.id + ) } } } @@ -80,7 +89,16 @@ fun PointHistoryRoute( }, onTopBarIconClicked = { viewModel.setSideEffect(PointHistoryContract.PointHistorySideEffect.PopBackStack) }, onClickCollectPoint = { viewModel.setEvent(PointHistoryContract.PointHistoryEvent.OnPointCollectBottomSheetClick) }, - onDisMissCollectPoint = { viewModel.setEvent(PointHistoryContract.PointHistoryEvent.OnPointCollectBottomSheetDismiss) } + onDisMissCollectPoint = { viewModel.setEvent(PointHistoryContract.PointHistoryEvent.OnPointCollectBottomSheetDismiss) }, + onSelectEnroll = { + viewModel.setSideEffect( + PointHistoryContract.PointHistorySideEffect.NavigateToEnroll( + enrollType = EnrollType.COURSE, + viewPath = POINT_HISTORY, + id = null + ) + ) + } ) } @@ -95,7 +113,8 @@ fun PointHistoryScreen( onTabBarClicked: (PointHistoryTabType) -> Unit, onTopBarIconClicked: () -> Unit, onClickCollectPoint: () -> Unit, - onDisMissCollectPoint: () -> Unit + onDisMissCollectPoint: () -> Unit, + onSelectEnroll: () -> Unit ) { Column( modifier = Modifier @@ -152,19 +171,23 @@ fun PointHistoryScreen( } items(pointHistory.size) { index -> PointHistoryCard(point = pointHistory[index]) - if (index < pointHistory.size - 1) { - HorizontalDivider( - color = DateRoadTheme.colors.gray100, - thickness = 1.dp - ) - } + HorizontalDivider( + color = DateRoadTheme.colors.gray100, + thickness = 1.dp + ) } } } DateRoadPointBottomSheet( isBottomSheetOpen = pointHistoryUiState.isPointCollectBottomSheetOpen, - onClick = { + title = stringResource(R.string.point_box_get_point_button_text), + onClick = { dateRoadCollectPointType -> + when (dateRoadCollectPointType) { + // TODO : add ADS + DateRoadCollectPointType.WATCH_ADS -> Unit + DateRoadCollectPointType.COURSE_REGISTRATION -> onSelectEnroll() + } onDisMissCollectPoint() }, onDismissRequest = { @@ -194,7 +217,8 @@ fun PointHistoryPreview() { onTabBarClicked = {}, onTopBarIconClicked = {}, onClickCollectPoint = {}, - onDisMissCollectPoint = {} + onDisMissCollectPoint = {}, + onSelectEnroll = {} ) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dc363f2..18e211a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -192,6 +192,7 @@ %1$s\๋‹˜์˜ ํฌ์ธํŠธ %1$d P ํฌ์ธํŠธ ๋ชจ์œผ๋Ÿฌ ๊ฐ€๊ธฐ + ํฌ์ธํŠธ๊ฐ€ ๋ถ€์กฑํ•ด์š”! ํฌ์ธํŠธ ๋‚ด์—ญ ๋ณด๊ธฐ @@ -287,9 +288,9 @@ ๊ด‘๊ณ  ์‹œ์ฒญํ•˜๊ธฐ - ์งง์€ ๊ด‘๊ณ  ๋ณด๊ณ  ๋ฐ”๋กœ ํฌ์ธํŠธ ํš๋“ํ•˜๊ธฐ + ์งง์€ ๊ด‘๊ณ  ๋ณด๊ณ  ๋ฐ”๋กœ %dP ํš๋“ํ•˜๊ธฐ ์ฝ”์Šค ๋“ฑ๋กํ•˜๊ธฐ - ๋‚˜๋งŒ์˜ ์ฝ”์Šค ๋“ฑ๋กํ•˜๊ณ  ํฌ์ธํŠธ ํš๋“ํ•˜๊ธฐ + ๋‚˜๋งŒ์˜ ์ฝ”์Šค ๋“ฑ๋กํ•˜๊ณ  %dP ํš๋“ํ•˜๊ธฐ ๋‚ด ํ”„๋กœํ•„ From 7cdcd9d7873b3b7a49790dd829a5c95d66cfe579 Mon Sep 17 00:00:00 2001 From: Yaechan Park <102402485+dpcks0509@users.noreply.github.com> Date: Sun, 30 Mar 2025 19:09:45 +0900 Subject: [PATCH 30/49] =?UTF-8?q?[feat]=20=EC=9E=A5=EC=86=8C=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EB=B0=94=ED=85=80=EC=8B=9C=ED=8A=B8=20UI=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0=20(#19)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: bottomsheet preview ๋ฐ ์˜คํƒ€ ์ˆ˜์ • * feat: DateRoadPlaceSearchBottomSheet ์ดˆ๊ธฐ UI ๊ตฌํ˜„ * feat: ์žฅ์†Œ๊ฒ€์ƒ‰ ๋ฐ”ํ…€์‹œํŠธ ViewModel ์—ฐ๊ฒฐ * refactor: ์žฅ์†Œ ๊ฒ€์ƒ‰ ํƒ€์ดํ•‘์‹œ ์ž๋™ ๊ฒ€์ƒ‰ ๋˜๊ฒŒ ์ˆ˜์ • * refactor: PlaceSearchBottomSheet ๋…ธ์ถœ ํฌ๊ธฐ ์ˆ˜์ • * feat: ์žฅ์†Œ๊ฒ€์ƒ‰ BottomSheet ํ‚ค๋ณด๋“œ ์•ˆ์˜ฌ๋ผ์˜ค๊ฒŒ ๊ตฌํ˜„ * feat: ์žฅ์†Œ ํ‚ค์›Œ๋“œ ์ผ์น˜ ํ•˜์ด๋ผ์ดํŠธ ๊ตฌํ˜„ * refactor: DateRoadPlaceSearchBottomSheet height ์ˆ˜์ • * fix: git ์ถฉ๋Œ ํ•ด๊ฒฐ * refactor: DateRoadPlaceSearchBottomSheet isDraggable false ์ถ”๊ฐ€ * refactor: EnrollPlaceSearchItem ํ„ฐ์น˜ ํฌ๊ธฐ ๋ฒ”์œ„ ์ˆ˜์ • * refactor: BOTTOM_SHEET_OPEN_RATIO ์ƒ์ˆ˜ํ™” * refactor: onValueChange์— onKeywordChanged ์„ค์ • ์‹ฌํ”Œํ•˜๊ฒŒ ์ˆ˜์ • * refactor: EmptyPlaceSearchResult ์ปดํฌ๋„ŒํŠธ ๋ถ„๋ฆฌ * refactor: ktLint ์ ์šฉ --- app/build.gradle.kts | 3 + .../DateRoadPlaceSearchBottomSheet.kt | 249 ++++++++++++++++++ .../bottomsheet/DateRoadPointBottomSheet.kt | 6 +- .../bottomsheet/DefaultDateRoadBottomSheet.kt | 9 + .../presentation/ui/enroll/EnrollContract.kt | 5 +- .../presentation/ui/enroll/EnrollScreen.kt | 23 +- .../ui/enroll/EnrollSecondScreen.kt | 16 +- .../presentation/ui/enroll/EnrollViewModel.kt | 30 ++- app/src/main/res/values/strings.xml | 2 + 9 files changed, 322 insertions(+), 21 deletions(-) create mode 100644 app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadPlaceSearchBottomSheet.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2832b3f..98a5785 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -127,6 +127,9 @@ dependencies { // Lottie implementation(libs.lottie.compose) + + // BottomSheet + implementation(libs.bottom.sheet) } ktlint { diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadPlaceSearchBottomSheet.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadPlaceSearchBottomSheet.kt new file mode 100644 index 0000000..5e22de8 --- /dev/null +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadPlaceSearchBottomSheet.kt @@ -0,0 +1,249 @@ +package org.sopt.teamdateroad.presentation.ui.component.bottomsheet + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Button +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.holix.android.bottomsheetdialog.compose.BottomSheetBehaviorProperties +import com.holix.android.bottomsheetdialog.compose.BottomSheetDialog +import com.holix.android.bottomsheetdialog.compose.BottomSheetDialogProperties +import org.sopt.teamdateroad.R +import org.sopt.teamdateroad.domain.model.PlaceInfo +import org.sopt.teamdateroad.domain.model.PlaceSearchResult +import org.sopt.teamdateroad.presentation.ui.enroll.component.EnrollPlaceSearchItem +import org.sopt.teamdateroad.presentation.util.modifier.noRippleClickable +import org.sopt.teamdateroad.ui.theme.DATEROADTheme +import org.sopt.teamdateroad.ui.theme.DateRoadTheme + +@Composable +fun DateRoadPlaceSearchBottomSheet( + isBottomSheetOpen: Boolean, + keyword: String, + placeSearchResult: PlaceSearchResult, + onKeywordChanged: (String) -> Unit, + onPlaceSelected: (PlaceInfo) -> Unit, + onDismissRequest: () -> Unit = {} +) { + if (isBottomSheetOpen) { + BottomSheetDialog( + onDismissRequest = onDismissRequest, + properties = BottomSheetDialogProperties( + behaviorProperties = BottomSheetBehaviorProperties( + state = BottomSheetBehaviorProperties.State.HalfExpanded, + halfExpandedRatio = BOTTOM_SHEET_OPEN_RATIO, + skipCollapsed = true, + isDraggable = false + ) + ) + ) { + Column( + modifier = Modifier + .clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)) + .background(DateRoadTheme.colors.white) + ) { + Spacer(modifier = Modifier.height(23.dp)) + + Row( + modifier = Modifier + .fillMaxWidth() + .height(40.dp) + .padding(start = 25.dp, end = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(R.string.enroll_place_search_bottom_sheet_title), + color = DateRoadTheme.colors.black, + style = DateRoadTheme.typography.bodyBold17 + ) + + Spacer(modifier = Modifier.weight(1f)) + + Image( + modifier = Modifier + .size(40.dp) + .noRippleClickable(onClick = onDismissRequest) + .padding(15.dp), + painter = painterResource(id = R.drawable.ic_bottom_sheet_close), + contentDescription = null + ) + } + + Spacer(modifier = Modifier.height(22.dp)) + + TextField( + value = keyword, + onValueChange = onKeywordChanged, + modifier = Modifier + .fillMaxWidth() + .height(54.dp) + .padding(start = 14.dp, end = 20.dp), + placeholder = { + Text( + modifier = Modifier, + text = stringResource(R.string.enroll_place_insert_bar_enter_place_placeholder), + color = DateRoadTheme.colors.gray300, + style = DateRoadTheme.typography.bodySemi15 + ) + }, + trailingIcon = { + IconButton( + modifier = Modifier.size(20.dp), + onClick = { + onKeywordChanged("") + } + ) { + Icon( + painter = painterResource(id = R.drawable.btn_enroll_delete_picture), + contentDescription = null, + tint = Color.Unspecified + ) + } + }, + colors = TextFieldDefaults.colors( + focusedContainerColor = DateRoadTheme.colors.gray100, + unfocusedContainerColor = DateRoadTheme.colors.gray100, + cursorColor = DateRoadTheme.colors.purple600, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent + ), + shape = RoundedCornerShape(14.dp), + singleLine = true, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Search + ) + ) + + Spacer(modifier = Modifier.height(10.dp)) + + if (placeSearchResult.placeInfos.isNotEmpty()) { + LazyColumn(modifier = Modifier.weight(1f)) { + itemsIndexed( + items = placeSearchResult.placeInfos, + key = { index, placeInfo -> placeInfo.hashCode() + index } + ) { index, placeInfo -> + EnrollPlaceSearchItem( + keyword = keyword, + placeInfo = placeInfo, + onClick = { onPlaceSelected(placeInfo) } + ) + + if (index != placeSearchResult.placeInfos.lastIndex) { + HorizontalDivider( + modifier = Modifier.fillMaxWidth(), + color = DateRoadTheme.colors.gray100, + thickness = 1.dp + ) + } + } + } + } else { + EmptyPlaceSearchResult() + } + } + } + } +} + +@Composable +private fun EmptyPlaceSearchResult() { + Box( + modifier = Modifier + .fillMaxSize() + .padding(bottom = 70.dp) + ) { + Column(modifier = Modifier.align(Alignment.Center)) { + Image( + modifier = Modifier + .width(167.dp) + .height(191.dp), + painter = painterResource(R.drawable.img_place_search_no_match), + contentDescription = null + ) + Spacer(modifier = Modifier.height(40.dp)) + Text( + text = stringResource(R.string.enroll_place_search_no_match), + color = DateRoadTheme.colors.gray300, + style = DateRoadTheme.typography.titleBold18 + ) + } + } +} + +@Preview +@Composable +fun DateRoadPlaceSearchBottomSheetPreview() { + DATEROADTheme { + var isBottomSheetOpen by rememberSaveable { mutableStateOf(false) } + var text by rememberSaveable { mutableStateOf("") } + + Button(onClick = { isBottomSheetOpen = true }) { + Text( + text = "DateRoadPlaceSearchBottomSheet", + color = DateRoadTheme.colors.black, + style = DateRoadTheme.typography.titleBold18 + ) + } + + DateRoadPlaceSearchBottomSheet( + isBottomSheetOpen = isBottomSheetOpen, + keyword = text, + placeSearchResult = PlaceSearchResult( + List(10) { + PlaceInfo( + "์นดํŽ˜ ๋‚˜๋ž‘", + "๊ฒฝ๊ธฐ ์˜์™•์‹œ ์ฒญ๊ณ„๋กœ 217" + ) + } + ), + onKeywordChanged = { text = it }, + onPlaceSelected = {}, + onDismissRequest = { isBottomSheetOpen = !isBottomSheetOpen } + ) + } +} + +@Preview(showBackground = true) +@Composable +fun EmptyPlaceSearchResultPreview() { + DATEROADTheme { + EmptyPlaceSearchResult() + } +} + +private const val BOTTOM_SHEET_OPEN_RATIO = 0.8f diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadPointBottomSheet.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadPointBottomSheet.kt index f463f91..5a24ef5 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadPointBottomSheet.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadPointBottomSheet.kt @@ -64,11 +64,11 @@ fun DateRoadPointBottomSheet( ) } Spacer(modifier = Modifier.height(17.dp)) - DateRoadBottonSheetContent( + DateRoadBottomSheetContent( dateLoadCollectPoint = DateRoadCollectPointType.WATCH_ADS, onClick = onClick ) - DateRoadBottonSheetContent( + DateRoadBottomSheetContent( dateLoadCollectPoint = DateRoadCollectPointType.COURSE_REGISTRATION, onClick = onClick ) @@ -78,7 +78,7 @@ fun DateRoadPointBottomSheet( } @Composable -fun DateRoadBottonSheetContent( +fun DateRoadBottomSheetContent( dateLoadCollectPoint: DateRoadCollectPointType, onClick: (DateRoadCollectPointType) -> Unit ) { diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DefaultDateRoadBottomSheet.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DefaultDateRoadBottomSheet.kt index 8023a8f..1398558 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DefaultDateRoadBottomSheet.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DefaultDateRoadBottomSheet.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.windowInsetsBottomHeight import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.SheetState @@ -68,6 +69,14 @@ fun DefaultDateRoadBottomSheetPreview() { DATEROADTheme { var isBottomSheetOpen by rememberSaveable { mutableStateOf(false) } + Button(onClick = { isBottomSheetOpen = true }) { + Text( + text = "DefaultDateRoadBottomSheet", + color = DateRoadTheme.colors.black, + style = DateRoadTheme.typography.titleExtra20 + ) + } + DefaultDateRoadBottomSheet( modifier = Modifier.padding(top = 20.dp, bottom = 16.dp, start = 16.dp, end = 16.dp), isBottomSheetOpen = isBottomSheetOpen, diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollContract.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollContract.kt index 28fc395..3e5b010 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollContract.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollContract.kt @@ -4,6 +4,7 @@ import org.sopt.teamdateroad.domain.model.CourseDetail import org.sopt.teamdateroad.domain.model.Enroll import org.sopt.teamdateroad.domain.model.Place import org.sopt.teamdateroad.domain.model.PlaceInfo +import org.sopt.teamdateroad.domain.model.PlaceSearchResult import org.sopt.teamdateroad.domain.model.TimelineDetail import org.sopt.teamdateroad.domain.type.RegionType import org.sopt.teamdateroad.presentation.type.EnrollScreenType @@ -56,8 +57,10 @@ class EnrollContract { val onRegionBottomSheetRegionSelected: RegionType? = RegionType.SEOUL, val onRegionBottomSheetAreaSelected: Any? = null, val isPlaceSearchBottomSheetOpen: Boolean = false, + val keyword: String = "", val place: Place = Place(), - val selectedPlaceInfos: List = emptyList(), + val placeSearchResult: PlaceSearchResult = PlaceSearchResult(placeInfos = emptyList()), + val placeInfos: List = emptyList(), val isPlaceEditable: Boolean = true, val isDurationBottomSheetOpen: Boolean = false, val durationPicker: List = listOf(Picker(items = (DURATION_START..DURATION_END).map { (it * 0.5).toString() })), diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt index 5f79966..9bf5968 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt @@ -33,6 +33,7 @@ import java.time.LocalDate import java.time.format.DateTimeFormatter import org.sopt.teamdateroad.R import org.sopt.teamdateroad.domain.model.Place +import org.sopt.teamdateroad.domain.model.PlaceInfo import org.sopt.teamdateroad.domain.type.RegionType import org.sopt.teamdateroad.presentation.type.DateRoadRegionBottomSheetType import org.sopt.teamdateroad.presentation.type.DateTagType @@ -253,6 +254,10 @@ fun EnrollRoute( onDateTextFieldClick = { viewModel.setEvent(EnrollContract.EnrollEvent.OnDateTextFieldClick) }, onTimeTextFieldClick = { viewModel.setEvent(EnrollContract.EnrollEvent.OnTimeTextFieldClick) }, onRegionTextFieldClick = { viewModel.setEvent(EnrollContract.EnrollEvent.OnRegionTextFieldClick) }, + onPlaceSearchButtonClick = { viewModel.setEvent(EnrollContract.EnrollEvent.OnPlaceSearchButtonClick) }, + onKeywordChanged = { keyword -> viewModel.setEvent(EnrollContract.EnrollEvent.OnKeywordChanged(keyword = keyword)) }, + onPlaceSelected = { placeInfo -> viewModel.setEvent(EnrollContract.EnrollEvent.OnPlaceSelected(placeInfo = placeInfo)) }, + onPlaceSearchBottomSheetDismiss = { viewModel.setEvent(EnrollContract.EnrollEvent.OnPlaceSearchBottomSheetDismiss) }, onSelectedPlaceCourseTimeClick = { viewModel.setEvent(EnrollContract.EnrollEvent.OnSelectedPlaceCourseTimeClick) }, onDatePickerBottomSheetDismissRequest = { viewModel.setEvent(EnrollContract.EnrollEvent.OnDatePickerBottomSheetDismissRequest) }, onTimePickerBottomSheetDismissRequest = { viewModel.setEvent(EnrollContract.EnrollEvent.OnTimePickerBottomSheetDismissRequest) }, @@ -284,7 +289,6 @@ fun EnrollRoute( onRegionBottomSheetButtonClick = { region: RegionType?, area: Any? -> viewModel.setEvent(EnrollContract.EnrollEvent.OnRegionBottomSheetButtonClick(region = region, area = area)) }, onAddPlaceButtonClick = { place -> viewModel.setEvent(EnrollContract.EnrollEvent.OnAddPlaceButtonClick(place = place)) }, onPlaceCardDragAndDrop = { places -> viewModel.setEvent(EnrollContract.EnrollEvent.OnPlaceCardDragAndDrop(places = places)) }, - onPlaceTitleValueChange = { placeTitle -> viewModel.setEvent(EnrollContract.EnrollEvent.OnPlaceTitleValueChange(placeTitle = placeTitle)) }, onDurationBottomSheetButtonClick = { placeDuration -> viewModel.setEvent(EnrollContract.EnrollEvent.OnDurationBottomSheetButtonClick(placeDuration = placeDuration)) }, onPlaceEditButtonClick = { editable -> viewModel.setEvent(EnrollContract.EnrollEvent.OnEditableValueChange(editable = editable)) }, onPlaceCardDeleteButtonClick = { index -> viewModel.setEvent(EnrollContract.EnrollEvent.OnPlaceCardDeleteButtonClick(index = index)) }, @@ -337,6 +341,10 @@ fun EnrollScreen( onDateTextFieldClick: () -> Unit, onTimeTextFieldClick: () -> Unit, onRegionTextFieldClick: () -> Unit, + onPlaceSearchButtonClick: () -> Unit, + onKeywordChanged: (String) -> Unit, + onPlaceSelected: (PlaceInfo) -> Unit, + onPlaceSearchBottomSheetDismiss: () -> Unit, onSelectedPlaceCourseTimeClick: () -> Unit, onDatePickerBottomSheetDismissRequest: () -> Unit, onTimePickerBottomSheetDismissRequest: () -> Unit, @@ -353,7 +361,6 @@ fun EnrollScreen( onRegionBottomSheetButtonClick: (RegionType?, Any?) -> Unit, onAddPlaceButtonClick: (Place) -> Unit, onPlaceCardDragAndDrop: (List) -> Unit, - onPlaceTitleValueChange: (String) -> Unit, onDurationBottomSheetButtonClick: (String) -> Unit, onPlaceEditButtonClick: (Boolean) -> Unit, onPlaceCardDeleteButtonClick: (Int) -> Unit, @@ -438,9 +445,12 @@ fun EnrollScreen( EnrollScreenType.SECOND -> EnrollSecondScreen( enrollUiState = enrollUiState, + onPlaceSearchButtonClick = onPlaceSearchButtonClick, + onKeywordChanged = onKeywordChanged, + onPlaceSelected = onPlaceSelected, + onPlaceSearchBottomSheetDismiss = onPlaceSearchBottomSheetDismiss, onSelectedPlaceCourseTimeClick = onSelectedPlaceCourseTimeClick, onAddPlaceButtonClick = onAddPlaceButtonClick, - onPlaceTitleValueChange = onPlaceTitleValueChange, onPlaceEditButtonClick = onPlaceEditButtonClick, onPlaceCardDeleteButtonClick = onPlaceCardDeleteButtonClick, onPlaceCardDragAndDrop = onPlaceCardDragAndDrop @@ -568,6 +578,7 @@ fun EnrollScreenPreview() { onDateTextFieldClick = {}, onTimeTextFieldClick = {}, onRegionTextFieldClick = {}, + onPlaceSearchButtonClick = {}, onSelectedPlaceCourseTimeClick = {}, onDatePickerBottomSheetDismissRequest = {}, onTimePickerBottomSheetDismissRequest = {}, @@ -583,7 +594,6 @@ fun EnrollScreenPreview() { onRegionBottomSheetAreaChipClick = {}, onRegionBottomSheetButtonClick = { _, _ -> }, onAddPlaceButtonClick = {}, - onPlaceTitleValueChange = {}, onDurationBottomSheetButtonClick = {}, onPlaceEditButtonClick = {}, onPlaceCardDeleteButtonClick = {}, @@ -591,7 +601,10 @@ fun EnrollScreenPreview() { onDescriptionValueChange = {}, onCostValueChange = {}, onEnrollSuccessDialogButtonClick = {}, - onSelectThumbnail = {} + onSelectThumbnail = {}, + onPlaceSearchBottomSheetDismiss = {}, + onKeywordChanged = { }, + onPlaceSelected = { } ) } } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollSecondScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollSecondScreen.kt index d31b3c5..9eebbe9 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollSecondScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollSecondScreen.kt @@ -37,10 +37,10 @@ import org.sopt.teamdateroad.R import org.sopt.teamdateroad.domain.model.Place import org.sopt.teamdateroad.domain.model.PlaceInfo import org.sopt.teamdateroad.presentation.type.PlaceCardType +import org.sopt.teamdateroad.presentation.ui.component.bottomsheet.DateRoadPlaceSearchBottomSheet import org.sopt.teamdateroad.presentation.ui.component.button.DateRoadTextButton import org.sopt.teamdateroad.presentation.ui.component.card.DateRoadPlaceCard import org.sopt.teamdateroad.presentation.ui.enroll.component.EnrollPlaceInsertBar -import org.sopt.teamdateroad.presentation.ui.enroll.component.PlaceSearchBottomSheet import org.sopt.teamdateroad.presentation.util.Time import org.sopt.teamdateroad.presentation.util.draganddrop.rememberDragAndDropListState import org.sopt.teamdateroad.presentation.util.mutablelist.move @@ -51,8 +51,6 @@ import org.sopt.teamdateroad.ui.theme.DateRoadTheme @Composable fun EnrollSecondScreen( enrollUiState: EnrollContract.EnrollUiState = EnrollContract.EnrollUiState(), - searchKeyword: String, - searchPlaceInfos: List, onPlaceSearchButtonClick: () -> Unit, onKeywordChanged: (String) -> Unit, onPlaceSelected: (PlaceInfo) -> Unit, @@ -100,7 +98,7 @@ fun EnrollSecondScreen( onPlaceSearchButtonClick = onPlaceSearchButtonClick, onSelectedCourseTimeClick = onSelectedPlaceCourseTimeClick, onAddCourseButtonClick = { - onAddPlaceButtonClick(Place(title = enrollUiState.place.title, address = enrollUiState.place.address, duration = enrollUiState.place.duration + Time.TIME)) + onAddPlaceButtonClick(Place(title = enrollUiState.place.title, duration = enrollUiState.place.duration + Time.TIME)) } ) Spacer(modifier = Modifier.height(22.dp)) @@ -164,8 +162,6 @@ fun EnrollSecondScreen( items(enrollUiState.enroll.places.size) { index -> DateRoadPlaceCard( modifier = Modifier - .fillMaxWidth() - .height(76.dp) .zIndex(if (index == dragDropListState.currentIndexOfDraggedItem) 1f else 0f) .graphicsLayer( scaleX = animateFloatAsState(if (dragDropListState.currentIndexOfDraggedItem == index) 1.1f else 1.0f, label = "").value, @@ -180,10 +176,10 @@ fun EnrollSecondScreen( Spacer(modifier = Modifier.height(16.dp)) } - PlaceSearchBottomSheet( + DateRoadPlaceSearchBottomSheet( isBottomSheetOpen = enrollUiState.isPlaceSearchBottomSheetOpen, - searchKeyword = searchKeyword, - searchPlaceInfos = searchPlaceInfos, + keyword = enrollUiState.keyword, + placeSearchResult = enrollUiState.placeSearchResult, onKeywordChanged = onKeywordChanged, onPlaceSelected = onPlaceSelected, onDismissRequest = onPlaceSearchBottomSheetDismiss @@ -195,8 +191,6 @@ fun EnrollSecondScreen( fun EnrollSecondScreenPreview() { DATEROADTheme { EnrollSecondScreen( - searchKeyword = "", - searchPlaceInfos = listOf(), onPlaceSearchButtonClick = {}, onAddPlaceButtonClick = {}, onSelectedPlaceCourseTimeClick = {}, diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollViewModel.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollViewModel.kt index e06298f..7e9b247 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollViewModel.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollViewModel.kt @@ -6,8 +6,10 @@ import javax.inject.Inject import kotlinx.coroutines.launch import org.sopt.teamdateroad.data.dataremote.util.Date.NEAREST_DATE_START_OUTPUT_FORMAT import org.sopt.teamdateroad.data.mapper.toEntity.toEnroll +import org.sopt.teamdateroad.domain.model.PlaceSearchResult import org.sopt.teamdateroad.domain.type.RegionType import org.sopt.teamdateroad.domain.usecase.GetCourseDetailUseCase +import org.sopt.teamdateroad.domain.usecase.GetPlaceSearchResultUseCase import org.sopt.teamdateroad.domain.usecase.GetTimelineDetailUseCase import org.sopt.teamdateroad.domain.usecase.PostCourseUseCase import org.sopt.teamdateroad.domain.usecase.PostTimelineUseCase @@ -25,7 +27,8 @@ class EnrollViewModel @Inject constructor( private val getCourseDetailUseCase: GetCourseDetailUseCase, private val getTimelineDetailUseCase: GetTimelineDetailUseCase, private val postCourseUseCase: PostCourseUseCase, - private val postTimelineUseCase: PostTimelineUseCase + private val postTimelineUseCase: PostTimelineUseCase, + private val getPlaceSearchResultUseCase: GetPlaceSearchResultUseCase ) : BaseViewModel() { override fun createInitialState(): EnrollContract.EnrollUiState = EnrollContract.EnrollUiState() @@ -74,6 +77,15 @@ class EnrollViewModel @Inject constructor( is EnrollContract.EnrollEvent.OnDateTextFieldClick -> setState { copy(isDatePickerBottomSheetOpen = true) } is EnrollContract.EnrollEvent.OnTimeTextFieldClick -> setState { copy(isTimePickerBottomSheetOpen = true) } is EnrollContract.EnrollEvent.OnRegionTextFieldClick -> setState { copy(isRegionBottomSheetOpen = true, onRegionBottomSheetRegionSelected = RegionType.SEOUL, onRegionBottomSheetAreaSelected = null) } + is EnrollContract.EnrollEvent.OnPlaceSearchButtonClick -> setState { copy(isPlaceSearchBottomSheetOpen = true) } + is EnrollContract.EnrollEvent.OnKeywordChanged -> { + setState { + copy(keyword = event.keyword) + } + getPlaceSearchResult() + } + + is EnrollContract.EnrollEvent.OnPlaceSearchBottomSheetDismiss -> setState { copy(isPlaceSearchBottomSheetOpen = false, keyword = "", placeSearchResult = PlaceSearchResult(emptyList())) } is EnrollContract.EnrollEvent.OnSelectedPlaceCourseTimeClick -> setState { copy(isDurationBottomSheetOpen = true) } is EnrollContract.EnrollEvent.OnDatePickerBottomSheetDismissRequest -> setState { copy(isDatePickerBottomSheetOpen = false) } is EnrollContract.EnrollEvent.OnTimePickerBottomSheetDismissRequest -> setState { copy(isTimePickerBottomSheetOpen = false) } @@ -93,6 +105,9 @@ class EnrollViewModel @Inject constructor( ) } is EnrollContract.EnrollEvent.OnTitleValueChange -> setState { copy(enroll = currentState.enroll.copy(title = event.title)) } + is EnrollContract.EnrollEvent.OnPlaceSelected -> { + setState { copy(keyword = "", placeSearchResult = PlaceSearchResult(emptyList()), place = currentState.place.copy(title = event.placeInfo.placeName), placeInfos = currentState.placeInfos + event.placeInfo, isPlaceSearchBottomSheetOpen = false) } + } is EnrollContract.EnrollEvent.OnDatePickerBottomSheetButtonClick -> setState { copy(enroll = currentState.enroll.copy(date = event.date), isDatePickerBottomSheetOpen = false) } @@ -175,4 +190,17 @@ class EnrollViewModel @Inject constructor( } } } + + private fun getPlaceSearchResult() { + viewModelScope.launch { + getPlaceSearchResultUseCase(keyword = currentState.keyword).fold( + onSuccess = { placeSearchResult -> + setState { copy(placeSearchResult = placeSearchResult) } + }, + onFailure = { + setEvent(EnrollContract.EnrollEvent.Enroll(loadState = LoadState.Error)) + } + ) + } + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 18e211a..06a287d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -252,6 +252,8 @@ ์ด ๋น„์šฉ์„ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š” ๋ฐ์ดํŠธ ์˜ˆ์ƒ ์ด ๋น„์šฉ์„ ์ˆซ์ž๋กœ๋งŒ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š” ์™„๋ฃŒ + ์žฅ์†Œ ๊ฒ€์ƒ‰ํ•˜๊ธฐ + ์ผ์น˜ํ•˜๋Š” ์žฅ์†Œ๊ฐ€ ์—†์–ด์š”! ๋Œ€ํ‘œ From 48f41511c5d3d627d8e058c01ce061c8c222671a Mon Sep 17 00:00:00 2001 From: Yaechan Park <102402485+dpcks0509@users.noreply.github.com> Date: Mon, 31 Mar 2025 23:21:31 +0900 Subject: [PATCH 31/49] =?UTF-8?q?[feat]=20=EC=9E=A5=EC=86=8C=EC=97=90=20?= =?UTF-8?q?=EB=8F=84=EB=A1=9C=EB=AA=85=20=EC=A3=BC=EC=86=8C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: DateRoadPlaceCard UI ์ˆ˜์ • ๋ฐ Place ๋„๋ฉ”์ธ์— address ํ•„๋“œ ์ถ”๊ฐ€ * refactor: ๋ฐ์ดํŠธ ์ฝ”์Šค ๋“ฑ๋ก API ํ•„๋“œ ์ˆ˜์ • * refactor: ๋ฐ์ดํŠธ ์ฝ”์Šค ์ƒ์„ธ ์กฐํšŒ API ํ•„๋“œ ์ˆ˜์ • * refactor: DateRoadPlaceCard ๋„๋กœ๋ช… ์ฃผ์†Œ start padding ์ˆ˜์ • * refactor: ์ƒˆ๋กœ์šด API version ๋ณ€๊ฒฝ * refactor: getPlaceSearchResult onSuccess, onFailure ๊ธฐ์กด ์ฝ”๋“œ ํฌ๋งท ํ†ต์ผ * refactor: DateRoadPlaceCard top padding ์ˆ˜์ • * refactor: ktlint ์ฒดํฌ * refactor: address nullabel ์ œ๊ฑฐ --- .../data/dataremote/util/Constraints.kt | 6 ++++-- .../ui/component/card/DateRoadPlaceCard.kt | 5 ++--- .../ui/coursedetail/CourseDetailScreen.kt | 2 ++ .../ui/enroll/EnrollSecondScreen.kt | 4 +++- .../presentation/ui/enroll/EnrollViewModel.kt | 19 +++++++++---------- 5 files changed, 20 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/util/Constraints.kt b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/util/Constraints.kt index aaedeb7..7aac26b 100644 --- a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/util/Constraints.kt +++ b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/util/Constraints.kt @@ -1,5 +1,7 @@ package org.sopt.teamdateroad.data.dataremote.util +import org.sopt.teamdateroad.data.dataremote.util.ApiConstraints.VERSION2 + object ApiConstraints { const val PROFILE_FORM_DATA_IMAGE = "image" const val COURSE_FORM_DATA_IMAGE = "images" @@ -7,6 +9,7 @@ object ApiConstraints { const val HTTPS = "https://" const val API = "api" const val VERSION = "v1" + const val VERSION2 = "v2" const val COURSES = "courses" const val DATE_ACCESS = "date-access" const val USERS = "users" @@ -77,6 +80,5 @@ object TotalCostZero { } object PlaceSearch { - private const val VERSION = "v2" - const val LOCAL_SEARCH_KEYWORD_JSON = "$VERSION/local/search/keyword.json" + const val LOCAL_SEARCH_KEYWORD_JSON = "$VERSION2/local/search/keyword.json" } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/card/DateRoadPlaceCard.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/card/DateRoadPlaceCard.kt index 58456d4..277edc6 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/card/DateRoadPlaceCard.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/card/DateRoadPlaceCard.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width @@ -41,8 +40,8 @@ fun DateRoadPlaceCard( Row( modifier = modifier - .fillMaxWidth() - .height(76.dp) +// .fillMaxWidth() +// .height(76.dp) ) { Column( modifier = Modifier diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailScreen.kt index 9a97bb8..c2f9857 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailScreen.kt @@ -422,10 +422,12 @@ fun CourseDetailScreenPreview() { places = listOf( Place( title = "Place 1", + address = "Address 1", duration = "1" ), Place( title = "Place 2", + address = "Address 2", duration = "2" ) ), diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollSecondScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollSecondScreen.kt index 9eebbe9..3ddba55 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollSecondScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollSecondScreen.kt @@ -98,7 +98,7 @@ fun EnrollSecondScreen( onPlaceSearchButtonClick = onPlaceSearchButtonClick, onSelectedCourseTimeClick = onSelectedPlaceCourseTimeClick, onAddCourseButtonClick = { - onAddPlaceButtonClick(Place(title = enrollUiState.place.title, duration = enrollUiState.place.duration + Time.TIME)) + onAddPlaceButtonClick(Place(title = enrollUiState.place.title, address = enrollUiState.place.address, duration = enrollUiState.place.duration + Time.TIME)) } ) Spacer(modifier = Modifier.height(22.dp)) @@ -162,6 +162,8 @@ fun EnrollSecondScreen( items(enrollUiState.enroll.places.size) { index -> DateRoadPlaceCard( modifier = Modifier + .fillMaxWidth() + .height(76.dp) .zIndex(if (index == dragDropListState.currentIndexOfDraggedItem) 1f else 0f) .graphicsLayer( scaleX = animateFloatAsState(if (dragDropListState.currentIndexOfDraggedItem == index) 1.1f else 1.0f, label = "").value, diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollViewModel.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollViewModel.kt index 7e9b247..b04dfb2 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollViewModel.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollViewModel.kt @@ -104,9 +104,10 @@ class EnrollViewModel @Inject constructor( thumbnailIndex = if (event.moveThumbnail) (thumbnailIndex - 1).coerceAtLeast(0) else thumbnailIndex ) } + is EnrollContract.EnrollEvent.OnTitleValueChange -> setState { copy(enroll = currentState.enroll.copy(title = event.title)) } is EnrollContract.EnrollEvent.OnPlaceSelected -> { - setState { copy(keyword = "", placeSearchResult = PlaceSearchResult(emptyList()), place = currentState.place.copy(title = event.placeInfo.placeName), placeInfos = currentState.placeInfos + event.placeInfo, isPlaceSearchBottomSheetOpen = false) } + setState { copy(keyword = "", placeSearchResult = PlaceSearchResult(emptyList()), place = currentState.place.copy(title = event.placeInfo.placeName, address = event.placeInfo.addressName), placeInfos = currentState.placeInfos + event.placeInfo, isPlaceSearchBottomSheetOpen = false) } } is EnrollContract.EnrollEvent.OnDatePickerBottomSheetButtonClick -> setState { copy(enroll = currentState.enroll.copy(date = event.date), isDatePickerBottomSheetOpen = false) } @@ -150,8 +151,9 @@ class EnrollViewModel @Inject constructor( setEvent(EnrollContract.EnrollEvent.FetchCourseDetail(fetchEnrollState = LoadState.Loading, courseDetail = null)) getCourseDetailUseCase(courseId = courseId).onSuccess { courseDetail -> setEvent(EnrollContract.EnrollEvent.FetchCourseDetail(fetchEnrollState = LoadState.Success, courseDetail = courseDetail.copy(startAt = courseDetail.startAt.substringBefore(NEAREST_DATE_START_OUTPUT_FORMAT)))) + }.onFailure { + setEvent(EnrollContract.EnrollEvent.FetchCourseDetail(fetchEnrollState = LoadState.Error, courseDetail = null)) } - setEvent(EnrollContract.EnrollEvent.FetchCourseDetail(fetchEnrollState = LoadState.Error, courseDetail = null)) } } @@ -193,14 +195,11 @@ class EnrollViewModel @Inject constructor( private fun getPlaceSearchResult() { viewModelScope.launch { - getPlaceSearchResultUseCase(keyword = currentState.keyword).fold( - onSuccess = { placeSearchResult -> - setState { copy(placeSearchResult = placeSearchResult) } - }, - onFailure = { - setEvent(EnrollContract.EnrollEvent.Enroll(loadState = LoadState.Error)) - } - ) + getPlaceSearchResultUseCase(keyword = currentState.keyword).onSuccess { placeSearchResult -> + setState { copy(placeSearchResult = placeSearchResult) } + }.onFailure { + setEvent(EnrollContract.EnrollEvent.Enroll(loadState = LoadState.Error)) + } } } } From c3a6196e87e5f789f701767ef1bde6ff3cb5e142 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=84=EC=9A=B0?= <85734140+jinuemong@users.noreply.github.com> Date: Thu, 3 Apr 2025 22:55:30 +0900 Subject: [PATCH 32/49] =?UTF-8?q?[feat]=20=EC=8B=9C=EC=B2=AD=ED=98=95=20?= =?UTF-8?q?=EA=B4=91=EA=B3=A0=20=EA=B5=AC=ED=98=84=20(#26)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: ๊ด‘๊ณ  ํฌ์ธํŠธ ์ƒ์„ฑ remote ์—ฐ๊ฒฐ ๋ฐ usecase ์ƒ์„ฑ * feat: viewModel ์ ์šฉ * chore: ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ ์šฉ ๋ฐ ads ์ถ”๊ฐ€ * chore: ci/cd ์ถ”๊ฐ€ * feat: ๊ด‘๊ณ  UI ๋กœ์ง ์ ์šฉ * fix: ํ‚ค ์˜ค๋ฅ˜ ์ˆ˜์ • * feat: ๊ด‘๊ณ  ์‹œ์ฒญ 5ํšŒ ์ œํ•œ ๋กœ์ง ๊ตฌํ˜„ * style: ์ฝ”๋“œ ํฌ๋งท ์ˆ˜์ • & ๋ฆฐํŠธ ์ฒดํฌ * fix : ๋นผ๋จน์€ ๋กœ์ง ์ถ”๊ฐ€ --- .github/workflows/android_cd.yml | 4 ++ .github/workflows/android_ci.yml | 4 ++ app/build.gradle.kts | 4 +- app/src/main/AndroidManifest.xml | 4 ++ .../ui/coursedetail/CourseDetailScreen.kt | 54 ++++++++++++++++-- .../ui/pointhistory/PointHistoryContract.kt | 7 ++- .../ui/pointhistory/PointHistoryScreen.kt | 57 +++++++++++++++++-- .../ui/pointhistory/PointHistoryViewModel.kt | 18 +++++- app/src/main/res/values/strings.xml | 2 + 9 files changed, 143 insertions(+), 11 deletions(-) diff --git a/.github/workflows/android_cd.yml b/.github/workflows/android_cd.yml index 7dd3ff4..a5910a1 100644 --- a/.github/workflows/android_cd.yml +++ b/.github/workflows/android_cd.yml @@ -51,11 +51,15 @@ jobs: KAKAO_NATIVE_APP_KEY_MANIFEST: ${{ secrets.KAKAO_NATIVE_APP_KEY }} KAKAO_NATIVE_APP_KEY: ${{ secrets.KAKAO_NATIVE_APP_KEY }} AMPLITUDE_API_KEY: ${{ secrets.AMPLITUDE_API_KEY }} + GOOGLE_ADS_API_ID: ${{ secrets.GOOGLE_ADS_API_ID }} + GOOGLE_ADS_API_ID_MANIFEST: ${{ secrets.GOOGLE_ADS_API_ID }} run: | echo "dev.base.url=\"$BASE_URL\"" >> local.properties echo "kakao.native.app.key.manifest=\"$KAKAO_NATIVE_APP_KEY_MANIFEST\"" >> local.properties echo "kakao.native.app.key=\"$KAKAO_NATIVE_APP_KEY\"" >> local.properties echo "amplitude.dev.api.key=\"$AMPLITUDE_API_KEY\"" >> local.properties + echo "google.ads.api.id=\"GOOGLE_ADS_API_ID\"" >> local.properties + echo "google.ads.api.id.manifest=\"GOOGLE_ADS_API_ID_MANIFEST\"" >> local.properties - name: Build Release APK run: | diff --git a/.github/workflows/android_ci.yml b/.github/workflows/android_ci.yml index 4c764ee..acd2f8a 100644 --- a/.github/workflows/android_ci.yml +++ b/.github/workflows/android_ci.yml @@ -54,6 +54,8 @@ jobs: KAKAO_NATIVE_APP_KEY: ${{ secrets.KAKAO_NATIVE_APP_KEY }} KAKAO_REST_API_KEY: ${{ secrets.KAKAO_REST_API_KEY }} AMPLITUDE_API_KEY: ${{ secrets.AMPLITUDE_API_KEY }} + GOOGLE_ADS_API_ID: ${{ secrets.GOOGLE_ADS_API_ID }} + GOOGLE_ADS_API_ID_MANIFEST: ${{ secrets.GOOGLE_ADS_API_ID }} run: | echo "dev.base.url=\"$BASE_URL\"" >> local.properties echo "kakao.base.url=\"$KAKAO_BASE_URL\"" >> local.properties @@ -61,6 +63,8 @@ jobs: echo "kakao.native.app.key=\"$KAKAO_NATIVE_APP_KEY\"" >> local.properties echo "kakao.rest.api.key=\"$KAKAO_REST_API_KEY\"" >> local.properties echo "amplitude.dev.api.key=\"$AMPLITUDE_API_KEY\"" >> local.properties + echo "google.ads.api.id=\"GOOGLE_ADS_API_ID\"" >> local.properties + echo "google.ads.api.id.manifest=\"GOOGLE_ADS_API_ID_MANIFEST\"" >> local.properties - name: Lint Check run: ./gradlew ktlintCheck -PcompileSdkVersion=34 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 98a5785..75eca75 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -33,8 +33,9 @@ android { buildConfigField("String", "KAKAO_NATIVE_APP_KEY", properties["kakao.native.app.key"].toString()) buildConfigField("String", "KAKAO_REST_API_KEY", properties["kakao.rest.api.key"].toString()) - + buildConfigField("String", "GOOGLE_ADS_API_ID", properties["google.ads.api.id"].toString()) manifestPlaceholders["KAKAO_NATIVE_APP_KEY_MANIFEST"] = properties["kakao.native.app.key.manifest"] as String + manifestPlaceholders["GOOGLE_ADS_API_ID_MANIFEST"] = properties["google.ads.api.id.manifest"] as String } buildTypes { @@ -96,6 +97,7 @@ dependencies { implementation(platform(libs.google.firebase.bom)) implementation(libs.google.firebase.crashlytics) implementation(libs.firebase.crashlytics.buildtools) + implementation(libs.google.gms.ads) // Network implementation(platform(libs.okhttp.bom)) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e75d9c2..96ffc14 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -18,6 +18,10 @@ android:usesCleartextTraffic="true" tools:targetApi="31"> + + navigateToEnroll(courseDetailSideEffect.enrollType, courseDetailSideEffect.viewPath, courseDetailSideEffect.id) is CourseDetailContract.CourseDetailSideEffect.PopBackStack -> popBackStack() + CourseDetailContract.CourseDetailSideEffect.NavigateToAds -> { + RewardedAd.load( + context, + BuildConfig.GOOGLE_ADS_API_ID, + adRequest, + object : RewardedAdLoadCallback() { + override fun onAdLoaded(ad: RewardedAd) { + viewModel.postAdsPoint() + } + + override fun onAdFailedToLoad(error: LoadAdError) { + when (error.code) { + AdRequest.ERROR_CODE_NO_FILL -> viewModel.setEvent(CourseDetailContract.CourseDetailEvent.FullAds) + else -> viewModel.setEvent(CourseDetailContract.CourseDetailEvent.FailLoadAdsPoint) + } + } + } + ) + } } } } @@ -150,6 +179,12 @@ fun CourseDetailRoute( }, onDismissCollectPoint = { viewModel.setEvent(CourseDetailContract.CourseDetailEvent.DismissDialogPointLack) + }, + onSelectAds = { + viewModel.setSideEffect(CourseDetailContract.CourseDetailSideEffect.NavigateToAds) + }, + onDismissFullAdsDialog = { + viewModel.setEvent(CourseDetailContract.CourseDetailEvent.DismissFullAdsDialog) } ) } @@ -201,7 +236,9 @@ fun CourseDetailScreen( dismissReportCourseBottomSheet: () -> Unit, enrollSchedule: () -> Unit, onTopBarIconClicked: () -> Unit, - openCourseDetail: () -> Unit + openCourseDetail: () -> Unit, + onSelectAds: () -> Unit, + onDismissFullAdsDialog: () -> Unit ) { var imageHeight by remember { mutableIntStateOf(0) } @@ -350,13 +387,20 @@ fun CourseDetailScreen( ) } + if (courseDetailUiState.isFullAdsDialogOpen) { + DateRoadOneButtonDialogWithDescription( + oneButtonDialogWithDescriptionType = OneButtonDialogWithDescriptionType.FULL_ADS, + onDismissRequest = onDismissFullAdsDialog, + onClickConfirm = onDismissFullAdsDialog + ) + } + DateRoadPointBottomSheet( isBottomSheetOpen = courseDetailUiState.isPointCollectBottomSheetOpen, title = stringResource(R.string.point_box_lack_point_button_text), onClick = { dateRoadCollectPointType -> when (dateRoadCollectPointType) { - // TODO : add ADS - DateRoadCollectPointType.WATCH_ADS -> Unit + DateRoadCollectPointType.WATCH_ADS -> onSelectAds() DateRoadCollectPointType.COURSE_REGISTRATION -> onSelectEnroll() } onDismissCollectPoint() @@ -463,7 +507,9 @@ fun CourseDetailScreenPreview() { dismissDialogDeleteCourse = {}, onDismissCollectPoint = {}, onSelectEnroll = {}, - dismissDialogReportCourse = {} + dismissDialogReportCourse = {}, + onSelectAds = {}, + onDismissFullAdsDialog = {} ) } } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryContract.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryContract.kt index 79dc391..05f548d 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryContract.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryContract.kt @@ -15,11 +15,13 @@ class PointHistoryContract { val userPoint: UserPoint = UserPoint(), val isPointCollectBottomSheetOpen: Boolean = false, val pointHistoryTabType: PointHistoryTabType = PointHistoryTabType.GAINED_HISTORY, - val pointHistory: PointHistory = PointHistory() + val pointHistory: PointHistory = PointHistory(), + val isFullAdsDialogOpen: Boolean = false ) : UiState sealed interface PointHistorySideEffect : UiSideEffect { data object PopBackStack : PointHistorySideEffect + data object NavigateToAds : PointHistorySideEffect data class NavigateToEnroll(val enrollType: EnrollType, val viewPath: String, val id: Int?) : PointHistorySideEffect } @@ -27,7 +29,10 @@ class PointHistoryContract { data class FetchPointHistory(val loadState: LoadState, val pointHistory: PointHistory) : PointHistoryEvent() data class FetchUserPoint(val loadState: LoadState, val userPoint: UserPoint) : PointHistoryEvent() data class OnTabBarClicked(val pointHistoryTabType: PointHistoryTabType) : PointHistoryEvent() + data object FailLoadAdsPoint : PointHistoryEvent() data object OnPointCollectBottomSheetClick : PointHistoryEvent() data object OnPointCollectBottomSheetDismiss : PointHistoryEvent() + data object FullAds : PointHistoryEvent() + data object DismissFullAdsDialog : PointHistoryEvent() } } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryScreen.kt index 140953a..20cdb86 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryScreen.kt @@ -13,7 +13,9 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -21,15 +23,22 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle +import com.google.android.gms.ads.AdRequest +import com.google.android.gms.ads.LoadAdError +import com.google.android.gms.ads.rewarded.RewardedAd +import com.google.android.gms.ads.rewarded.RewardedAdLoadCallback +import org.sopt.teamdateroad.BuildConfig import org.sopt.teamdateroad.R import org.sopt.teamdateroad.domain.model.Point import org.sopt.teamdateroad.domain.model.PointHistory import org.sopt.teamdateroad.domain.model.UserPoint import org.sopt.teamdateroad.presentation.type.EmptyViewType import org.sopt.teamdateroad.presentation.type.EnrollType +import org.sopt.teamdateroad.presentation.type.OneButtonDialogWithDescriptionType import org.sopt.teamdateroad.presentation.type.PointHistoryTabType import org.sopt.teamdateroad.presentation.ui.component.bottomsheet.DateRoadPointBottomSheet import org.sopt.teamdateroad.presentation.ui.component.bottomsheet.model.collect.DateRoadCollectPointType +import org.sopt.teamdateroad.presentation.ui.component.dialog.DateRoadOneButtonDialogWithDescription import org.sopt.teamdateroad.presentation.ui.component.tabbar.DateRoadTabBar import org.sopt.teamdateroad.presentation.ui.component.tabbar.DateRoadTabTitle import org.sopt.teamdateroad.presentation.ui.component.topbar.DateRoadBasicTopBar @@ -53,6 +62,8 @@ fun PointHistoryRoute( ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val lifecycleOwner = LocalLifecycleOwner.current + val context = LocalContext.current + val adRequest = remember { AdRequest.Builder().build() } LaunchedEffect(Unit) { viewModel.fetchPointHistory() @@ -69,6 +80,27 @@ fun PointHistoryRoute( pointHistorySideEffect.viewPath, pointHistorySideEffect.id ) + + PointHistoryContract.PointHistorySideEffect.NavigateToAds -> { + RewardedAd.load( + context, + BuildConfig.GOOGLE_ADS_API_ID, + adRequest, + object : RewardedAdLoadCallback() { + override fun onAdLoaded(ad: RewardedAd) { + viewModel.postAdsPoint() + } + + override fun onAdFailedToLoad(error: LoadAdError) { + when (error.code) { + AdRequest.ERROR_CODE_NO_FILL -> viewModel.setEvent(PointHistoryContract.PointHistoryEvent.FullAds) + + else -> viewModel.setEvent(PointHistoryContract.PointHistoryEvent.FailLoadAdsPoint) + } + } + } + ) + } } } } @@ -98,6 +130,12 @@ fun PointHistoryRoute( id = null ) ) + }, + onSelectAds = { + viewModel.setSideEffect(PointHistoryContract.PointHistorySideEffect.NavigateToAds) + }, + onDismissFullAdsDialog = { + viewModel.setEvent(PointHistoryContract.PointHistoryEvent.DismissFullAdsDialog) } ) } @@ -114,7 +152,9 @@ fun PointHistoryScreen( onTopBarIconClicked: () -> Unit, onClickCollectPoint: () -> Unit, onDisMissCollectPoint: () -> Unit, - onSelectEnroll: () -> Unit + onSelectEnroll: () -> Unit, + onSelectAds: () -> Unit, + onDismissFullAdsDialog: () -> Unit ) { Column( modifier = Modifier @@ -179,13 +219,20 @@ fun PointHistoryScreen( } } + if (pointHistoryUiState.isFullAdsDialogOpen) { + DateRoadOneButtonDialogWithDescription( + oneButtonDialogWithDescriptionType = OneButtonDialogWithDescriptionType.FULL_ADS, + onDismissRequest = onDismissFullAdsDialog, + onClickConfirm = onDismissFullAdsDialog + ) + } + DateRoadPointBottomSheet( isBottomSheetOpen = pointHistoryUiState.isPointCollectBottomSheetOpen, title = stringResource(R.string.point_box_get_point_button_text), onClick = { dateRoadCollectPointType -> when (dateRoadCollectPointType) { - // TODO : add ADS - DateRoadCollectPointType.WATCH_ADS -> Unit + DateRoadCollectPointType.WATCH_ADS -> onSelectAds() DateRoadCollectPointType.COURSE_REGISTRATION -> onSelectEnroll() } onDisMissCollectPoint() @@ -218,7 +265,9 @@ fun PointHistoryPreview() { onTopBarIconClicked = {}, onClickCollectPoint = {}, onDisMissCollectPoint = {}, - onSelectEnroll = {} + onSelectEnroll = {}, + onSelectAds = {}, + onDismissFullAdsDialog = {} ) } } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryViewModel.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryViewModel.kt index 2772fcb..990cac5 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryViewModel.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryViewModel.kt @@ -6,13 +6,15 @@ import javax.inject.Inject import kotlinx.coroutines.launch import org.sopt.teamdateroad.domain.usecase.GetPointHistoryUseCase import org.sopt.teamdateroad.domain.usecase.GetUserPointUseCase +import org.sopt.teamdateroad.domain.usecase.PostAdsPointUseCase import org.sopt.teamdateroad.presentation.util.base.BaseViewModel import org.sopt.teamdateroad.presentation.util.view.LoadState @HiltViewModel class PointHistoryViewModel @Inject constructor( private val getPointHistoryUseCase: GetPointHistoryUseCase, - private val getUserPointUseCase: GetUserPointUseCase + private val getUserPointUseCase: GetUserPointUseCase, + private val postAdsPointUseCase: PostAdsPointUseCase ) : BaseViewModel() { override fun createInitialState(): PointHistoryContract.PointHistoryUiState = PointHistoryContract.PointHistoryUiState() @@ -24,6 +26,9 @@ class PointHistoryViewModel @Inject constructor( is PointHistoryContract.PointHistoryEvent.FetchUserPoint -> setState { copy(loadState = event.loadState, userPoint = event.userPoint) } PointHistoryContract.PointHistoryEvent.OnPointCollectBottomSheetClick -> setState { copy(isPointCollectBottomSheetOpen = true) } PointHistoryContract.PointHistoryEvent.OnPointCollectBottomSheetDismiss -> setState { copy(isPointCollectBottomSheetOpen = false) } + PointHistoryContract.PointHistoryEvent.FailLoadAdsPoint -> setState { copy(loadState = LoadState.Loading) } + PointHistoryContract.PointHistoryEvent.DismissFullAdsDialog -> setState { copy(isFullAdsDialogOpen = false) } + PointHistoryContract.PointHistoryEvent.FullAds -> setState { copy(isFullAdsDialogOpen = true) } } } @@ -54,4 +59,15 @@ class PointHistoryViewModel @Inject constructor( } } } + + fun postAdsPoint() { + viewModelScope.launch { + postAdsPointUseCase().onSuccess { + fetchPointHistory() + fetchUserPoint() + }.onFailure { + setState { copy(loadState = LoadState.Loading) } + } + } + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 06a287d..3def010 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -112,6 +112,8 @@ ์กฐ๊ธˆ๋งŒ ๊ธฐ๋‹ค๋ ค์ฃผ์„ธ์š” :) ๋ฐ์ดํŠธ๋ฅผ ๋” ์ด์ƒ ๋“ฑ๋กํ•  ์ˆ˜ ์—†์–ด์š”! ๋ฐ์ดํŠธ๋Š” ์ตœ๋Œ€ 5๊ฐœ๊นŒ์ง€๋งŒ ๋“ฑ๋ก ๊ฐ€๋Šฅํ•ด์š” + ๊ด‘๊ณ ๋Š” ํ•˜๋ฃจ 5ํšŒ๊นŒ์ง€๋งŒ ์‹œ์ฒญํ•  ์ˆ˜ ์žˆ์–ด์š” + ๊ด‘๊ณ  ์‹œ์ฒญ ํ•œ๋„๋Š” ๋งค์ผ ์ž์ •์— ์ดˆ๊ธฐํ™”๋ผ์š” ํš๋“ ๋‚ด์—ญ From 5ebb6cf718b33bc34cc6febdaabbd06deb041f9a Mon Sep 17 00:00:00 2001 From: Yaechan Park <102402485+dpcks0509@users.noreply.github.com> Date: Sat, 5 Apr 2025 21:47:31 +0900 Subject: [PATCH 33/49] =?UTF-8?q?[feat]=20=EC=9E=A5=EC=86=8C=EA=B2=80?= =?UTF-8?q?=EC=83=89=20Paging=20=EB=AC=B4=ED=95=9C=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A1=A4=20&=20Debounce=20=EC=A0=81=EC=9A=A9=20(#28)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: paging-runtime ์˜์กด์„ฑ ์ถ”๊ฐ€ * feat: PlaceSearchResultPagingSource ๊ตฌํ˜„ * feat: Paging ์ดˆ๊ธฐ ๊ตฌํ˜„ * refactor: DateRoadPlaceCard ์ฃผ์„ ์ œ๊ฑฐ * feat: ์žฅ์†Œ ๊ฒ€์ƒ‰ paging ๊ตฌํ˜„ * feat: Paging MAX_SIZE ์ง€์ • * refactor: paging ๋ฐฉ์‹ ๋ฆฌํŒฉํ† ๋ง * refactor: UseCase์— @Singleton ์ถ”๊ฐ€ * refactor: ์žฅ์†Œ ๊ฒ€์ƒ‰ debounce ์ ์šฉ * [feat] ์‹œ์ฒญํ˜• ๊ด‘๊ณ  ๊ตฌํ˜„ (#26) * feat: ๊ด‘๊ณ  ํฌ์ธํŠธ ์ƒ์„ฑ remote ์—ฐ๊ฒฐ ๋ฐ usecase ์ƒ์„ฑ * feat: viewModel ์ ์šฉ * chore: ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ ์šฉ ๋ฐ ads ์ถ”๊ฐ€ * chore: ci/cd ์ถ”๊ฐ€ * feat: ๊ด‘๊ณ  UI ๋กœ์ง ์ ์šฉ * fix: ํ‚ค ์˜ค๋ฅ˜ ์ˆ˜์ • * feat: ๊ด‘๊ณ  ์‹œ์ฒญ 5ํšŒ ์ œํ•œ ๋กœ์ง ๊ตฌํ˜„ * style: ์ฝ”๋“œ ํฌ๋งท ์ˆ˜์ • & ๋ฆฐํŠธ ์ฒดํฌ * fix : ๋นผ๋จน์€ ๋กœ์ง ์ถ”๊ฐ€ * remove: PlaceSearchUiState ์ œ๊ฑฐ * refactor: PagingData Result ๋ž˜ํผ ์ œ๊ฑฐ ๋ฐ searchPlaceInfos flatMapLatest ํ™œ์šฉ * refactor: DEBOUNCE_TIME ๋‹จ์œ„ ์ถ”๊ฐ€ * refactor: PlaceSearchPagingSource Data Layer๋กœ ์ด๋™ --------- Co-authored-by: ๊น€์ง„์šฐ <85734140+jinuemong@users.noreply.github.com> --- .../datasource/PlaceSearchDataSource.kt | 6 +- .../PlaceSearchDataSourceImpl.kt | 59 ++++++++++++++++- .../response/ResponsePlaceSearchResultDto.kt | 3 + .../dataremote/service/PlaceSearchService.kt | 4 +- .../todomain/ResponsePlaceSearchDtoMapper.kt | 3 +- .../PlaceSearchRepositoryImpl.kt | 14 +++-- .../domain/model/PlaceSearchResult.kt | 3 +- .../repository/PlaceSearchRepository.kt | 6 +- .../usecase/GetPlaceSearchResultUseCase.kt | 8 ++- .../ui/component/card/DateRoadPlaceCard.kt | 5 +- .../presentation/ui/enroll/EnrollContract.kt | 5 +- .../presentation/ui/enroll/EnrollScreen.kt | 15 +++++ .../ui/enroll/EnrollSecondScreen.kt | 15 +++-- .../presentation/ui/enroll/EnrollViewModel.kt | 50 ++++++++++----- .../PlaceSearchBottomSheet.kt} | 63 ++++++++++--------- 15 files changed, 188 insertions(+), 71 deletions(-) rename app/src/main/java/org/sopt/teamdateroad/presentation/ui/{component/bottomsheet/DateRoadPlaceSearchBottomSheet.kt => enroll/PlaceSearchBottomSheet.kt} (86%) diff --git a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/datasource/PlaceSearchDataSource.kt b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/datasource/PlaceSearchDataSource.kt index 30831a0..400197f 100644 --- a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/datasource/PlaceSearchDataSource.kt +++ b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/datasource/PlaceSearchDataSource.kt @@ -1,9 +1,11 @@ package org.sopt.teamdateroad.data.dataremote.datasource -import org.sopt.teamdateroad.data.dataremote.model.response.ResponsePlaceSearchResultDto +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import org.sopt.teamdateroad.data.dataremote.model.response.ResponsePlaceInfoDto interface PlaceSearchDataSource { suspend fun getPlaceSearchResult( keyword: String - ): Result + ): Flow> } diff --git a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/datasourceimpl/PlaceSearchDataSourceImpl.kt b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/datasourceimpl/PlaceSearchDataSourceImpl.kt index b21d37c..06ba3f9 100644 --- a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/datasourceimpl/PlaceSearchDataSourceImpl.kt +++ b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/datasourceimpl/PlaceSearchDataSourceImpl.kt @@ -1,12 +1,65 @@ package org.sopt.teamdateroad.data.dataremote.datasourceimpl +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.PagingSource +import androidx.paging.PagingState import javax.inject.Inject +import kotlinx.coroutines.flow.Flow import org.sopt.teamdateroad.data.dataremote.datasource.PlaceSearchDataSource -import org.sopt.teamdateroad.data.dataremote.model.response.ResponsePlaceSearchResultDto +import org.sopt.teamdateroad.data.dataremote.model.response.ResponsePlaceInfoDto import org.sopt.teamdateroad.data.dataremote.service.PlaceSearchService +import org.sopt.teamdateroad.presentation.ui.enroll.MAX_SIZE +import org.sopt.teamdateroad.presentation.ui.enroll.PAGE_SIZE class PlaceSearchDataSourceImpl @Inject constructor(private val placeSearchService: PlaceSearchService) : PlaceSearchDataSource { - override suspend fun getPlaceSearchResult(keyword: String): Result { - return runCatching { placeSearchService.getPlaceSearchResult(keyword) } + override suspend fun getPlaceSearchResult(keyword: String): Flow> { + return Pager( + config = PagingConfig(pageSize = PAGE_SIZE, maxSize = MAX_SIZE), + pagingSourceFactory = { + PlaceSearchPagingSource( + keyword = keyword, + placeSearchService = placeSearchService + ) + } + ).flow + } +} + +class PlaceSearchPagingSource( + private val keyword: String, + private val placeSearchService: PlaceSearchService +) : PagingSource() { + override suspend fun load(params: LoadParams): LoadResult { + val page = params.key ?: INITIAL_PAGE + + return runCatching { + val placeSearchResult = placeSearchService.getPlaceSearchResult( + keyword = keyword, + page = page, + size = PAGE_SIZE + ) + + LoadResult.Page( + data = placeSearchResult.placeInfos, + prevKey = if (page == INITIAL_PAGE) null else page - PAGE_OFFSET, + nextKey = if (placeSearchResult.meta.isEnd) null else page + PAGE_OFFSET + ) + }.getOrElse { throwable -> + LoadResult.Error(throwable) + } + } + + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition?.let { position -> + state.closestPageToPosition(position)?.prevKey?.plus(PAGE_OFFSET) + ?: state.closestPageToPosition(position)?.nextKey?.minus(PAGE_OFFSET) + } + } + + companion object { + private const val INITIAL_PAGE = 1 + private const val PAGE_OFFSET = 1 } } diff --git a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/model/response/ResponsePlaceSearchResultDto.kt b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/model/response/ResponsePlaceSearchResultDto.kt index 105bc53..4517400 100644 --- a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/model/response/ResponsePlaceSearchResultDto.kt +++ b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/model/response/ResponsePlaceSearchResultDto.kt @@ -5,6 +5,9 @@ import kotlinx.serialization.Serializable @Serializable data class ResponsePlaceSearchResultDto( + @SerializedName("meta") + val meta: ResponseMetaDto, + @SerializedName("documents") val placeInfos: List ) diff --git a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/service/PlaceSearchService.kt b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/service/PlaceSearchService.kt index 7c7d3a9..60a05c9 100644 --- a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/service/PlaceSearchService.kt +++ b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/service/PlaceSearchService.kt @@ -8,6 +8,8 @@ import retrofit2.http.Query interface PlaceSearchService { @GET(LOCAL_SEARCH_KEYWORD_JSON) suspend fun getPlaceSearchResult( - @Query("query") keyword: String + @Query("query") keyword: String, + @Query("page") page: Int, + @Query("size") size: Int ): ResponsePlaceSearchResultDto } diff --git a/app/src/main/java/org/sopt/teamdateroad/data/mapper/todomain/ResponsePlaceSearchDtoMapper.kt b/app/src/main/java/org/sopt/teamdateroad/data/mapper/todomain/ResponsePlaceSearchDtoMapper.kt index 28a0e74..da6f95c 100644 --- a/app/src/main/java/org/sopt/teamdateroad/data/mapper/todomain/ResponsePlaceSearchDtoMapper.kt +++ b/app/src/main/java/org/sopt/teamdateroad/data/mapper/todomain/ResponsePlaceSearchDtoMapper.kt @@ -4,5 +4,6 @@ import org.sopt.teamdateroad.data.dataremote.model.response.ResponsePlaceSearchR import org.sopt.teamdateroad.domain.model.PlaceSearchResult fun ResponsePlaceSearchResultDto.toDomain(): PlaceSearchResult = PlaceSearchResult( - placeInfos = this.placeInfos.map { responsePlaceInfoDto -> responsePlaceInfoDto.toDomain() } + placeInfos = this.placeInfos.map { responsePlaceInfoDto -> responsePlaceInfoDto.toDomain() }, + isEnd = this.meta.isEnd ) diff --git a/app/src/main/java/org/sopt/teamdateroad/data/repositoryimpl/PlaceSearchRepositoryImpl.kt b/app/src/main/java/org/sopt/teamdateroad/data/repositoryimpl/PlaceSearchRepositoryImpl.kt index 611f426..caaccdd 100644 --- a/app/src/main/java/org/sopt/teamdateroad/data/repositoryimpl/PlaceSearchRepositoryImpl.kt +++ b/app/src/main/java/org/sopt/teamdateroad/data/repositoryimpl/PlaceSearchRepositoryImpl.kt @@ -1,15 +1,21 @@ package org.sopt.teamdateroad.data.repositoryimpl +import androidx.paging.PagingData +import androidx.paging.map import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map import org.sopt.teamdateroad.data.dataremote.datasource.PlaceSearchDataSource import org.sopt.teamdateroad.data.mapper.todomain.toDomain -import org.sopt.teamdateroad.domain.model.PlaceSearchResult +import org.sopt.teamdateroad.domain.model.PlaceInfo import org.sopt.teamdateroad.domain.repository.PlaceSearchRepository class PlaceSearchRepositoryImpl @Inject constructor(private val placeSearchDataSource: PlaceSearchDataSource) : PlaceSearchRepository { - override suspend fun getPlaceSearchResult(keyword: String): Result { - return placeSearchDataSource.getPlaceSearchResult(keyword).map { responsePlaceSearchResultDto -> - responsePlaceSearchResultDto.toDomain() + override suspend fun getPlaceSearchResult(keyword: String): Flow> { + return placeSearchDataSource.getPlaceSearchResult(keyword).map { pagingData -> + pagingData.map { responsePlaceInfoDto -> + responsePlaceInfoDto.toDomain() + } } } } diff --git a/app/src/main/java/org/sopt/teamdateroad/domain/model/PlaceSearchResult.kt b/app/src/main/java/org/sopt/teamdateroad/domain/model/PlaceSearchResult.kt index 78a45ee..0005188 100644 --- a/app/src/main/java/org/sopt/teamdateroad/domain/model/PlaceSearchResult.kt +++ b/app/src/main/java/org/sopt/teamdateroad/domain/model/PlaceSearchResult.kt @@ -1,5 +1,6 @@ package org.sopt.teamdateroad.domain.model data class PlaceSearchResult( - val placeInfos: List + val placeInfos: List, + val isEnd: Boolean ) diff --git a/app/src/main/java/org/sopt/teamdateroad/domain/repository/PlaceSearchRepository.kt b/app/src/main/java/org/sopt/teamdateroad/domain/repository/PlaceSearchRepository.kt index 0506f04..482b66c 100644 --- a/app/src/main/java/org/sopt/teamdateroad/domain/repository/PlaceSearchRepository.kt +++ b/app/src/main/java/org/sopt/teamdateroad/domain/repository/PlaceSearchRepository.kt @@ -1,9 +1,11 @@ package org.sopt.teamdateroad.domain.repository -import org.sopt.teamdateroad.domain.model.PlaceSearchResult +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import org.sopt.teamdateroad.domain.model.PlaceInfo interface PlaceSearchRepository { suspend fun getPlaceSearchResult( keyword: String - ): Result + ): Flow> } diff --git a/app/src/main/java/org/sopt/teamdateroad/domain/usecase/GetPlaceSearchResultUseCase.kt b/app/src/main/java/org/sopt/teamdateroad/domain/usecase/GetPlaceSearchResultUseCase.kt index 4866486..6614016 100644 --- a/app/src/main/java/org/sopt/teamdateroad/domain/usecase/GetPlaceSearchResultUseCase.kt +++ b/app/src/main/java/org/sopt/teamdateroad/domain/usecase/GetPlaceSearchResultUseCase.kt @@ -1,11 +1,15 @@ package org.sopt.teamdateroad.domain.usecase +import androidx.paging.PagingData import javax.inject.Inject -import org.sopt.teamdateroad.domain.model.PlaceSearchResult +import javax.inject.Singleton +import kotlinx.coroutines.flow.Flow +import org.sopt.teamdateroad.domain.model.PlaceInfo import org.sopt.teamdateroad.domain.repository.PlaceSearchRepository +@Singleton class GetPlaceSearchResultUseCase @Inject constructor( private val placeSearchRepository: PlaceSearchRepository ) { - suspend operator fun invoke(keyword: String): Result = placeSearchRepository.getPlaceSearchResult(keyword) + suspend operator fun invoke(keyword: String): Flow> = placeSearchRepository.getPlaceSearchResult(keyword) } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/card/DateRoadPlaceCard.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/card/DateRoadPlaceCard.kt index 277edc6..58456d4 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/card/DateRoadPlaceCard.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/card/DateRoadPlaceCard.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width @@ -40,8 +41,8 @@ fun DateRoadPlaceCard( Row( modifier = modifier -// .fillMaxWidth() -// .height(76.dp) + .fillMaxWidth() + .height(76.dp) ) { Column( modifier = Modifier diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollContract.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollContract.kt index 3e5b010..28fc395 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollContract.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollContract.kt @@ -4,7 +4,6 @@ import org.sopt.teamdateroad.domain.model.CourseDetail import org.sopt.teamdateroad.domain.model.Enroll import org.sopt.teamdateroad.domain.model.Place import org.sopt.teamdateroad.domain.model.PlaceInfo -import org.sopt.teamdateroad.domain.model.PlaceSearchResult import org.sopt.teamdateroad.domain.model.TimelineDetail import org.sopt.teamdateroad.domain.type.RegionType import org.sopt.teamdateroad.presentation.type.EnrollScreenType @@ -57,10 +56,8 @@ class EnrollContract { val onRegionBottomSheetRegionSelected: RegionType? = RegionType.SEOUL, val onRegionBottomSheetAreaSelected: Any? = null, val isPlaceSearchBottomSheetOpen: Boolean = false, - val keyword: String = "", val place: Place = Place(), - val placeSearchResult: PlaceSearchResult = PlaceSearchResult(placeInfos = emptyList()), - val placeInfos: List = emptyList(), + val selectedPlaceInfos: List = emptyList(), val isPlaceEditable: Boolean = true, val isDurationBottomSheetOpen: Boolean = false, val durationPicker: List = listOf(Picker(items = (DURATION_START..DURATION_END).map { (it * 0.5).toString() })), diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt index 9bf5968..ddcba6c 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt @@ -29,8 +29,12 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle +import androidx.paging.PagingData +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems import java.time.LocalDate import java.time.format.DateTimeFormatter +import kotlinx.coroutines.flow.flowOf import org.sopt.teamdateroad.R import org.sopt.teamdateroad.domain.model.Place import org.sopt.teamdateroad.domain.model.PlaceInfo @@ -108,6 +112,9 @@ fun EnrollRoute( timelineId: Int? ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val searchKeyword by viewModel.searchKeyword.collectAsStateWithLifecycle("") + val searchPlaceInfos = viewModel.searchPlaceInfos.collectAsLazyPagingItems() + val lifecycleOwner = LocalLifecycleOwner.current val getGalleryLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? -> @@ -206,6 +213,8 @@ fun EnrollRoute( EnrollScreen( padding = padding, enrollUiState = uiState, + searchKeyword = searchKeyword, + searchPlaceInfos = searchPlaceInfos, onTopBarBackButtonClick = { viewModel.setEvent(EnrollContract.EnrollEvent.OnTopBarBackButtonClick) @@ -335,6 +344,8 @@ fun EnrollRoute( fun EnrollScreen( padding: PaddingValues, enrollUiState: EnrollContract.EnrollUiState = EnrollContract.EnrollUiState(), + searchKeyword: String, + searchPlaceInfos: LazyPagingItems, onTopBarBackButtonClick: () -> Unit, onTopBarLoadButtonClick: () -> Unit, onEnrollButtonClick: () -> Unit, @@ -445,6 +456,8 @@ fun EnrollScreen( EnrollScreenType.SECOND -> EnrollSecondScreen( enrollUiState = enrollUiState, + searchKeyword = searchKeyword, + searchPlaceInfos = searchPlaceInfos, onPlaceSearchButtonClick = onPlaceSearchButtonClick, onKeywordChanged = onKeywordChanged, onPlaceSelected = onPlaceSelected, @@ -572,6 +585,8 @@ fun EnrollScreenPreview() { enrollUiState = EnrollContract.EnrollUiState( loadState = LoadState.Success ), + searchKeyword = "", + searchPlaceInfos = flowOf(PagingData.empty()).collectAsLazyPagingItems(), onTopBarBackButtonClick = {}, onTopBarLoadButtonClick = {}, onEnrollButtonClick = {}, diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollSecondScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollSecondScreen.kt index 3ddba55..2ba0e2f 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollSecondScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollSecondScreen.kt @@ -31,13 +31,16 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex +import androidx.paging.PagingData +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch import org.sopt.teamdateroad.R import org.sopt.teamdateroad.domain.model.Place import org.sopt.teamdateroad.domain.model.PlaceInfo import org.sopt.teamdateroad.presentation.type.PlaceCardType -import org.sopt.teamdateroad.presentation.ui.component.bottomsheet.DateRoadPlaceSearchBottomSheet import org.sopt.teamdateroad.presentation.ui.component.button.DateRoadTextButton import org.sopt.teamdateroad.presentation.ui.component.card.DateRoadPlaceCard import org.sopt.teamdateroad.presentation.ui.enroll.component.EnrollPlaceInsertBar @@ -51,6 +54,8 @@ import org.sopt.teamdateroad.ui.theme.DateRoadTheme @Composable fun EnrollSecondScreen( enrollUiState: EnrollContract.EnrollUiState = EnrollContract.EnrollUiState(), + searchKeyword: String, + searchPlaceInfos: LazyPagingItems, onPlaceSearchButtonClick: () -> Unit, onKeywordChanged: (String) -> Unit, onPlaceSelected: (PlaceInfo) -> Unit, @@ -178,10 +183,10 @@ fun EnrollSecondScreen( Spacer(modifier = Modifier.height(16.dp)) } - DateRoadPlaceSearchBottomSheet( + PlaceSearchBottomSheet( isBottomSheetOpen = enrollUiState.isPlaceSearchBottomSheetOpen, - keyword = enrollUiState.keyword, - placeSearchResult = enrollUiState.placeSearchResult, + searchKeyword = searchKeyword, + searchPlaceInfos = searchPlaceInfos, onKeywordChanged = onKeywordChanged, onPlaceSelected = onPlaceSelected, onDismissRequest = onPlaceSearchBottomSheetDismiss @@ -193,6 +198,8 @@ fun EnrollSecondScreen( fun EnrollSecondScreenPreview() { DATEROADTheme { EnrollSecondScreen( + searchKeyword = "", + searchPlaceInfos = flowOf(PagingData.empty()).collectAsLazyPagingItems(), onPlaceSearchButtonClick = {}, onAddPlaceButtonClick = {}, onSelectedPlaceCourseTimeClick = {}, diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollViewModel.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollViewModel.kt index b04dfb2..49d4c05 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollViewModel.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollViewModel.kt @@ -1,12 +1,23 @@ package org.sopt.teamdateroad.presentation.ui.enroll import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData +import androidx.paging.cachedIn import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.launch import org.sopt.teamdateroad.data.dataremote.util.Date.NEAREST_DATE_START_OUTPUT_FORMAT import org.sopt.teamdateroad.data.mapper.toEntity.toEnroll -import org.sopt.teamdateroad.domain.model.PlaceSearchResult +import org.sopt.teamdateroad.domain.model.PlaceInfo import org.sopt.teamdateroad.domain.type.RegionType import org.sopt.teamdateroad.domain.usecase.GetCourseDetailUseCase import org.sopt.teamdateroad.domain.usecase.GetPlaceSearchResultUseCase @@ -32,6 +43,18 @@ class EnrollViewModel @Inject constructor( ) : BaseViewModel() { override fun createInitialState(): EnrollContract.EnrollUiState = EnrollContract.EnrollUiState() + private val _searchKeyword: MutableStateFlow = MutableStateFlow("") + val searchKeyword: StateFlow get() = _searchKeyword.asStateFlow() + + @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) + val searchPlaceInfos: Flow> = searchKeyword + .debounce(DEBOUNCE_TIME_MILLS) + .distinctUntilChanged() + .flatMapLatest { query -> + getPlaceSearchResultUseCase(query) + } + .cachedIn(viewModelScope) + override suspend fun handleEvent(event: EnrollContract.EnrollEvent) { when (event) { is EnrollContract.EnrollEvent.OnTopBarBackButtonClick -> { @@ -78,14 +101,13 @@ class EnrollViewModel @Inject constructor( is EnrollContract.EnrollEvent.OnTimeTextFieldClick -> setState { copy(isTimePickerBottomSheetOpen = true) } is EnrollContract.EnrollEvent.OnRegionTextFieldClick -> setState { copy(isRegionBottomSheetOpen = true, onRegionBottomSheetRegionSelected = RegionType.SEOUL, onRegionBottomSheetAreaSelected = null) } is EnrollContract.EnrollEvent.OnPlaceSearchButtonClick -> setState { copy(isPlaceSearchBottomSheetOpen = true) } - is EnrollContract.EnrollEvent.OnKeywordChanged -> { - setState { - copy(keyword = event.keyword) - } - getPlaceSearchResult() + is EnrollContract.EnrollEvent.OnKeywordChanged -> _searchKeyword.value = event.keyword + + is EnrollContract.EnrollEvent.OnPlaceSearchBottomSheetDismiss -> { + setState { copy(isPlaceSearchBottomSheetOpen = false) } + _searchKeyword.value = "" } - is EnrollContract.EnrollEvent.OnPlaceSearchBottomSheetDismiss -> setState { copy(isPlaceSearchBottomSheetOpen = false, keyword = "", placeSearchResult = PlaceSearchResult(emptyList())) } is EnrollContract.EnrollEvent.OnSelectedPlaceCourseTimeClick -> setState { copy(isDurationBottomSheetOpen = true) } is EnrollContract.EnrollEvent.OnDatePickerBottomSheetDismissRequest -> setState { copy(isDatePickerBottomSheetOpen = false) } is EnrollContract.EnrollEvent.OnTimePickerBottomSheetDismissRequest -> setState { copy(isTimePickerBottomSheetOpen = false) } @@ -107,11 +129,11 @@ class EnrollViewModel @Inject constructor( is EnrollContract.EnrollEvent.OnTitleValueChange -> setState { copy(enroll = currentState.enroll.copy(title = event.title)) } is EnrollContract.EnrollEvent.OnPlaceSelected -> { - setState { copy(keyword = "", placeSearchResult = PlaceSearchResult(emptyList()), place = currentState.place.copy(title = event.placeInfo.placeName, address = event.placeInfo.addressName), placeInfos = currentState.placeInfos + event.placeInfo, isPlaceSearchBottomSheetOpen = false) } + setState { copy(place = currentState.place.copy(title = event.placeInfo.placeName, address = event.placeInfo.addressName), selectedPlaceInfos = currentState.selectedPlaceInfos + event.placeInfo, isPlaceSearchBottomSheetOpen = false) } + _searchKeyword.value = "" } is EnrollContract.EnrollEvent.OnDatePickerBottomSheetButtonClick -> setState { copy(enroll = currentState.enroll.copy(date = event.date), isDatePickerBottomSheetOpen = false) } - is EnrollContract.EnrollEvent.OnTimePickerBottomSheetButtonClick -> setState { copy(enroll = currentState.enroll.copy(startAt = event.startAt), isTimePickerBottomSheetOpen = false) } is EnrollContract.EnrollEvent.OnDateChipClicked -> setState { copy( @@ -193,13 +215,7 @@ class EnrollViewModel @Inject constructor( } } - private fun getPlaceSearchResult() { - viewModelScope.launch { - getPlaceSearchResultUseCase(keyword = currentState.keyword).onSuccess { placeSearchResult -> - setState { copy(placeSearchResult = placeSearchResult) } - }.onFailure { - setEvent(EnrollContract.EnrollEvent.Enroll(loadState = LoadState.Error)) - } - } + companion object { + private const val DEBOUNCE_TIME_MILLS = 300L } } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadPlaceSearchBottomSheet.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/PlaceSearchBottomSheet.kt similarity index 86% rename from app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadPlaceSearchBottomSheet.kt rename to app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/PlaceSearchBottomSheet.kt index 5e22de8..8ce441b 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadPlaceSearchBottomSheet.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/PlaceSearchBottomSheet.kt @@ -1,4 +1,4 @@ -package org.sopt.teamdateroad.presentation.ui.component.bottomsheet +package org.sopt.teamdateroad.presentation.ui.enroll import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Button @@ -38,25 +37,28 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.paging.PagingData +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems import com.holix.android.bottomsheetdialog.compose.BottomSheetBehaviorProperties import com.holix.android.bottomsheetdialog.compose.BottomSheetDialog import com.holix.android.bottomsheetdialog.compose.BottomSheetDialogProperties +import kotlinx.coroutines.flow.flowOf import org.sopt.teamdateroad.R import org.sopt.teamdateroad.domain.model.PlaceInfo -import org.sopt.teamdateroad.domain.model.PlaceSearchResult import org.sopt.teamdateroad.presentation.ui.enroll.component.EnrollPlaceSearchItem import org.sopt.teamdateroad.presentation.util.modifier.noRippleClickable import org.sopt.teamdateroad.ui.theme.DATEROADTheme import org.sopt.teamdateroad.ui.theme.DateRoadTheme @Composable -fun DateRoadPlaceSearchBottomSheet( +fun PlaceSearchBottomSheet( isBottomSheetOpen: Boolean, - keyword: String, - placeSearchResult: PlaceSearchResult, + searchKeyword: String, + searchPlaceInfos: LazyPagingItems, onKeywordChanged: (String) -> Unit, onPlaceSelected: (PlaceInfo) -> Unit, - onDismissRequest: () -> Unit = {} + onDismissRequest: () -> Unit ) { if (isBottomSheetOpen) { BottomSheetDialog( @@ -105,7 +107,7 @@ fun DateRoadPlaceSearchBottomSheet( Spacer(modifier = Modifier.height(22.dp)) TextField( - value = keyword, + value = searchKeyword, onValueChange = onKeywordChanged, modifier = Modifier .fillMaxWidth() @@ -151,19 +153,20 @@ fun DateRoadPlaceSearchBottomSheet( Spacer(modifier = Modifier.height(10.dp)) - if (placeSearchResult.placeInfos.isNotEmpty()) { + if (searchPlaceInfos.itemCount > 0) { LazyColumn(modifier = Modifier.weight(1f)) { - itemsIndexed( - items = placeSearchResult.placeInfos, - key = { index, placeInfo -> placeInfo.hashCode() + index } - ) { index, placeInfo -> + items( + searchPlaceInfos.itemCount + ) { index -> + val placeInfo = searchPlaceInfos[index] ?: return@items + EnrollPlaceSearchItem( - keyword = keyword, + keyword = searchKeyword, placeInfo = placeInfo, onClick = { onPlaceSelected(placeInfo) } ) - if (index != placeSearchResult.placeInfos.lastIndex) { + if (index != searchPlaceInfos.itemCount - 1) { HorizontalDivider( modifier = Modifier.fillMaxWidth(), color = DateRoadTheme.colors.gray100, @@ -172,6 +175,7 @@ fun DateRoadPlaceSearchBottomSheet( } } } + Spacer(modifier = Modifier.height(12.dp)) } else { EmptyPlaceSearchResult() } @@ -207,10 +211,18 @@ private fun EmptyPlaceSearchResult() { @Preview @Composable -fun DateRoadPlaceSearchBottomSheetPreview() { +fun PlaceSearchBottomSheetPreview() { DATEROADTheme { var isBottomSheetOpen by rememberSaveable { mutableStateOf(false) } - var text by rememberSaveable { mutableStateOf("") } + var searchKeyword by rememberSaveable { mutableStateOf("") } + + val searchPlaceInfos = flowOf( + PagingData.from( + List(10) { + PlaceInfo("์นดํŽ˜ ๋‚˜๋ž‘", "๊ฒฝ๊ธฐ ์˜์™•์‹œ ์ฒญ๊ณ„๋กœ 217") + } + ) + ).collectAsLazyPagingItems() Button(onClick = { isBottomSheetOpen = true }) { Text( @@ -220,18 +232,11 @@ fun DateRoadPlaceSearchBottomSheetPreview() { ) } - DateRoadPlaceSearchBottomSheet( + PlaceSearchBottomSheet( isBottomSheetOpen = isBottomSheetOpen, - keyword = text, - placeSearchResult = PlaceSearchResult( - List(10) { - PlaceInfo( - "์นดํŽ˜ ๋‚˜๋ž‘", - "๊ฒฝ๊ธฐ ์˜์™•์‹œ ์ฒญ๊ณ„๋กœ 217" - ) - } - ), - onKeywordChanged = { text = it }, + searchKeyword = searchKeyword, + searchPlaceInfos = searchPlaceInfos, + onKeywordChanged = { searchKeyword = it }, onPlaceSelected = {}, onDismissRequest = { isBottomSheetOpen = !isBottomSheetOpen } ) @@ -247,3 +252,5 @@ fun EmptyPlaceSearchResultPreview() { } private const val BOTTOM_SHEET_OPEN_RATIO = 0.8f +const val PAGE_SIZE = 10 +const val MAX_SIZE = 50 From 3882d9644bc1cf57163cada418812b3c15e3cf4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=84=EC=9A=B0?= <85734140+jinuemong@users.noreply.github.com> Date: Mon, 7 Apr 2025 21:46:29 +0900 Subject: [PATCH 34/49] =?UTF-8?q?[feat]=20=ED=8F=AC=EC=9D=B8=ED=8A=B8=20?= =?UTF-8?q?=EB=A1=9C=EA=B9=85=20=EC=B6=94=EA=B0=80=20(#31)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: ๋กœ๊น… ์ถ”๊ฐ€ * feat: ์ฝ”์Šค ๋กœ๊น… ๋“ฑ๋ก --- .../ui/coursedetail/CourseDetailScreen.kt | 17 ++++++++------- .../ui/pointhistory/PointHistoryScreen.kt | 21 +++++++++++++------ 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailScreen.kt index abacfb2..46b6dd7 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/coursedetail/CourseDetailScreen.kt @@ -55,6 +55,7 @@ import org.sopt.teamdateroad.presentation.ui.coursedetail.component.CourseDetail import org.sopt.teamdateroad.presentation.ui.coursedetail.component.CourseDetailBottomBar import org.sopt.teamdateroad.presentation.ui.coursedetail.component.CourseDetailUnopenedDetail import org.sopt.teamdateroad.presentation.ui.coursedetail.component.courseDetailOpenedDetail +import org.sopt.teamdateroad.presentation.util.AdsAmplitude import org.sopt.teamdateroad.presentation.util.CourseDetail.POINT_LACK import org.sopt.teamdateroad.presentation.util.CourseDetailAmplitude.CLICK_COURSE_BACK import org.sopt.teamdateroad.presentation.util.CourseDetailAmplitude.CLICK_COURSE_PURCHASE @@ -174,13 +175,18 @@ fun CourseDetailRoute( viewModel.setEvent(CourseDetailContract.CourseDetailEvent.OnReportWebViewClicked) }, onReportWebViewClose = { viewModel.setEvent(CourseDetailContract.CourseDetailEvent.DismissReportWebView) }, - onSelectEnroll = { - viewModel.setSideEffect(CourseDetailContract.CourseDetailSideEffect.NavigateToEnroll(enrollType = EnrollType.COURSE, viewPath = COURSE_DETAIL, id = null)) - }, onDismissCollectPoint = { + AmplitudeUtils.trackEvent(eventName = AdsAmplitude.CLICK_COLLECT_POINT_CLOSE) viewModel.setEvent(CourseDetailContract.CourseDetailEvent.DismissDialogPointLack) }, + onSelectEnroll = { + AmplitudeUtils.trackEvent(eventName = AdsAmplitude.CLICK_COURSE) + viewModel.setEvent(CourseDetailContract.CourseDetailEvent.DismissDialogPointLack) + viewModel.setSideEffect(CourseDetailContract.CourseDetailSideEffect.NavigateToEnroll(enrollType = EnrollType.COURSE, viewPath = COURSE_DETAIL, id = null)) + }, onSelectAds = { + AmplitudeUtils.trackEvent(eventName = AdsAmplitude.CLICK_AD) + viewModel.setEvent(CourseDetailContract.CourseDetailEvent.DismissDialogPointLack) viewModel.setSideEffect(CourseDetailContract.CourseDetailSideEffect.NavigateToAds) }, onDismissFullAdsDialog = { @@ -403,11 +409,8 @@ fun CourseDetailScreen( DateRoadCollectPointType.WATCH_ADS -> onSelectAds() DateRoadCollectPointType.COURSE_REGISTRATION -> onSelectEnroll() } - onDismissCollectPoint() }, - onDismissRequest = { - onDismissCollectPoint() - } + onDismissRequest = onDismissCollectPoint ) DateRoadBasicBottomSheet( diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryScreen.kt index 20cdb86..63aaba2 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryScreen.kt @@ -48,7 +48,9 @@ import org.sopt.teamdateroad.presentation.ui.component.view.DateRoadIdleView import org.sopt.teamdateroad.presentation.ui.component.view.DateRoadLoadingView import org.sopt.teamdateroad.presentation.ui.pointhistory.component.PointHistoryCard import org.sopt.teamdateroad.presentation.ui.pointhistory.component.PointHistoryPointBox +import org.sopt.teamdateroad.presentation.util.AdsAmplitude import org.sopt.teamdateroad.presentation.util.ViewPath.POINT_HISTORY +import org.sopt.teamdateroad.presentation.util.amplitude.AmplitudeUtils import org.sopt.teamdateroad.presentation.util.view.LoadState import org.sopt.teamdateroad.ui.theme.DATEROADTheme import org.sopt.teamdateroad.ui.theme.DateRoadTheme @@ -68,6 +70,7 @@ fun PointHistoryRoute( LaunchedEffect(Unit) { viewModel.fetchPointHistory() viewModel.fetchUserPoint() + AmplitudeUtils.trackEvent(eventName = AdsAmplitude.VIEW_POINT) } LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { @@ -120,9 +123,16 @@ fun PointHistoryRoute( ) }, onTopBarIconClicked = { viewModel.setSideEffect(PointHistoryContract.PointHistorySideEffect.PopBackStack) }, - onClickCollectPoint = { viewModel.setEvent(PointHistoryContract.PointHistoryEvent.OnPointCollectBottomSheetClick) }, - onDisMissCollectPoint = { viewModel.setEvent(PointHistoryContract.PointHistoryEvent.OnPointCollectBottomSheetDismiss) }, + onClickCollectPoint = { + viewModel.setEvent(PointHistoryContract.PointHistoryEvent.OnPointCollectBottomSheetClick) + }, + onDisMissCollectPoint = { + AmplitudeUtils.trackEvent(eventName = AdsAmplitude.CLICK_COLLECT_POINT_CLOSE) + viewModel.setEvent(PointHistoryContract.PointHistoryEvent.OnPointCollectBottomSheetDismiss) + }, onSelectEnroll = { + AmplitudeUtils.trackEvent(eventName = AdsAmplitude.CLICK_COURSE) + viewModel.setEvent(PointHistoryContract.PointHistoryEvent.OnPointCollectBottomSheetDismiss) viewModel.setSideEffect( PointHistoryContract.PointHistorySideEffect.NavigateToEnroll( enrollType = EnrollType.COURSE, @@ -132,6 +142,8 @@ fun PointHistoryRoute( ) }, onSelectAds = { + AmplitudeUtils.trackEvent(eventName = AdsAmplitude.CLICK_AD) + viewModel.setEvent(PointHistoryContract.PointHistoryEvent.OnPointCollectBottomSheetDismiss) viewModel.setSideEffect(PointHistoryContract.PointHistorySideEffect.NavigateToAds) }, onDismissFullAdsDialog = { @@ -235,11 +247,8 @@ fun PointHistoryScreen( DateRoadCollectPointType.WATCH_ADS -> onSelectAds() DateRoadCollectPointType.COURSE_REGISTRATION -> onSelectEnroll() } - onDisMissCollectPoint() }, - onDismissRequest = { - onDisMissCollectPoint() - } + onDismissRequest = onDisMissCollectPoint ) } From 67d727c8fc3d77e7a55065a54aac93884c82ba60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=84=EC=9A=B0?= <85734140+jinuemong@users.noreply.github.com> Date: Mon, 7 Apr 2025 21:47:03 +0900 Subject: [PATCH 35/49] =?UTF-8?q?[refactor]=20=EC=9E=A5=EC=86=8C=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20LazyPaging=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20(#34)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor : collect ๋กœ์ง ๊ฐœ์„  * style: ์ฝ”๋“œ ํฌ๋งท ์ˆ˜์ • & ๋ฆฐํŠธ ์ฒดํฌ --- .../PlaceSearchDataSourceImpl.kt | 4 +- .../presentation/ui/enroll/EnrollScreen.kt | 9 +- .../ui/enroll/EnrollSecondScreen.kt | 9 +- .../ui/enroll/PlaceSearchBottomSheet.kt | 256 ------------------ 4 files changed, 8 insertions(+), 270 deletions(-) delete mode 100644 app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/PlaceSearchBottomSheet.kt diff --git a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/datasourceimpl/PlaceSearchDataSourceImpl.kt b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/datasourceimpl/PlaceSearchDataSourceImpl.kt index 06ba3f9..f519581 100644 --- a/app/src/main/java/org/sopt/teamdateroad/data/dataremote/datasourceimpl/PlaceSearchDataSourceImpl.kt +++ b/app/src/main/java/org/sopt/teamdateroad/data/dataremote/datasourceimpl/PlaceSearchDataSourceImpl.kt @@ -10,8 +10,8 @@ import kotlinx.coroutines.flow.Flow import org.sopt.teamdateroad.data.dataremote.datasource.PlaceSearchDataSource import org.sopt.teamdateroad.data.dataremote.model.response.ResponsePlaceInfoDto import org.sopt.teamdateroad.data.dataremote.service.PlaceSearchService -import org.sopt.teamdateroad.presentation.ui.enroll.MAX_SIZE -import org.sopt.teamdateroad.presentation.ui.enroll.PAGE_SIZE +import org.sopt.teamdateroad.presentation.ui.enroll.component.MAX_SIZE +import org.sopt.teamdateroad.presentation.ui.enroll.component.PAGE_SIZE class PlaceSearchDataSourceImpl @Inject constructor(private val placeSearchService: PlaceSearchService) : PlaceSearchDataSource { override suspend fun getPlaceSearchResult(keyword: String): Flow> { diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt index ddcba6c..9f16532 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt @@ -29,12 +29,9 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle -import androidx.paging.PagingData -import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems import java.time.LocalDate import java.time.format.DateTimeFormatter -import kotlinx.coroutines.flow.flowOf import org.sopt.teamdateroad.R import org.sopt.teamdateroad.domain.model.Place import org.sopt.teamdateroad.domain.model.PlaceInfo @@ -214,7 +211,7 @@ fun EnrollRoute( padding = padding, enrollUiState = uiState, searchKeyword = searchKeyword, - searchPlaceInfos = searchPlaceInfos, + searchPlaceInfos = searchPlaceInfos.itemSnapshotList.items, onTopBarBackButtonClick = { viewModel.setEvent(EnrollContract.EnrollEvent.OnTopBarBackButtonClick) @@ -345,7 +342,7 @@ fun EnrollScreen( padding: PaddingValues, enrollUiState: EnrollContract.EnrollUiState = EnrollContract.EnrollUiState(), searchKeyword: String, - searchPlaceInfos: LazyPagingItems, + searchPlaceInfos: List, onTopBarBackButtonClick: () -> Unit, onTopBarLoadButtonClick: () -> Unit, onEnrollButtonClick: () -> Unit, @@ -586,7 +583,7 @@ fun EnrollScreenPreview() { loadState = LoadState.Success ), searchKeyword = "", - searchPlaceInfos = flowOf(PagingData.empty()).collectAsLazyPagingItems(), + searchPlaceInfos = listOf(), onTopBarBackButtonClick = {}, onTopBarLoadButtonClick = {}, onEnrollButtonClick = {}, diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollSecondScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollSecondScreen.kt index 2ba0e2f..d31b3c5 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollSecondScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollSecondScreen.kt @@ -31,11 +31,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex -import androidx.paging.PagingData -import androidx.paging.compose.LazyPagingItems -import androidx.paging.compose.collectAsLazyPagingItems import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch import org.sopt.teamdateroad.R import org.sopt.teamdateroad.domain.model.Place @@ -44,6 +40,7 @@ import org.sopt.teamdateroad.presentation.type.PlaceCardType import org.sopt.teamdateroad.presentation.ui.component.button.DateRoadTextButton import org.sopt.teamdateroad.presentation.ui.component.card.DateRoadPlaceCard import org.sopt.teamdateroad.presentation.ui.enroll.component.EnrollPlaceInsertBar +import org.sopt.teamdateroad.presentation.ui.enroll.component.PlaceSearchBottomSheet import org.sopt.teamdateroad.presentation.util.Time import org.sopt.teamdateroad.presentation.util.draganddrop.rememberDragAndDropListState import org.sopt.teamdateroad.presentation.util.mutablelist.move @@ -55,7 +52,7 @@ import org.sopt.teamdateroad.ui.theme.DateRoadTheme fun EnrollSecondScreen( enrollUiState: EnrollContract.EnrollUiState = EnrollContract.EnrollUiState(), searchKeyword: String, - searchPlaceInfos: LazyPagingItems, + searchPlaceInfos: List, onPlaceSearchButtonClick: () -> Unit, onKeywordChanged: (String) -> Unit, onPlaceSelected: (PlaceInfo) -> Unit, @@ -199,7 +196,7 @@ fun EnrollSecondScreenPreview() { DATEROADTheme { EnrollSecondScreen( searchKeyword = "", - searchPlaceInfos = flowOf(PagingData.empty()).collectAsLazyPagingItems(), + searchPlaceInfos = listOf(), onPlaceSearchButtonClick = {}, onAddPlaceButtonClick = {}, onSelectedPlaceCourseTimeClick = {}, diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/PlaceSearchBottomSheet.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/PlaceSearchBottomSheet.kt deleted file mode 100644 index 8ce441b..0000000 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/PlaceSearchBottomSheet.kt +++ /dev/null @@ -1,256 +0,0 @@ -package org.sopt.teamdateroad.presentation.ui.enroll - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.Button -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.material3.TextFieldDefaults -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.paging.PagingData -import androidx.paging.compose.LazyPagingItems -import androidx.paging.compose.collectAsLazyPagingItems -import com.holix.android.bottomsheetdialog.compose.BottomSheetBehaviorProperties -import com.holix.android.bottomsheetdialog.compose.BottomSheetDialog -import com.holix.android.bottomsheetdialog.compose.BottomSheetDialogProperties -import kotlinx.coroutines.flow.flowOf -import org.sopt.teamdateroad.R -import org.sopt.teamdateroad.domain.model.PlaceInfo -import org.sopt.teamdateroad.presentation.ui.enroll.component.EnrollPlaceSearchItem -import org.sopt.teamdateroad.presentation.util.modifier.noRippleClickable -import org.sopt.teamdateroad.ui.theme.DATEROADTheme -import org.sopt.teamdateroad.ui.theme.DateRoadTheme - -@Composable -fun PlaceSearchBottomSheet( - isBottomSheetOpen: Boolean, - searchKeyword: String, - searchPlaceInfos: LazyPagingItems, - onKeywordChanged: (String) -> Unit, - onPlaceSelected: (PlaceInfo) -> Unit, - onDismissRequest: () -> Unit -) { - if (isBottomSheetOpen) { - BottomSheetDialog( - onDismissRequest = onDismissRequest, - properties = BottomSheetDialogProperties( - behaviorProperties = BottomSheetBehaviorProperties( - state = BottomSheetBehaviorProperties.State.HalfExpanded, - halfExpandedRatio = BOTTOM_SHEET_OPEN_RATIO, - skipCollapsed = true, - isDraggable = false - ) - ) - ) { - Column( - modifier = Modifier - .clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)) - .background(DateRoadTheme.colors.white) - ) { - Spacer(modifier = Modifier.height(23.dp)) - - Row( - modifier = Modifier - .fillMaxWidth() - .height(40.dp) - .padding(start = 25.dp, end = 12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = stringResource(R.string.enroll_place_search_bottom_sheet_title), - color = DateRoadTheme.colors.black, - style = DateRoadTheme.typography.bodyBold17 - ) - - Spacer(modifier = Modifier.weight(1f)) - - Image( - modifier = Modifier - .size(40.dp) - .noRippleClickable(onClick = onDismissRequest) - .padding(15.dp), - painter = painterResource(id = R.drawable.ic_bottom_sheet_close), - contentDescription = null - ) - } - - Spacer(modifier = Modifier.height(22.dp)) - - TextField( - value = searchKeyword, - onValueChange = onKeywordChanged, - modifier = Modifier - .fillMaxWidth() - .height(54.dp) - .padding(start = 14.dp, end = 20.dp), - placeholder = { - Text( - modifier = Modifier, - text = stringResource(R.string.enroll_place_insert_bar_enter_place_placeholder), - color = DateRoadTheme.colors.gray300, - style = DateRoadTheme.typography.bodySemi15 - ) - }, - trailingIcon = { - IconButton( - modifier = Modifier.size(20.dp), - onClick = { - onKeywordChanged("") - } - ) { - Icon( - painter = painterResource(id = R.drawable.btn_enroll_delete_picture), - contentDescription = null, - tint = Color.Unspecified - ) - } - }, - colors = TextFieldDefaults.colors( - focusedContainerColor = DateRoadTheme.colors.gray100, - unfocusedContainerColor = DateRoadTheme.colors.gray100, - cursorColor = DateRoadTheme.colors.purple600, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - disabledIndicatorColor = Color.Transparent - ), - shape = RoundedCornerShape(14.dp), - singleLine = true, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Text, - imeAction = ImeAction.Search - ) - ) - - Spacer(modifier = Modifier.height(10.dp)) - - if (searchPlaceInfos.itemCount > 0) { - LazyColumn(modifier = Modifier.weight(1f)) { - items( - searchPlaceInfos.itemCount - ) { index -> - val placeInfo = searchPlaceInfos[index] ?: return@items - - EnrollPlaceSearchItem( - keyword = searchKeyword, - placeInfo = placeInfo, - onClick = { onPlaceSelected(placeInfo) } - ) - - if (index != searchPlaceInfos.itemCount - 1) { - HorizontalDivider( - modifier = Modifier.fillMaxWidth(), - color = DateRoadTheme.colors.gray100, - thickness = 1.dp - ) - } - } - } - Spacer(modifier = Modifier.height(12.dp)) - } else { - EmptyPlaceSearchResult() - } - } - } - } -} - -@Composable -private fun EmptyPlaceSearchResult() { - Box( - modifier = Modifier - .fillMaxSize() - .padding(bottom = 70.dp) - ) { - Column(modifier = Modifier.align(Alignment.Center)) { - Image( - modifier = Modifier - .width(167.dp) - .height(191.dp), - painter = painterResource(R.drawable.img_place_search_no_match), - contentDescription = null - ) - Spacer(modifier = Modifier.height(40.dp)) - Text( - text = stringResource(R.string.enroll_place_search_no_match), - color = DateRoadTheme.colors.gray300, - style = DateRoadTheme.typography.titleBold18 - ) - } - } -} - -@Preview -@Composable -fun PlaceSearchBottomSheetPreview() { - DATEROADTheme { - var isBottomSheetOpen by rememberSaveable { mutableStateOf(false) } - var searchKeyword by rememberSaveable { mutableStateOf("") } - - val searchPlaceInfos = flowOf( - PagingData.from( - List(10) { - PlaceInfo("์นดํŽ˜ ๋‚˜๋ž‘", "๊ฒฝ๊ธฐ ์˜์™•์‹œ ์ฒญ๊ณ„๋กœ 217") - } - ) - ).collectAsLazyPagingItems() - - Button(onClick = { isBottomSheetOpen = true }) { - Text( - text = "DateRoadPlaceSearchBottomSheet", - color = DateRoadTheme.colors.black, - style = DateRoadTheme.typography.titleBold18 - ) - } - - PlaceSearchBottomSheet( - isBottomSheetOpen = isBottomSheetOpen, - searchKeyword = searchKeyword, - searchPlaceInfos = searchPlaceInfos, - onKeywordChanged = { searchKeyword = it }, - onPlaceSelected = {}, - onDismissRequest = { isBottomSheetOpen = !isBottomSheetOpen } - ) - } -} - -@Preview(showBackground = true) -@Composable -fun EmptyPlaceSearchResultPreview() { - DATEROADTheme { - EmptyPlaceSearchResult() - } -} - -private const val BOTTOM_SHEET_OPEN_RATIO = 0.8f -const val PAGE_SIZE = 10 -const val MAX_SIZE = 50 From 635d3e9a23cdf4ce69c202c32762918b9034861f Mon Sep 17 00:00:00 2001 From: Yaechan Park <102402485+dpcks0509@users.noreply.github.com> Date: Mon, 7 Apr 2025 21:47:14 +0900 Subject: [PATCH 36/49] =?UTF-8?q?fix:=20=EC=9E=A5=EC=86=8C=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=ED=82=A4=EB=B3=B4=EB=93=9C,=20=EB=B0=94=ED=85=80?= =?UTF-8?q?=EC=8B=9C=ED=8A=B8=20=EC=95=A0=EB=8B=88=EB=A9=94=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EA=B2=B9=EC=B9=A8=20=ED=95=B4=EA=B2=B0=20(#36)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/enroll/EnrollScreen.kt | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt index 9f16532..7204ae6 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt @@ -22,6 +22,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -113,6 +114,7 @@ fun EnrollRoute( val searchPlaceInfos = viewModel.searchPlaceInfos.collectAsLazyPagingItems() val lifecycleOwner = LocalLifecycleOwner.current + val keyboardController = LocalSoftwareKeyboardController.current val getGalleryLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? -> if (uri != null) viewModel.setEvent(EnrollContract.EnrollEvent.SetImage(images = listOf(uri.toString()))) @@ -257,9 +259,18 @@ fun EnrollRoute( AmplitudeUtils.trackEvent(eventName = CLICK_BRING_COURSE) }, onEnrollButtonClick = { viewModel.setEvent(EnrollContract.EnrollEvent.OnEnrollButtonClick) }, - onDateTextFieldClick = { viewModel.setEvent(EnrollContract.EnrollEvent.OnDateTextFieldClick) }, - onTimeTextFieldClick = { viewModel.setEvent(EnrollContract.EnrollEvent.OnTimeTextFieldClick) }, - onRegionTextFieldClick = { viewModel.setEvent(EnrollContract.EnrollEvent.OnRegionTextFieldClick) }, + onDateTextFieldClick = { + keyboardController?.hide() + viewModel.setEvent(EnrollContract.EnrollEvent.OnDateTextFieldClick) + }, + onTimeTextFieldClick = { + keyboardController?.hide() + viewModel.setEvent(EnrollContract.EnrollEvent.OnTimeTextFieldClick) + }, + onRegionTextFieldClick = { + keyboardController?.hide() + viewModel.setEvent(EnrollContract.EnrollEvent.OnRegionTextFieldClick) + }, onPlaceSearchButtonClick = { viewModel.setEvent(EnrollContract.EnrollEvent.OnPlaceSearchButtonClick) }, onKeywordChanged = { keyword -> viewModel.setEvent(EnrollContract.EnrollEvent.OnKeywordChanged(keyword = keyword)) }, onPlaceSelected = { placeInfo -> viewModel.setEvent(EnrollContract.EnrollEvent.OnPlaceSelected(placeInfo = placeInfo)) }, From 705fc7a9de59063dc4f1630c3c152cc9f710ab9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=8C=E1=85=B5=E1=86=AB?= =?UTF-8?q?=E1=84=8B=E1=85=AE?= Date: Tue, 8 Apr 2025 23:26:51 +0900 Subject: [PATCH 37/49] =?UTF-8?q?feat=20:=20=EB=B9=8C=EB=93=9C=20=ED=82=A4?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/android_cd.yml | 48 ++++++++++++++++++++++---------- app/build.gradle.kts | 10 +++++++ 2 files changed, 43 insertions(+), 15 deletions(-) diff --git a/.github/workflows/android_cd.yml b/.github/workflows/android_cd.yml index a5910a1..01f41b0 100644 --- a/.github/workflows/android_cd.yml +++ b/.github/workflows/android_cd.yml @@ -16,10 +16,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Gradle cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ~/.gradle/caches @@ -28,8 +28,8 @@ jobs: restore-keys: | ${{ runner.os }}-gradle- - - name: set up JDK 17 - uses: actions/setup-java@v3 + - name: Set up JDK 17 + uses: actions/setup-java@v4 with: java-version: 17 distribution: 'temurin' @@ -43,7 +43,7 @@ jobs: - name: Decode google-services.json env: FIREBASE_SECRET: ${{ secrets.FIREBASE_SECRET }} - run: echo $FIREBASE_SECRET > app/google-services.json + run: echo "$FIREBASE_SECRET" > app/google-services.json - name: Access local properties env: @@ -54,22 +54,39 @@ jobs: GOOGLE_ADS_API_ID: ${{ secrets.GOOGLE_ADS_API_ID }} GOOGLE_ADS_API_ID_MANIFEST: ${{ secrets.GOOGLE_ADS_API_ID }} run: | - echo "dev.base.url=\"$BASE_URL\"" >> local.properties + echo "dev.base.url=\"$HFM_BASE_URL\"" >> local.properties echo "kakao.native.app.key.manifest=\"$KAKAO_NATIVE_APP_KEY_MANIFEST\"" >> local.properties echo "kakao.native.app.key=\"$KAKAO_NATIVE_APP_KEY\"" >> local.properties echo "amplitude.dev.api.key=\"$AMPLITUDE_API_KEY\"" >> local.properties - echo "google.ads.api.id=\"GOOGLE_ADS_API_ID\"" >> local.properties - echo "google.ads.api.id.manifest=\"GOOGLE_ADS_API_ID_MANIFEST\"" >> local.properties + echo "google.ads.api.id=\"$GOOGLE_ADS_API_ID\"" >> local.properties + echo "google.ads.api.id.manifest=\"$GOOGLE_ADS_API_ID_MANIFEST\"" >> local.properties - - name: Build Release APK + - name: Decode Keystore + run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > dateroad.keystore + + - name: Export signing environment run: | - ./gradlew :app:assembleRelease + echo "STORE_PASSWORD=${{ secrets.KEYSTORE_PASSWORD }}" >> $GITHUB_ENV + echo "KEY_ALIAS=${{ secrets.KEY_ALIAS }}" >> $GITHUB_ENV + echo "KEY_PASSWORD=${{ secrets.KEY_PASSWORD }}" >> $GITHUB_ENV + + - name: Build Signed Release APK + run: ./gradlew :app:assembleRelease + + - name: Upload Signed APK + uses: actions/upload-artifact@v4 + with: + name: release-apk + path: ./app/build/outputs/apk/release/app-release.apk + + - name: Build Signed Release AAB + run: ./gradlew :app:bundleRelease - - name: Upload Release APK - uses: actions/upload-artifact@v3 + - name: Upload Signed AAB + uses: actions/upload-artifact@v4 with: - name: release - path: ./app/build/outputs/apk/release/app-release-unsigned.apk + name: release-aab + path: ./app/build/outputs/bundle/release/app-release.aab - name: Discord Notify - Success if: ${{ success() }} @@ -81,7 +98,8 @@ jobs: username: DATEROAD-ANDROID ๐Ÿซ content: | Release Test๊ฐ€ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค! - [โ‡๏ธ APK๋ฅผ ๋‹ค์šด๋กœ๋“œํ•ด ๋ณด์„ธ์š”! โ‡๏ธ](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) + [โ‡๏ธ APK ๋‹ค์šด๋กœ๋“œ](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) + [๐Ÿ“ฆ AAB ๋‹ค์šด๋กœ๋“œ](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) - name: Discord Notify - Failure if: ${{ failure() }} diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 75eca75..264b651 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -37,6 +37,14 @@ android { manifestPlaceholders["KAKAO_NATIVE_APP_KEY_MANIFEST"] = properties["kakao.native.app.key.manifest"] as String manifestPlaceholders["GOOGLE_ADS_API_ID_MANIFEST"] = properties["google.ads.api.id.manifest"] as String } + signingConfigs { + create("release") { + storeFile = file("dateroad.keystore") + storePassword = System.getenv("STORE_PASSWORD") + keyAlias = System.getenv("KEY_ALIAS") + keyPassword = System.getenv("KEY_PASSWORD") + } + } buildTypes { debug { @@ -47,6 +55,7 @@ android { } release { + signingConfig = signingConfigs.getByName("release") isMinifyEnabled = true isShrinkResources = true buildConfigField("String", "BASE_URL", properties["prod.base.url"].toString()) @@ -58,6 +67,7 @@ android { ) } } + compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 From 3b688faec8cfdffe6fd999176984eb20a9b7966d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=8C=E1=85=B5=E1=86=AB?= =?UTF-8?q?=E1=84=8B=E1=85=AE?= Date: Tue, 8 Apr 2025 23:52:35 +0900 Subject: [PATCH 38/49] =?UTF-8?q?feat=20:=20=EB=B9=8C=EB=93=9C=20=ED=82=A4?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/android_cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/android_cd.yml b/.github/workflows/android_cd.yml index 01f41b0..9d986c3 100644 --- a/.github/workflows/android_cd.yml +++ b/.github/workflows/android_cd.yml @@ -62,7 +62,7 @@ jobs: echo "google.ads.api.id.manifest=\"$GOOGLE_ADS_API_ID_MANIFEST\"" >> local.properties - name: Decode Keystore - run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > dateroad.keystore + run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > app/dateroad.keystore - name: Export signing environment run: | From 7821b1bdb2905b4dbdcf570fd4e39346ee3ed0be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=8C=E1=85=B5=E1=86=AB?= =?UTF-8?q?=E1=84=8B=E1=85=AE?= Date: Wed, 9 Apr 2025 00:02:50 +0900 Subject: [PATCH 39/49] =?UTF-8?q?feat=20:=20=EB=B9=8C=EB=93=9C=20=ED=82=A4?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/android_cd.yml | 2 +- app/build.gradle.kts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/android_cd.yml b/.github/workflows/android_cd.yml index 9d986c3..2cc5065 100644 --- a/.github/workflows/android_cd.yml +++ b/.github/workflows/android_cd.yml @@ -62,7 +62,7 @@ jobs: echo "google.ads.api.id.manifest=\"$GOOGLE_ADS_API_ID_MANIFEST\"" >> local.properties - name: Decode Keystore - run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > app/dateroad.keystore + run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > app/date_road.keystore - name: Export signing environment run: | diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 264b651..5a8b717 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -39,7 +39,7 @@ android { } signingConfigs { create("release") { - storeFile = file("dateroad.keystore") + storeFile = file("date_road.keystore") storePassword = System.getenv("STORE_PASSWORD") keyAlias = System.getenv("KEY_ALIAS") keyPassword = System.getenv("KEY_PASSWORD") From 45d6747783fdbf3c009b8b8994844e4296792426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=8C=E1=85=B5=E1=86=AB?= =?UTF-8?q?=E1=84=8B=E1=85=AE?= Date: Wed, 9 Apr 2025 00:20:02 +0900 Subject: [PATCH 40/49] =?UTF-8?q?feat=20:=20=EB=B9=8C=EB=93=9C=20=ED=82=A4?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5a8b717..c1f2474 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -39,7 +39,7 @@ android { } signingConfigs { create("release") { - storeFile = file("date_road.keystore") + storeFile = file("app/date_road.keystore") storePassword = System.getenv("STORE_PASSWORD") keyAlias = System.getenv("KEY_ALIAS") keyPassword = System.getenv("KEY_PASSWORD") From 9d25e88588a9814a338cc79723c3f0ada2c45ccd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=8C=E1=85=B5=E1=86=AB?= =?UTF-8?q?=E1=84=8B=E1=85=AE?= Date: Wed, 9 Apr 2025 00:33:29 +0900 Subject: [PATCH 41/49] =?UTF-8?q?feat=20:=20=EB=B9=8C=EB=93=9C=20=ED=82=A4?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c1f2474..5a8b717 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -39,7 +39,7 @@ android { } signingConfigs { create("release") { - storeFile = file("app/date_road.keystore") + storeFile = file("date_road.keystore") storePassword = System.getenv("STORE_PASSWORD") keyAlias = System.getenv("KEY_ALIAS") keyPassword = System.getenv("KEY_PASSWORD") From 56ce39c79fa28d7b07f753140c3afa5d31344d0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=8C=E1=85=B5=E1=86=AB?= =?UTF-8?q?=E1=84=8B=E1=85=AE?= Date: Wed, 9 Apr 2025 00:45:06 +0900 Subject: [PATCH 42/49] =?UTF-8?q?feat=20:=20=EB=B9=8C=EB=93=9C=20=ED=82=A4?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5a8b717..14b11ce 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -38,11 +38,14 @@ android { manifestPlaceholders["GOOGLE_ADS_API_ID_MANIFEST"] = properties["google.ads.api.id.manifest"] as String } signingConfigs { - create("release") { - storeFile = file("date_road.keystore") - storePassword = System.getenv("STORE_PASSWORD") - keyAlias = System.getenv("KEY_ALIAS") - keyPassword = System.getenv("KEY_PASSWORD") + val keystoreFile = file("date_road.keystore") + if (keystoreFile.exists()) { + create("release") { + storeFile = keystoreFile + storePassword = System.getenv("STORE_PASSWORD") + keyAlias = System.getenv("KEY_ALIAS") + keyPassword = System.getenv("KEY_PASSWORD") + } } } From 1e1511b0708db4742f21949642e9fac18b014f9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=8C=E1=85=B5=E1=86=AB?= =?UTF-8?q?=E1=84=8B=E1=85=AE?= Date: Wed, 9 Apr 2025 00:46:05 +0900 Subject: [PATCH 43/49] =?UTF-8?q?feat=20:=20=EB=B9=8C=EB=93=9C=20=ED=82=A4?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 14b11ce..b2ae770 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -16,6 +16,8 @@ val properties = Properties().apply { } android { + val keystoreFile = file("date_road.keystore") + namespace = "org.sopt.teamdateroad" compileSdk = libs.versions.compileSdk.get().toInt() @@ -38,7 +40,6 @@ android { manifestPlaceholders["GOOGLE_ADS_API_ID_MANIFEST"] = properties["google.ads.api.id.manifest"] as String } signingConfigs { - val keystoreFile = file("date_road.keystore") if (keystoreFile.exists()) { create("release") { storeFile = keystoreFile @@ -58,7 +59,9 @@ android { } release { - signingConfig = signingConfigs.getByName("release") + if (keystoreFile.exists()) { + signingConfig = signingConfigs.getByName("release") + } isMinifyEnabled = true isShrinkResources = true buildConfigField("String", "BASE_URL", properties["prod.base.url"].toString()) From acc378547c981589d0e4ec8f598f6fb75a12129c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=84=EC=9A=B0?= <85734140+jinuemong@users.noreply.github.com> Date: Tue, 29 Apr 2025 15:15:06 +0900 Subject: [PATCH 44/49] =?UTF-8?q?1=EC=B0=A8=20QA=20=EB=B0=98=EC=98=81=20(#?= =?UTF-8?q?42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: ์บ˜๋ฆฐ๋” ์ˆ˜์ • * feat: ๊ธˆ์•ก ํ•„๋“œ ํฌ๋ฉง ์ ์šฉ * feat: ์Šคํ”Œ๋ž˜์‹œ ํ™”๋ฉด ๋ฒ„ํŠผ ๊ธ€์”จ ์œ„์น˜ ์กฐ์ • * feat: ํ™ˆ ์ƒ๋‹จ ์—ฌ๋ฐฑ ์ˆ˜์ • * feat: ๋ฐ์ดํŠธ ์ผ์ • ๋ฐ ํŒ์—… ์ฐฝ ์œ„์น˜ ์ˆ˜์ • * feat: empty view ์ „๋ถ€ ์ˆ˜์ • * style: ktLint ์ ์šฉ --------- Co-authored-by: dpcks0509 --- .../ui/component/card/DateRoadCourseCard.kt | 1 + .../ui/component/view/DateRoadEmptyView.kt | 5 +- .../presentation/ui/enroll/EnrollContract.kt | 4 +- .../ui/enroll/EnrollThirdScreen.kt | 4 +- .../component/PlaceSearchBottomSheet.kt | 56 ++++++++++--------- .../ui/home/component/HomeTopBar.kt | 20 ++++--- .../presentation/ui/look/LookScreen.kt | 12 +++- .../ui/mycourse/MyCourseScreen.kt | 44 ++++++++------- .../presentation/ui/past/PastScreen.kt | 7 ++- .../ui/pointhistory/PointHistoryScreen.kt | 52 ++++++++--------- .../presentation/ui/read/ReadScreen.kt | 1 + .../presentation/ui/signin/SignInScreen.kt | 55 ++++++++++++------ .../ui/timeline/TimelineScreen.kt | 6 +- .../presentation/util/Constraints.kt | 2 +- .../util/view/NumberCommaTransformation.kt | 32 +++++++++++ app/src/main/res/values/strings.xml | 2 +- 16 files changed, 190 insertions(+), 113 deletions(-) create mode 100644 app/src/main/java/org/sopt/teamdateroad/presentation/util/view/NumberCommaTransformation.kt diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/card/DateRoadCourseCard.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/card/DateRoadCourseCard.kt index fc1f9c1..53b344b 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/card/DateRoadCourseCard.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/card/DateRoadCourseCard.kt @@ -40,6 +40,7 @@ fun DateRoadCourseCard( Row( modifier = modifier .fillMaxWidth() + .padding(end = 16.dp) .height(130.dp) .background(DateRoadTheme.colors.white) .noRippleClickable(onClick = { onClick(course.courseId) }) diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/view/DateRoadEmptyView.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/view/DateRoadEmptyView.kt index 459f5bc..0f5a818 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/view/DateRoadEmptyView.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/view/DateRoadEmptyView.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -30,7 +31,9 @@ fun DateRoadEmptyView( verticalArrangement = Arrangement.Center ) { Image( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .height(383.dp) + .fillMaxWidth(), painter = painterResource(id = emptyViewType.imageRes), contentDescription = null, contentScale = ContentScale.FillWidth diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollContract.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollContract.kt index 28fc395..d7c120b 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollContract.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollContract.kt @@ -50,7 +50,9 @@ class EnrollContract { val timePickers: List = listOf( Picker(items = listOf(TimePicker.AM, TimePicker.PM)), Picker(items = (HOUR_START..HOUR_END).map { it.toString() }), - Picker(items = (MINUTE_START..MINUTE_END).map { it.toString().padStart(2, '0') }) + Picker( + items = (MINUTE_START..MINUTE_END step 5).map { it.toString().padStart(2, '0') } + ) ), val isRegionBottomSheetOpen: Boolean = false, val onRegionBottomSheetRegionSelected: RegionType? = RegionType.SEOUL, diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollThirdScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollThirdScreen.kt index 58415bb..9eb7f35 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollThirdScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollThirdScreen.kt @@ -17,6 +17,7 @@ import org.sopt.teamdateroad.R import org.sopt.teamdateroad.presentation.ui.component.textfield.DateRoadBasicTextField import org.sopt.teamdateroad.presentation.ui.component.textfield.DateRoadTextArea import org.sopt.teamdateroad.presentation.util.view.LoadState +import org.sopt.teamdateroad.presentation.util.view.NumberCommaTransformation import org.sopt.teamdateroad.ui.theme.DATEROADTheme @Composable @@ -45,7 +46,8 @@ fun EnrollThirdScreen( onValueChange = { newValue -> if (newValue.all { it.isDigit() }) onCostValueChange(newValue) }, - keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number) + keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number), + visualTransformation = NumberCommaTransformation() ) Spacer(modifier = Modifier.height(6.dp)) } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/component/PlaceSearchBottomSheet.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/component/PlaceSearchBottomSheet.kt index 6dcb28a..3bb8fdc 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/component/PlaceSearchBottomSheet.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/component/PlaceSearchBottomSheet.kt @@ -2,11 +2,10 @@ package org.sopt.teamdateroad.presentation.ui.enroll.component import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -73,13 +72,13 @@ fun PlaceSearchBottomSheet( .clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)) .background(DateRoadTheme.colors.white) ) { - Spacer(modifier = Modifier.height(23.dp)) + Spacer(modifier = Modifier.height(15.dp)) Row( modifier = Modifier .fillMaxWidth() .height(40.dp) - .padding(start = 25.dp, end = 12.dp), + .padding(start = 16.dp, end = 6.dp), verticalAlignment = Alignment.CenterVertically ) { Text( @@ -100,7 +99,7 @@ fun PlaceSearchBottomSheet( ) } - Spacer(modifier = Modifier.height(22.dp)) + Spacer(modifier = Modifier.height(16.dp)) TextField( value = searchKeyword, @@ -108,7 +107,7 @@ fun PlaceSearchBottomSheet( modifier = Modifier .fillMaxWidth() .height(54.dp) - .padding(start = 14.dp, end = 20.dp), + .padding(horizontal = 14.dp), placeholder = { Text( modifier = Modifier, @@ -147,10 +146,11 @@ fun PlaceSearchBottomSheet( ) ) - Spacer(modifier = Modifier.height(10.dp)) - if (searchPlaceInfos.isNotEmpty()) { LazyColumn(modifier = Modifier.weight(1f)) { + item { + Spacer(modifier = Modifier.height(10.dp)) + } itemsIndexed(searchPlaceInfos) { index: Int, placeInfo: PlaceInfo -> EnrollPlaceSearchItem( keyword = searchKeyword, @@ -166,8 +166,10 @@ fun PlaceSearchBottomSheet( ) } } + item { + Spacer(modifier = Modifier.height(12.dp)) + } } - Spacer(modifier = Modifier.height(12.dp)) } else { EmptyPlaceSearchResult() } @@ -178,26 +180,26 @@ fun PlaceSearchBottomSheet( @Composable private fun EmptyPlaceSearchResult() { - Box( + Column( modifier = Modifier - .fillMaxSize() - .padding(bottom = 70.dp) + .height(472.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center ) { - Column(modifier = Modifier.align(Alignment.Center)) { - Image( - modifier = Modifier - .width(167.dp) - .height(191.dp), - painter = painterResource(R.drawable.img_place_search_no_match), - contentDescription = null - ) - Spacer(modifier = Modifier.height(40.dp)) - Text( - text = stringResource(R.string.enroll_place_search_no_match), - color = DateRoadTheme.colors.gray300, - style = DateRoadTheme.typography.titleBold18 - ) - } + Image( + modifier = Modifier + .width(167.dp) + .height(191.dp), + painter = painterResource(R.drawable.img_place_search_no_match), + contentDescription = null + ) + Spacer(modifier = Modifier.height(38.dp)) + Text( + text = stringResource(R.string.enroll_place_search_no_match), + color = DateRoadTheme.colors.gray300, + style = DateRoadTheme.typography.titleBold18 + ) } } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/home/component/HomeTopBar.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/home/component/HomeTopBar.kt index f94162d..9469f02 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/home/component/HomeTopBar.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/home/component/HomeTopBar.kt @@ -1,11 +1,12 @@ package org.sopt.teamdateroad.presentation.ui.home.component import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -24,23 +25,26 @@ fun DateRoadHomeTopBar( profileImage: String? = null, onClick: () -> Unit = {} ) { - Row( + Box( modifier = Modifier .fillMaxWidth() + .height(54.dp) .background(Color.Transparent) - .padding(horizontal = 22.dp, vertical = 10.dp), - verticalAlignment = Alignment.CenterVertically + .padding(start = 11.dp, end = 16.dp) ) { Icon( painter = painterResource(id = R.drawable.ic_dateroad_logo), contentDescription = null, - tint = DateRoadTheme.colors.white + tint = DateRoadTheme.colors.white, + modifier = Modifier + .align(Alignment.CenterStart) + .size(54.dp) ) - Spacer(modifier = Modifier.weight(1f)) DateRoadPointTag( text = title, profileImage = profileImage, - onClick = onClick + onClick = onClick, + modifier = Modifier.align(Alignment.CenterEnd) ) } } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/look/LookScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/look/LookScreen.kt index b29c023..abcf866 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/look/LookScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/look/LookScreen.kt @@ -215,13 +215,16 @@ fun LookScreen( ) } } - Spacer(modifier = Modifier.height(20.dp)) + Spacer(modifier = Modifier.height(10.dp)) if (lookUiState.courses.isEmpty()) { Box( modifier = Modifier - .fillMaxSize() + .fillMaxSize(), + contentAlignment = Alignment.TopCenter ) { - DateRoadEmptyView(emptyViewType = EmptyViewType.LOOK) + DateRoadEmptyView( + emptyViewType = EmptyViewType.LOOK + ) } } LazyVerticalGrid( @@ -231,6 +234,9 @@ fun LookScreen( horizontalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(16.dp) ) { + item { + Spacer(modifier = Modifier.height(10.dp)) + } items(lookUiState.courses.size) { index -> LookCourseCard(course = lookUiState.courses[index], onClick = { onCourseCardClicked(lookUiState.courses[index].courseId) }) } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/mycourse/MyCourseScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/mycourse/MyCourseScreen.kt index 428fa0b..cddb45e 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/mycourse/MyCourseScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/mycourse/MyCourseScreen.kt @@ -3,6 +3,7 @@ package org.sopt.teamdateroad.presentation.ui.mycourse import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn @@ -116,28 +117,33 @@ fun MyCourseScreen( backGroundColor = DateRoadTheme.colors.white, onLeftIconClick = onIconClick ) - LazyColumn { - if (myCourseUiState.courses.isEmpty()) { - item { - DateRoadEmptyView( - emptyViewType = when (myCourseUiState.myCourseType) { - MyCourseType.ENROLL -> EmptyViewType.MY_COURSE_ENROLL - MyCourseType.READ -> EmptyViewType.MY_COURSE_READ + if (myCourseUiState.courses.isEmpty()) { + Column( + modifier = Modifier.fillMaxSize() + ) { + Spacer(modifier = Modifier.weight(60f)) + DateRoadEmptyView( + emptyViewType = when (myCourseUiState.myCourseType) { + MyCourseType.ENROLL -> EmptyViewType.MY_COURSE_ENROLL + MyCourseType.READ -> EmptyViewType.MY_COURSE_READ + } + ) + Spacer(modifier = Modifier.weight(165f)) + } + } else { + LazyColumn { + items(myCourseUiState.courses) { course -> + DateRoadCourseCard( + course = course, + onClick = { + when (myCourseUiState.myCourseType) { + MyCourseType.ENROLL -> navigateToCourseDetail(course.courseId) + MyCourseType.READ -> navigateToEnroll(course.courseId) + } } ) } } - items(myCourseUiState.courses) { course -> - DateRoadCourseCard( - course = course, - onClick = { - when (myCourseUiState.myCourseType) { - MyCourseType.ENROLL -> navigateToCourseDetail(course.courseId) - MyCourseType.READ -> navigateToEnroll(course.courseId) - } - } - ) - } } } } @@ -150,7 +156,7 @@ fun MyCourseScreenPreview() { padding = PaddingValues(0.dp), myCourseUiState = MyCourseContract.MyCourseUiState( loadState = LoadState.Success, - myCourseType = MyCourseType.READ, + myCourseType = MyCourseType.ENROLL, courses = listOf( Course( courseId = 1, diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/past/PastScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/past/PastScreen.kt index e5c6ff3..f83302c 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/past/PastScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/past/PastScreen.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn @@ -93,7 +94,11 @@ fun PastScreen( onLeftIconClick = popBackStack ) if (pastUiState.timelines.isEmpty()) { - DateRoadEmptyView(emptyViewType = EmptyViewType.PAST) + Column(modifier = Modifier.fillMaxSize()) { + Spacer(modifier = Modifier.weight(69f)) + DateRoadEmptyView(emptyViewType = EmptyViewType.PAST) + Spacer(modifier = Modifier.weight(165f)) + } } else { LazyColumn( modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 6.dp, bottom = 11.dp), diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryScreen.kt index 63aaba2..9fa90ea 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryScreen.kt @@ -1,7 +1,6 @@ package org.sopt.teamdateroad.presentation.ui.pointhistory import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer @@ -29,7 +28,6 @@ import com.google.android.gms.ads.rewarded.RewardedAd import com.google.android.gms.ads.rewarded.RewardedAdLoadCallback import org.sopt.teamdateroad.BuildConfig import org.sopt.teamdateroad.R -import org.sopt.teamdateroad.domain.model.Point import org.sopt.teamdateroad.domain.model.PointHistory import org.sopt.teamdateroad.domain.model.UserPoint import org.sopt.teamdateroad.presentation.type.EmptyViewType @@ -201,32 +199,26 @@ fun PointHistoryScreen( ) } } - LazyColumn { - val pointHistory = when (pointHistoryUiState.pointHistoryTabType) { - PointHistoryTabType.GAINED_HISTORY -> pointHistoryUiState.pointHistory.gained - PointHistoryTabType.USED_HISTORY -> pointHistoryUiState.pointHistory.used - } - if (pointHistory.isEmpty()) { - item { - Box( - modifier = Modifier - .fillParentMaxSize() - ) { - DateRoadEmptyView( - emptyViewType = when (pointHistoryUiState.pointHistoryTabType) { - PointHistoryTabType.USED_HISTORY -> EmptyViewType.POINT_HISTORY_USED_HISTORY - PointHistoryTabType.GAINED_HISTORY -> EmptyViewType.POINT_HISTORY_GAINED_HISTORY - } - ) - } + val pointHistory = when (pointHistoryUiState.pointHistoryTabType) { + PointHistoryTabType.GAINED_HISTORY -> pointHistoryUiState.pointHistory.gained + PointHistoryTabType.USED_HISTORY -> pointHistoryUiState.pointHistory.used + } + if (pointHistory.isEmpty()) { + DateRoadEmptyView( + emptyViewType = when (pointHistoryUiState.pointHistoryTabType) { + PointHistoryTabType.USED_HISTORY -> EmptyViewType.POINT_HISTORY_USED_HISTORY + PointHistoryTabType.GAINED_HISTORY -> EmptyViewType.POINT_HISTORY_GAINED_HISTORY + } + ) + } else { + LazyColumn { + items(pointHistory.size) { index -> + PointHistoryCard(point = pointHistory[index]) + HorizontalDivider( + color = DateRoadTheme.colors.gray100, + thickness = 1.dp + ) } - } - items(pointHistory.size) { index -> - PointHistoryCard(point = pointHistory[index]) - HorizontalDivider( - color = DateRoadTheme.colors.gray100, - thickness = 1.dp - ) } } } @@ -263,9 +255,9 @@ fun PointHistoryPreview() { loadState = LoadState.Success, pointHistory = PointHistory( gained = listOf( - Point(point = "+150", description = "์„œ๋ฒ„์˜ ๋ฐ”๋‹ค์—ฌํ–‰", createdAt = "2023.12.31"), - Point(point = "+150", description = "์„œ๋ฒ„์˜ ๋ฐ”๋‹ค์—ฌํ–‰", createdAt = "2023.12.31"), - Point(point = "+150", description = "์„œ๋ฒ„์˜ ๋ฐ”๋‹ค์—ฌํ–‰", createdAt = "2023.12.31") +// Point(point = "+150", description = "์„œ๋ฒ„์˜ ๋ฐ”๋‹ค์—ฌํ–‰", createdAt = "2023.12.31"), +// Point(point = "+150", description = "์„œ๋ฒ„์˜ ๋ฐ”๋‹ค์—ฌํ–‰", createdAt = "2023.12.31"), +// Point(point = "+150", description = "์„œ๋ฒ„์˜ ๋ฐ”๋‹ค์—ฌํ–‰", createdAt = "2023.12.31") ), used = listOf() ) diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/read/ReadScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/read/ReadScreen.kt index e817e79..a7230e7 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/read/ReadScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/read/ReadScreen.kt @@ -107,6 +107,7 @@ fun ReadScreen( text = stringResource(id = R.string.read_title_empty, readUiState.name), style = DateRoadTheme.typography.titleExtra24 ) + Spacer(modifier = Modifier.height(16.dp)) DateRoadEmptyView(emptyViewType = EmptyViewType.READ) } else { Text( diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/signin/SignInScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/signin/SignInScreen.kt index 3ecca34..04b0a26 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/signin/SignInScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/signin/SignInScreen.kt @@ -3,10 +3,14 @@ package org.sopt.teamdateroad.presentation.ui.signin import android.content.Context import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -109,28 +113,43 @@ fun SignInScreen( if (signInUiState.isWebViewOpened) { DateRoadWebView(url = PRIVACY_POLICY_URL, onClose = webViewClose) } else { - Column( + Box( modifier = Modifier .fillMaxSize() .background(DateRoadTheme.colors.purple600), - horizontalAlignment = Alignment.CenterHorizontally + contentAlignment = Alignment.Center ) { - Spacer(modifier = Modifier.weight(226f)) - Image(painter = painterResource(id = R.drawable.img_splash_logo), contentDescription = null) - Spacer(modifier = Modifier.weight(167f)) - DateRoadKakaoLoginButton( - modifier = Modifier.padding(horizontal = 30.dp), - onClick = onSignInClicked - ) - Spacer(modifier = Modifier.weight(16f)) - Text( - text = "๊ฐœ์ธ์ •๋ณด์ฒ˜๋ฆฌ๋ฐฉ์นจ", - color = DateRoadTheme.colors.gray200, - style = DateRoadTheme.typography.bodyMed15, - textDecoration = TextDecoration.Underline, - modifier = Modifier.noRippleClickable(onClick = onWebViewClicked) - ) - Spacer(modifier = Modifier.weight(37f)) + Column( + modifier = Modifier + .wrapContentWidth() + .fillMaxHeight(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.weight(270f)) + Image(painter = painterResource(id = R.drawable.img_splash_logo), contentDescription = null) + Spacer(modifier = Modifier.weight(328f)) + } + Column( + modifier = Modifier + .wrapContentWidth() + .fillMaxHeight(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.weight(591f)) + DateRoadKakaoLoginButton( + modifier = Modifier.padding(horizontal = 30.dp), + onClick = onSignInClicked + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = "๊ฐœ์ธ์ •๋ณด์ฒ˜๋ฆฌ๋ฐฉ์นจ", + color = DateRoadTheme.colors.gray200, + style = DateRoadTheme.typography.bodyMed15, + textDecoration = TextDecoration.Underline, + modifier = Modifier.noRippleClickable(onClick = onWebViewClicked) + ) + Spacer(modifier = Modifier.weight(124f)) + } } } } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/timeline/TimelineScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/timeline/TimelineScreen.kt index a66917b..d5de826 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/timeline/TimelineScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/timeline/TimelineScreen.kt @@ -135,7 +135,6 @@ fun TimelineScreen( ) } ) - Spacer(modifier = Modifier.height(52.dp)) Column( modifier = Modifier @@ -143,11 +142,13 @@ fun TimelineScreen( .align(Alignment.CenterHorizontally) ) { if (uiState.timelines.isEmpty()) { + Spacer(modifier = Modifier.height(29.dp)) DateRoadEmptyView( modifier = Modifier.fillMaxWidth(), emptyViewType = EmptyViewType.TIMELINE ) } else { + Spacer(modifier = Modifier.height(52.dp)) HorizontalPager( count = uiState.timelines.size, state = pagerState, @@ -157,12 +158,13 @@ fun TimelineScreen( ) { page -> val date = uiState.timelines[page] val timelineType = TimelineType.getTimelineTypeByIndex(page) + val paddingEnd = if (uiState.timelines.lastIndex == page) 0.dp else 16.dp TimelineCard( timeline = date, timelineType = timelineType, onClick = { navigateToTimelineDetail(timelineType, date.timelineId) }, modifier = Modifier - .padding(end = 16.dp) + .padding(end = paddingEnd) ) } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/util/Constraints.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/util/Constraints.kt index 6278e40..aa4ac31 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/util/Constraints.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/util/Constraints.kt @@ -18,7 +18,7 @@ object CourseDetailAmplitude { object DatePicker { const val DATE_PATTERN = "yyyy.MM.dd" const val YEAR_START = 2000 - const val YEAR_END = 2026 + const val YEAR_END = 2050 const val YEAR_START_INDEX = 25 const val MONTH_START = 1 const val MONTH_END = 12 diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/util/view/NumberCommaTransformation.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/util/view/NumberCommaTransformation.kt new file mode 100644 index 0000000..40a5aff --- /dev/null +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/util/view/NumberCommaTransformation.kt @@ -0,0 +1,32 @@ +package org.sopt.teamdateroad.presentation.util.view + +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.input.OffsetMapping +import androidx.compose.ui.text.input.TransformedText +import androidx.compose.ui.text.input.VisualTransformation +import java.text.NumberFormat +import java.util.Locale + +class NumberCommaTransformation : VisualTransformation { + + private fun Long?.formatWithComma(): String = + NumberFormat.getNumberInstance(Locale.US).format(this ?: 0) + + override fun filter(text: AnnotatedString): TransformedText { + val newText = AnnotatedString( + text = if (text.text.isEmpty()) "" else text.text.toLongOrNull().formatWithComma() + ) + return TransformedText( + text = newText, + offsetMapping = object : OffsetMapping { + override fun originalToTransformed(offset: Int): Int { + return if (offset != newText.length) newText.length else text.length + } + + override fun transformedToOriginal(offset: Int): Int { + return text.length + } + } + ) + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3def010..37c3be3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -43,7 +43,7 @@ ์•„์ง ํฌ์ธํŠธ ์‚ฌ์šฉ ๋‚ด์—ญ์ด ์—†์–ด์š”! ๋‹ค๋ฅธ ์ปคํ”Œ๋“ค์˜ ๋ฐ์ดํŠธ ์ฝ”์Šค๋ฅผ ์—ด๋žŒํ•ด ๋ณด์„ธ์š”! ์•„์ง ๋“ฑ๋ก๋œ ์ฝ”์Šค๊ฐ€ ์—†์–ด์š”! - ์•„์ง ์—ฐ์ธ๊ณผ์˜ ๋ฐ์ดํŠธ ์ผ์ •์„ ๋“ฑ๋กํ•˜์ง€ ์•Š์œผ์…จ๋‚˜์š”? + ์•„์ง ์—ฐ์ธ๊ณผ์˜ ๋ฐ์ดํŠธ ์ผ์ •์„ \n๋“ฑ๋กํ•˜์ง€ ์•Š์œผ์…จ๋‚˜์š”? ์ง€๋‚œ ๋ฐ์ดํŠธ๊ฐ€ ์—†์–ด์š”! ์•„์ง ์—ด๋žŒํ•œ ์ฝ”์Šค๊ฐ€ ์—†์–ด์š”! ์•„์ง ๋“ฑ๋กํ•œ ์ฝ”์Šค๊ฐ€ ์—†์–ด์š”! From 3ec1518ba033b20a5debf5988a2321a846639186 Mon Sep 17 00:00:00 2001 From: Yaechan Park <102402485+dpcks0509@users.noreply.github.com> Date: Wed, 30 Apr 2025 22:46:22 +0900 Subject: [PATCH 45/49] =?UTF-8?q?1=EC=B0=A8=20QA=20=EB=B0=98=EC=98=81-2=20?= =?UTF-8?q?(#45)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: ์ผ์ • ์‹œ๊ฐ„ ํƒœ๊ทธ ํฌ๊ธฐ ํ†ต์ผ * feat: ๋ฐ์ดํŠธ ์ฝ”์Šค ์˜ฌ๋ฆฌ๊ณ  ํฌ์ธํŠธ ๋ฐ›๊ธฐ ๋ฒ„ํŠผ ์ƒ์„ฑ (ํด๋ฆญ ์ด๋ฒคํŠธ ์ œ์™ธ) * feat: ์žฅ์†Œ ๊ฒ€์ƒ‰ ๋ฐ”ํ…€์‹œํŠธ Empty ํ™”๋ฉด bottom padding ์ถ”๊ฐ€ * feat: ์ฝ”์Šค, ์ผ์ • ํƒ€์ดํ‹€ ๊ตฌ๋ถ„๋˜๊ฒŒ ์ˆ˜์ • * feat: ๋ฐ์ดํŠธ ์ผ์ •์—์„œ ๋ฐ”๋กœ ์ฝ”์Šค ๋“ฑ๋ก ๊ธฐ๋Šฅ ๊ตฌํ˜„ --- .../presentation/type/DateChipGroupType.kt | 5 ++++ .../ui/component/card/DateRoadPlaceCard.kt | 3 ++- .../ui/component/tag/DateRoadTextTag.kt | 19 ++++++++++---- .../ui/enroll/EnrollFirstScreen.kt | 6 ++++- .../presentation/ui/enroll/EnrollScreen.kt | 2 +- .../ui/enroll/EnrollSecondScreen.kt | 6 ++++- .../component/PlaceSearchBottomSheet.kt | 9 +++++-- .../ui/navigator/MainNavigator.kt | 9 +++++++ .../ui/navigator/component/MainNavHost.kt | 1 + .../ui/timelinedetail/TimelineDetailScreen.kt | 26 ++++++++++++++++++- .../navigation/TimelineDetailNavigation.kt | 4 ++- .../presentation/util/Constraints.kt | 3 ++- app/src/main/res/values/strings.xml | 6 +++-- 13 files changed, 83 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/type/DateChipGroupType.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/type/DateChipGroupType.kt index 9d0c0dc..4283b60 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/type/DateChipGroupType.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/type/DateChipGroupType.kt @@ -19,5 +19,10 @@ enum class DateChipGroupType( titleRes = R.string.date_chip_group_enroll_course, titleTextStyle = defaultDateRoadTypography.bodySemi15, maxSize = 3 + ), + TIMELINE( + titleRes = R.string.date_chip_group_enroll_timeline, + titleTextStyle = defaultDateRoadTypography.bodySemi15, + maxSize = 3 ) } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/card/DateRoadPlaceCard.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/card/DateRoadPlaceCard.kt index 58456d4..4a43b1c 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/card/DateRoadPlaceCard.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/card/DateRoadPlaceCard.kt @@ -75,6 +75,7 @@ fun DateRoadPlaceCard( Spacer(modifier = Modifier.width(10.dp)) DateRoadTextTag( + modifier = Modifier.width(74.dp), textContent = place.duration, tagContentType = TagType.PLACE_CARD_TIME ) @@ -130,7 +131,7 @@ fun DateRoadPlaceCardPreview() { DateRoadPlaceCard( placeCardType = PlaceCardType.COURSE_NORMAL, sequence = 0, - place = Place(title = "์„ฑ์ˆ˜๋ฏธ์ˆ ๊ด€ ์„ฑ์ˆ˜์ ์„ฑ์ˆ˜๋ฏธ์ˆ ๊ด€ ์„ฑ์ˆ˜์ ์„ฑ์ˆ˜๋ฏธ์ˆ ๊ด€ ์„ฑ์ˆ˜์ ์„ฑ์ˆ˜๋ฏธ์ˆ ๊ด€ ์„ฑ์ˆ˜์ ์„ฑ์ˆ˜๋ฏธ์ˆ ๊ด€ ์„ฑ์ˆ˜์ ", address = "์„œ์šธ ๊ด‘์ง„๊ตฌ ์ž์–‘๋™ 704-1", duration = "2.5์‹œ๊ฐ„") + place = Place(title = "์„ฑ์ˆ˜๋ฏธ์ˆ ๊ด€ ์„ฑ์ˆ˜์ ์„ฑ์ˆ˜๋ฏธ์ˆ ๊ด€ ์„ฑ์ˆ˜์ ์„ฑ์ˆ˜๋ฏธ์ˆ ๊ด€ ์„ฑ์ˆ˜์ ์„ฑ์ˆ˜๋ฏธ์ˆ ๊ด€ ์„ฑ์ˆ˜์ ์„ฑ์ˆ˜๋ฏธ์ˆ ๊ด€ ์„ฑ์ˆ˜์ ", address = "์„œ์šธ ๊ด‘์ง„๊ตฌ ์ž์–‘๋™ 704-1", duration = "4.0์‹œ๊ฐ„") ) Spacer(modifier = Modifier.height(8.dp)) DateRoadPlaceCard( diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/tag/DateRoadTextTag.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/tag/DateRoadTextTag.kt index cdf360d..f504c49 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/tag/DateRoadTextTag.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/tag/DateRoadTextTag.kt @@ -1,9 +1,12 @@ package org.sopt.teamdateroad.presentation.ui.component.tag +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import org.sopt.teamdateroad.presentation.type.TagType import org.sopt.teamdateroad.ui.theme.DATEROADTheme @@ -18,11 +21,17 @@ fun DateRoadTextTag( modifier = modifier, tagType = tagContentType ) { - Text( - text = textContent, - style = tagContentType.textStyle, - color = tagContentType.contentColor - ) + Box( + modifier = modifier, + contentAlignment = Alignment.Center + ) { + Text( + text = textContent, + style = tagContentType.textStyle, + color = tagContentType.contentColor, + textAlign = TextAlign.Center + ) + } } } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollFirstScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollFirstScreen.kt index ff31161..23e1ccb 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollFirstScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollFirstScreen.kt @@ -19,6 +19,7 @@ import org.sopt.teamdateroad.domain.type.SeoulAreaType import org.sopt.teamdateroad.presentation.type.DateChipGroupType import org.sopt.teamdateroad.presentation.type.DateTagType import org.sopt.teamdateroad.presentation.type.DateTagType.Companion.getDateTagTypeByName +import org.sopt.teamdateroad.presentation.type.EnrollType import org.sopt.teamdateroad.presentation.ui.component.chipgroup.DateRoadDateChipGroup import org.sopt.teamdateroad.presentation.ui.component.textfield.DateRoadBasicTextField import org.sopt.teamdateroad.presentation.util.view.LoadState @@ -67,7 +68,10 @@ fun EnrollFirstScreen( ) Spacer(modifier = Modifier.height(20.dp)) DateRoadDateChipGroup( - dateChipGroupType = DateChipGroupType.ENROLL, + dateChipGroupType = when (enrollUiState.enrollType) { + EnrollType.COURSE -> DateChipGroupType.ENROLL + EnrollType.TIMELINE -> DateChipGroupType.TIMELINE + }, selectedDateTags = enrollUiState.enroll.tags.mapNotNull { tag -> tag.getDateTagTypeByName() }, onSelectedDateTagsChanged = onDateChipClicked ) diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt index 7204ae6..a68a9d2 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt @@ -594,7 +594,7 @@ fun EnrollScreenPreview() { loadState = LoadState.Success ), searchKeyword = "", - searchPlaceInfos = listOf(), + searchPlaceInfos = emptyList(), onTopBarBackButtonClick = {}, onTopBarLoadButtonClick = {}, onEnrollButtonClick = {}, diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollSecondScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollSecondScreen.kt index d31b3c5..bb31bc2 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollSecondScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollSecondScreen.kt @@ -36,6 +36,7 @@ import kotlinx.coroutines.launch import org.sopt.teamdateroad.R import org.sopt.teamdateroad.domain.model.Place import org.sopt.teamdateroad.domain.model.PlaceInfo +import org.sopt.teamdateroad.presentation.type.EnrollType import org.sopt.teamdateroad.presentation.type.PlaceCardType import org.sopt.teamdateroad.presentation.ui.component.button.DateRoadTextButton import org.sopt.teamdateroad.presentation.ui.component.card.DateRoadPlaceCard @@ -78,7 +79,10 @@ fun EnrollSecondScreen( Spacer(modifier = Modifier.height(11.dp)) Text( modifier = Modifier.padding(horizontal = 16.dp), - text = stringResource(id = R.string.enroll_place_title), + text = when (enrollUiState.enrollType) { + EnrollType.COURSE -> stringResource(id = R.string.enroll_course_place_title) + EnrollType.TIMELINE -> stringResource(id = R.string.enroll_timeline_place_title) + }, color = DateRoadTheme.colors.black, style = DateRoadTheme.typography.bodyBold17 ) diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/component/PlaceSearchBottomSheet.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/component/PlaceSearchBottomSheet.kt index 3bb8fdc..fdd1d4e 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/component/PlaceSearchBottomSheet.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/component/PlaceSearchBottomSheet.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -31,6 +32,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction @@ -180,10 +182,13 @@ fun PlaceSearchBottomSheet( @Composable private fun EmptyPlaceSearchResult() { + val screenHeight = LocalConfiguration.current.screenHeightDp.dp + val offsetPadding = screenHeight * 0.2f + Column( modifier = Modifier - .height(472.dp) - .fillMaxWidth(), + .fillMaxSize() + .padding(bottom = offsetPadding), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/navigator/MainNavigator.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/navigator/MainNavigator.kt index a58b06a..30e0daa 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/navigator/MainNavigator.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/navigator/MainNavigator.kt @@ -33,6 +33,7 @@ import org.sopt.teamdateroad.presentation.ui.signin.navigation.SignInRoute import org.sopt.teamdateroad.presentation.ui.signin.navigation.navigationSignIn import org.sopt.teamdateroad.presentation.ui.timeline.navigation.navigationTimeline import org.sopt.teamdateroad.presentation.ui.timelinedetail.navigation.navigateToTimelineDetail +import org.sopt.teamdateroad.presentation.util.ViewPath class MainNavigator( val navHostController: NavHostController @@ -78,6 +79,14 @@ class MainNavigator( navHostController.navigationEnroll(enrollType = enrollType, viewPath = viewPath, courseId = courseId) } + fun navigateToTimeLineToEnrollCourse(courseId: Int) { + navHostController.navigationEnroll( + enrollType = EnrollType.COURSE, + viewPath = ViewPath.TIMELINE_TO_ENROLL_COURSE, + courseId = courseId + ) + } + fun navigateToHome(navOptions: NavOptions? = null) { navHostController.navigationHome( navOptions ?: navOptions { diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/navigator/component/MainNavHost.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/navigator/component/MainNavHost.kt index 4b897f4..5fa0426 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/navigator/component/MainNavHost.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/navigator/component/MainNavHost.kt @@ -146,6 +146,7 @@ fun MainNavHost( timelineDetailGraph( popBackStack = navigator::popBackStackIfNotHome, + navigateToEnrollCourse = navigator::navigateToTimeLineToEnrollCourse, viewPath = previousView ) } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/timelinedetail/TimelineDetailScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/timelinedetail/TimelineDetailScreen.kt index 05727c4..1a849d6 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/timelinedetail/TimelineDetailScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/timelinedetail/TimelineDetailScreen.kt @@ -71,6 +71,7 @@ import org.sopt.teamdateroad.ui.theme.DateRoadTheme @Composable fun TimelineDetailRoute( popBackStack: () -> Unit, + navigateToEnrollCourse: (Int) -> Unit, timelineId: Int, timelineType: TimelineType, previousView: String @@ -114,6 +115,7 @@ fun TimelineDetailRoute( timelineType = timelineType, onTopBarItemClick = popBackStack, onButtonClick = { viewModel.setEvent(TimelineDetailContract.TimelineDetailEvent.SetShowDeleteBottomSheet(true)) }, + onEnrollCourseButtonClick = navigateToEnrollCourse, showKakaoClicked = { viewModel.setEvent(TimelineDetailContract.TimelineDetailEvent.SetShowKakaoDialog(true)) AmplitudeUtils.trackEventWithProperties( @@ -149,6 +151,7 @@ fun TimelineDetailScreen( timelineType: TimelineType, onTopBarItemClick: () -> Unit = {}, onButtonClick: () -> Unit = {}, + onEnrollCourseButtonClick: (Int) -> Unit = {}, showKakaoClicked: () -> Unit = {}, setShowKakaoDialog: (Boolean) -> Unit, setShowDeleteBottomSheet: (Boolean) -> Unit, @@ -308,6 +311,27 @@ fun TimelineDetailScreen( ) } } + } else { + Column( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(vertical = 16.dp, horizontal = 70.dp) + .background(DateRoadTheme.colors.purple600, CircleShape) + .noRippleClickable(onClick = { onEnrollCourseButtonClick(uiState.timelineDetail.timelineId) }) + ) { + Box( + modifier = Modifier + .padding(horizontal = 24.dp, vertical = 16.dp) + .clip(CircleShape) + ) { + Text( + text = stringResource(id = R.string.timeline_detail_point), + style = DateRoadTheme.typography.bodyBold15, + color = DateRoadTheme.colors.white, + textAlign = TextAlign.Center + ) + } + } } } } @@ -377,7 +401,7 @@ fun TimelineDetailScreenPreview() { loadState = LoadState.Success, timelineDetail = TimelineDetail( date = "2024-08-17", - dDay = "D-3", + dDay = "", title = "Seoul City Tour", city = "Seoul", startAt = "10:00 AM", diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/timelinedetail/navigation/TimelineDetailNavigation.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/timelinedetail/navigation/TimelineDetailNavigation.kt index c88e8b6..b4b0985 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/timelinedetail/navigation/TimelineDetailNavigation.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/timelinedetail/navigation/TimelineDetailNavigation.kt @@ -15,7 +15,8 @@ fun NavController.navigateToTimelineDetail(timelineType: TimelineType, timelineI fun NavGraphBuilder.timelineDetailGraph( viewPath: String, - popBackStack: () -> Unit + popBackStack: () -> Unit, + navigateToEnrollCourse: (Int) -> Unit ) { composable( route = TimelineDetailRoutes.ROUTE_WITH_ARGUMENT, @@ -29,6 +30,7 @@ fun NavGraphBuilder.timelineDetailGraph( TimelineDetailRoute( popBackStack = popBackStack, + navigateToEnrollCourse = navigateToEnrollCourse, timelineId = timelineId, timelineType = timelineType, previousView = viewPath diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/util/Constraints.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/util/Constraints.kt index aa4ac31..de0b866 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/util/Constraints.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/util/Constraints.kt @@ -153,7 +153,7 @@ object SignIn { } object Time { - const val TIME = " ์‹œ๊ฐ„" + const val TIME = "์‹œ๊ฐ„" } object TimelineAmplitude { @@ -204,6 +204,7 @@ object ViewPath { const val MY_COURSE_READ = "๋‚ด๊ฐ€ ์—ด๋žŒํ•œ ์ฝ”์Šค" const val COURSE_DETAIL = "์ฝ”์Šค ์ƒ์„ธ" const val LOOK = "์ฝ”์Šค ๋‘˜๋Ÿฌ๋ณด๊ธฐ" + const val TIMELINE_TO_ENROLL_COURSE = "์ผ์ •์„ ๋ฐ์ดํŠธ ์ฝ”์Šค๋กœ ๋“ฑ๋ก" const val POINT_HISTORY = "ํฌ์ธํŠธ ๋‚ด์—ญ" } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 37c3be3..be6a2c8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -172,7 +172,7 @@ ํฌ์ธํŠธ ๋‚ด์—ญ ์ฝ”์Šค ๋“ฑ๋กํ•˜๊ธฐ - ์ผ์ • ๋“ฑ๋กํ•˜๊ธฐ + ์ผ์ • ๊ณ„ํšํ•˜๊ธฐ ๋‚ด๊ฐ€ ์—ด๋žŒํ•œ ์ฝ”์Šค ๋‚ด๊ฐ€ ๋“ฑ๋กํ•œ ์ฝ”์Šค ์ฝ”์Šค ๋‘˜๋Ÿฌ๋ณด๊ธฐ @@ -206,6 +206,7 @@ ๋‚˜์˜ ๋ฐ์ดํŠธ ์„ฑํ–ฅ (%1$d/%2$d) ๋ฐ์ดํŠธ์ฝ”์Šค์™€ ์–ด์šธ๋ฆฌ๋Š” ํƒœ๊ทธ๋ฅผ ์„ ํƒํ•ด ์ฃผ์„ธ์š” (%1$d/%2$d) + ์˜ˆ์ •๋œ ๋ฐ์ดํŠธ์™€ ์–ด์šธ๋ฆฌ๋Š” ํƒœ๊ทธ๋ฅผ ์„ ํƒํ•ด ์ฃผ์„ธ์š” (%1$d/%2$d) %1$.1f ์‹œ๊ฐ„ @@ -246,7 +247,8 @@ ๋ฐ์ดํŠธ ์ง€์—ญ์„ ์„ ํƒํ•ด ์ฃผ์„ธ์š” (ํ•„์ˆ˜) ์žฅ์†Œ๋ช…์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š” ์†Œ์š” ์‹œ๊ฐ„ - ์–ด๋–ค ์ฝ”์Šค๋กœ ์ด๋™ํ•˜์…จ๋‚˜์š”? + ์–ด๋–ค ์ฝ”์Šค๋กœ ์ด๋™ํ•˜์…จ๋‚˜์š”? + ์–ด๋–ค ์ฝ”์Šค๋ฅผ ๊ณ„ํšํ•˜์…จ๋‚˜์š”? ์žฅ์†Œ์™€ ์†Œ์š”์‹œ๊ฐ„์„ ์ž…๋ ฅํ•˜์—ฌ ์ฝ”์Šค๋ฅผ ์ถ”๊ฐ€ํ•ด ์ฃผ์„ธ์š” ์ตœ์†Œ 2๊ฐœ์˜ ์žฅ์†Œ๋ฅผ ์ถ”๊ฐ€ํ•ด ์ฃผ์„ธ์š” ์ฝ”์Šค์— ๋Œ€ํ•œ ์„ค๋ช…์„ ์ ์–ด ์ฃผ์„ธ์š” From bfa1932cbe442039d20d98e2ba1e3bf44e12404c Mon Sep 17 00:00:00 2001 From: Yaechan Park <102402485+dpcks0509@users.noreply.github.com> Date: Thu, 8 May 2025 09:41:40 +0900 Subject: [PATCH 46/49] =?UTF-8?q?1=EC=B0=A8=20QA=20=EB=B0=98=EC=98=81-3=20?= =?UTF-8?q?(#48)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: ๋ฐ์ดํŠธ ์ผ์ • ๋ฐ•์Šค ํฌ๊ธฐ ๋‹ค๋ฆ„ ๋ฌธ์ œ ํ•ด๊ฒฐ * fix: ์ฝ”์Šค ๋‘˜๋Ÿฌ๋ณด๊ธฐ ๋นˆ์นธ ๋ฌธ์ œ ํ•ด๊ฒฐ --- .../presentation/ui/look/LookScreen.kt | 3 --- .../ui/look/component/LookCourseCard.kt | 1 + .../ui/timeline/TimelineScreen.kt | 3 +-- .../ui/timeline/component/TimelineCard.kt | 21 +++++++++---------- 4 files changed, 12 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/look/LookScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/look/LookScreen.kt index abcf866..8e6cd6f 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/look/LookScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/look/LookScreen.kt @@ -234,9 +234,6 @@ fun LookScreen( horizontalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(16.dp) ) { - item { - Spacer(modifier = Modifier.height(10.dp)) - } items(lookUiState.courses.size) { index -> LookCourseCard(course = lookUiState.courses[index], onClick = { onCourseCardClicked(lookUiState.courses[index].courseId) }) } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/look/component/LookCourseCard.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/look/component/LookCourseCard.kt index 8e81ef9..ad20ad9 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/look/component/LookCourseCard.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/look/component/LookCourseCard.kt @@ -50,6 +50,7 @@ fun LookCourseCard( .fillMaxWidth() .noRippleClickable(onClick = { onClick(course.courseId) }) ) { + Spacer(modifier = Modifier.height(10.dp)) Box { AsyncImage( model = ImageRequest.Builder(context = context) diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/timeline/TimelineScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/timeline/TimelineScreen.kt index d5de826..f37be6b 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/timeline/TimelineScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/timeline/TimelineScreen.kt @@ -163,8 +163,7 @@ fun TimelineScreen( timeline = date, timelineType = timelineType, onClick = { navigateToTimelineDetail(timelineType, date.timelineId) }, - modifier = Modifier - .padding(end = paddingEnd) + paddingEnd = paddingEnd ) } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/timeline/component/TimelineCard.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/timeline/component/TimelineCard.kt index 6c43fbf..d9f096f 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/timeline/component/TimelineCard.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/timeline/component/TimelineCard.kt @@ -28,6 +28,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import org.sopt.teamdateroad.R import org.sopt.teamdateroad.domain.model.Timeline @@ -42,15 +43,16 @@ import org.sopt.teamdateroad.ui.theme.defaultDateRoadColors @Composable fun TimelineCard( - modifier: Modifier, timeline: Timeline, timelineType: TimelineType, - onClick: (Int) -> Unit = {} + onClick: (Int) -> Unit = {}, + paddingEnd: Dp = 0.dp ) { Box( - modifier = modifier - .clip(RoundedCornerShape(24.dp)) + modifier = Modifier .aspectRatio(291 / 406f) + .padding(end = paddingEnd) + .clip(RoundedCornerShape(24.dp)) .background(timelineType.backgroundColor) .noRippleClickable(onClick = { onClick(timeline.timelineId) }) ) { @@ -58,8 +60,7 @@ fun TimelineCard( painter = painterResource(id = R.drawable.bg_timeline_card), contentDescription = null, tint = timelineType.lineColor, - modifier = Modifier - .fillMaxSize() + modifier = Modifier.fillMaxSize() ) Column( modifier = Modifier @@ -170,7 +171,7 @@ fun TimelineCard( modifier = Modifier .weight(1f) .fillMaxWidth() - .padding(horizontal = 20.dp, vertical = 25.dp) + .padding(horizontal = 20.dp, vertical = 24.dp) ) { Text( text = timeline.city, @@ -185,8 +186,7 @@ fun TimelineCard( color = DateRoadTheme.colors.black, maxLines = 2, overflow = TextOverflow.Ellipsis, - modifier = Modifier - .fillMaxWidth() + modifier = Modifier.fillMaxWidth() ) } } @@ -198,12 +198,11 @@ fun TimelineCard( fun TimelineCardPreview() { Column { TimelineCard( - modifier = Modifier, timelineType = TimelineType.PURPLE, timeline = Timeline( timelineId = 0, dDay = "3", - title = "์„ฑ์ˆ˜๋™ ๋‹น์ผ์น˜๊ธฐ ๋ฐ์ดํŠธ\n๊ฐ€๋ณผ๊นŒ์š”?", + title = "์„ฑ์ˆ˜๋™ ๋‹น์ผ์น˜๊ธฐ ๋ฐ์ดํŠธ ๊ฐ€๋ณผ๊นŒ์š”?", date = "JUNE.23", city = "๊ฑด๋Œ€/์„ฑ์ˆ˜/์™•์‹ญ๋ฆฌ", tags = listOf(DateTagType.SHOPPING, DateTagType.DRIVE, DateTagType.EXHIBITION_POPUP) From f49791466febf5cf036c14da6090031f160b93db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=84=EC=9A=B0?= <85734140+jinuemong@users.noreply.github.com> Date: Thu, 8 May 2025 10:03:17 +0900 Subject: [PATCH 47/49] =?UTF-8?q?1=EC=B0=A8=20QA=20=EB=B0=98=EC=98=81-4=20?= =?UTF-8?q?(#49)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: ์บ˜๋ฆฐ๋” ๊ตฌํ˜„ * feat: ์นดํ†ก ๋ฒ„ํŠผ ์ˆ˜์ • * feat: ํƒœ๊ทธ ์œ„์น˜ ์ˆ˜์ • --- .../DateRoadDatePickerBottomSheet.kt | 155 ++++++++++++++++++ .../button/DateRoadKakaoLoginButton.kt | 12 +- .../ui/component/tag/DateRoadTag.kt | 4 +- .../ui/component/tag/DateRoadTextTag.kt | 19 +-- .../presentation/ui/enroll/EnrollScreen.kt | 13 +- 5 files changed, 173 insertions(+), 30 deletions(-) create mode 100644 app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadDatePickerBottomSheet.kt diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadDatePickerBottomSheet.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadDatePickerBottomSheet.kt new file mode 100644 index 0000000..a488d70 --- /dev/null +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/bottomsheet/DateRoadDatePickerBottomSheet.kt @@ -0,0 +1,155 @@ +package org.sopt.teamdateroad.presentation.ui.component.bottomsheet + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.teamdateroad.presentation.ui.component.bottomsheet.model.Picker +import org.sopt.teamdateroad.presentation.ui.component.numberpicker.DateRoadNumberPicker +import org.sopt.teamdateroad.presentation.ui.component.numberpicker.state.PickerState +import org.sopt.teamdateroad.presentation.util.DatePicker +import org.sopt.teamdateroad.ui.theme.DateRoadTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DateRoadDatePickerBottomSheet( + isBottomSheetOpen: Boolean, + isButtonEnabled: Boolean, + buttonText: String, + onDatePickerBottomSheetButtonClick: (String) -> Unit, + onDismissRequest: () -> Unit = {} +) { + val yearItems = (DatePicker.YEAR_START..DatePicker.YEAR_END).map { it.toString() } + val monthItems = (DatePicker.MONTH_START..DatePicker.MONTH_END).map { it.toString().padStart(2, '0') } + + val yearState = remember { PickerState() } + val monthState = remember { PickerState() } + val dayState = remember { PickerState() } + val dayItemsState = remember { mutableStateOf((1..31).map { it.toString().padStart(2, '0') }) } + + LaunchedEffect(yearState.selectedItem, monthState.selectedItem) { + val year = yearState.selectedItem.toIntOrNull() ?: DatePicker.YEAR_START + val month = monthState.selectedItem.toIntOrNull() ?: DatePicker.MONTH_START + val lastDay = getLastDayOfMonth(year, month) + val newDays = (DatePicker.DAY_START..lastDay).map { it.toString().padStart(2, '0') } + + dayItemsState.value = newDays + if (dayState.selectedItem !in newDays) { + dayState.selectedItem = newDays.first() + } + } + + val yearPicker = Picker( + items = yearItems, + startIndex = yearItems.indexOf(yearState.selectedItem), + pickerState = yearState + ) + val monthPicker = Picker( + items = monthItems, + startIndex = monthItems.indexOf(monthState.selectedItem), + pickerState = monthState + ) + val dayPicker = Picker( + items = dayItemsState.value, + startIndex = dayItemsState.value.indexOf(dayState.selectedItem), + pickerState = dayState + ) + + val pickers = listOf(yearPicker, monthPicker, dayPicker) + + LaunchedEffect(Unit) { + yearState.selectedItem = yearItems[DatePicker.YEAR_START_INDEX] + monthState.selectedItem = monthItems.first() + dayState.selectedItem = dayItemsState.value.first() + } + + DateRoadBottomSheet( + modifier = Modifier.padding(top = 20.dp, bottom = 16.dp, start = 16.dp, end = 16.dp), + isBottomSheetOpen = isBottomSheetOpen, + isButtonEnabled = isButtonEnabled, + buttonText = buttonText, + onButtonClick = { + val selectedDate = pickers.joinToString(separator = DatePicker.SEPARATOR) { + it.pickerState.selectedItem.padStart(2, '0') + } + onDatePickerBottomSheetButtonClick(selectedDate) + // ์ƒํƒœ ์ดˆ๊ธฐํ™” + yearState.selectedItem = yearItems[DatePicker.YEAR_START_INDEX] + monthState.selectedItem = monthItems.first() + dayState.selectedItem = dayItemsState.value.first() + }, + onDismissRequest = { + yearState.selectedItem = yearItems[DatePicker.YEAR_START_INDEX] + monthState.selectedItem = monthItems.first() + dayState.selectedItem = dayItemsState.value.first() + onDismissRequest() + } + ) { + Column { + Row(modifier = Modifier.fillMaxWidth()) { + pickers.forEachIndexed { index, item -> + DateRoadNumberPicker( + modifier = Modifier.weight(1f), + items = item.items, + startIndex = if (index == 0)item.items.indexOf(item.pickerState.selectedItem) else 0, + pickerState = item.pickerState + ) + if (index != pickers.size - 1) { + Spacer(modifier = Modifier.width(17.dp)) + } + } + } + Spacer(modifier = Modifier.height(19.dp)) + } + } +} + +fun getLastDayOfMonth(year: Int, month: Int): Int { + return when (month) { + 1, 3, 5, 7, 8, 10, 12 -> 31 + 4, 6, 9, 11 -> 30 + 2 -> if (isLeapYear(year)) 29 else 28 + else -> 31 + } +} + +fun isLeapYear(year: Int): Boolean { + return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) +} + +@Preview +@Composable +fun DateRoadDatePickerBottomSheetPreview() { + var isBottomSheetOpen by rememberSaveable { mutableStateOf(false) } + + Button(onClick = { isBottomSheetOpen = true }) { + Text( + text = "DateRoadPickerBottomSheet", + color = DateRoadTheme.colors.black, + style = DateRoadTheme.typography.titleExtra24 + ) + } + + DateRoadDatePickerBottomSheet( + isBottomSheetOpen = isBottomSheetOpen, + isButtonEnabled = true, + buttonText = "์ทจ์†Œ", + onDatePickerBottomSheetButtonClick = {} + ) +} diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/button/DateRoadKakaoLoginButton.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/button/DateRoadKakaoLoginButton.kt index 150f2f0..57a31ac 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/button/DateRoadKakaoLoginButton.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/button/DateRoadKakaoLoginButton.kt @@ -1,10 +1,8 @@ package org.sopt.teamdateroad.presentation.ui.component.button import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -39,16 +37,16 @@ fun DateRoadKakaoLoginButton( paddingHorizontal = 14.dp, onClick = onClick ) { - Row( - verticalAlignment = Alignment.CenterVertically + Box( + modifier.fillMaxWidth() ) { Image( painter = painterResource(id = R.drawable.ic_kakao_logo), contentDescription = null, modifier = Modifier .clip(CircleShape) + .align(Alignment.CenterStart) ) - Spacer(modifier = Modifier.size(5.dp)) Text( text = stringResource(id = R.string.kakao_login), fontSize = 15.sp, @@ -57,7 +55,7 @@ fun DateRoadKakaoLoginButton( color = contentColor, textAlign = TextAlign.Center, modifier = Modifier - .align(Alignment.CenterVertically) + .align(Alignment.Center) .fillMaxWidth() ) } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/tag/DateRoadTag.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/tag/DateRoadTag.kt index a132a21..546f6b4 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/tag/DateRoadTag.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/tag/DateRoadTag.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp @@ -20,7 +21,8 @@ fun DateRoadTag( modifier = modifier .clip(RoundedCornerShape(tagType.roundedCornerShape.dp)) .background(color = tagType.backgroundColor) - .padding(horizontal = tagType.paddingHorizontal.dp, vertical = tagType.paddingVertical.dp) + .padding(horizontal = tagType.paddingHorizontal.dp, vertical = tagType.paddingVertical.dp), + contentAlignment = Alignment.Center ) { content() } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/tag/DateRoadTextTag.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/tag/DateRoadTextTag.kt index f504c49..aa15e5c 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/tag/DateRoadTextTag.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/tag/DateRoadTextTag.kt @@ -1,10 +1,8 @@ package org.sopt.teamdateroad.presentation.ui.component.tag -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -21,17 +19,12 @@ fun DateRoadTextTag( modifier = modifier, tagType = tagContentType ) { - Box( - modifier = modifier, - contentAlignment = Alignment.Center - ) { - Text( - text = textContent, - style = tagContentType.textStyle, - color = tagContentType.contentColor, - textAlign = TextAlign.Center - ) - } + Text( + text = textContent, + style = tagContentType.textStyle, + color = tagContentType.contentColor, + textAlign = TextAlign.Center + ) } } diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt index a68a9d2..08294e8 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/enroll/EnrollScreen.kt @@ -44,6 +44,7 @@ import org.sopt.teamdateroad.presentation.type.EnrollType import org.sopt.teamdateroad.presentation.type.MyCourseType import org.sopt.teamdateroad.presentation.type.OneButtonDialogType import org.sopt.teamdateroad.presentation.type.OneButtonDialogWithDescriptionType +import org.sopt.teamdateroad.presentation.ui.component.bottomsheet.DateRoadDatePickerBottomSheet import org.sopt.teamdateroad.presentation.ui.component.bottomsheet.DateRoadPickerBottomSheet import org.sopt.teamdateroad.presentation.ui.component.bottomsheet.DateRoadRegionBottomSheet import org.sopt.teamdateroad.presentation.ui.component.button.DateRoadBasicButton @@ -56,7 +57,6 @@ import org.sopt.teamdateroad.presentation.ui.component.view.DateRoadErrorView import org.sopt.teamdateroad.presentation.ui.component.view.DateRoadLoadingView import org.sopt.teamdateroad.presentation.ui.enroll.component.EnrollPhotos import org.sopt.teamdateroad.presentation.util.DatePicker -import org.sopt.teamdateroad.presentation.util.DatePicker.SEPARATOR import org.sopt.teamdateroad.presentation.util.EnrollAmplitude.CLICK_BRING_COURSE import org.sopt.teamdateroad.presentation.util.EnrollAmplitude.CLICK_COURSE1_BACK import org.sopt.teamdateroad.presentation.util.EnrollAmplitude.CLICK_COURSE2_BACK @@ -501,17 +501,12 @@ fun EnrollScreen( Spacer(modifier = Modifier.height(16.dp)) } - DateRoadPickerBottomSheet( + DateRoadDatePickerBottomSheet( isBottomSheetOpen = enrollUiState.isDatePickerBottomSheetOpen, isButtonEnabled = true, buttonText = stringResource(id = R.string.apply), - onButtonClick = { - onDatePickerBottomSheetButtonClick( - enrollUiState.datePickers.joinToString(separator = SEPARATOR) { it.pickerState.selectedItem.padStart(2, '0') } - ) - }, - onDismissRequest = onDatePickerBottomSheetDismissRequest, - pickers = enrollUiState.datePickers + onDatePickerBottomSheetButtonClick = onDatePickerBottomSheetButtonClick, + onDismissRequest = onDatePickerBottomSheetDismissRequest ) DateRoadPickerBottomSheet( From dd1bc5aeee04c89449cc0013463897116248ad3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=84=EC=9A=B0?= <85734140+jinuemong@users.noreply.github.com> Date: Tue, 13 May 2025 09:34:25 +0900 Subject: [PATCH 48/49] =?UTF-8?q?1=EC=B0=A8=20QA=20=EB=B0=98=EC=98=81-5=20?= =?UTF-8?q?(#50)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: empty view ์ˆ˜์ • * feat: ํƒœ๊ทธ ์‚ฌ์ด์ฆˆ ๋ฌธ์ œ ํ•ด๊ฒฐ * chore : ๊ตฌ๊ธ€ ๊ด‘๊ณ  id ์—…๋ฐ์ดํŠธ --- .github/workflows/android_cd.yml | 2 +- .github/workflows/android_ci.yml | 2 +- .../teamdateroad/presentation/type/TagType.kt | 2 +- .../ui/component/card/DateRoadPlaceCard.kt | 4 +- .../ui/pointhistory/PointHistoryScreen.kt | 45 ++++- ...img_empty_point_history_gained_history.xml | 166 +++++++++--------- .../img_empty_point_history_used_history.xml | 166 +++++++++--------- 7 files changed, 213 insertions(+), 174 deletions(-) diff --git a/.github/workflows/android_cd.yml b/.github/workflows/android_cd.yml index 2cc5065..e03d7f1 100644 --- a/.github/workflows/android_cd.yml +++ b/.github/workflows/android_cd.yml @@ -52,7 +52,7 @@ jobs: KAKAO_NATIVE_APP_KEY: ${{ secrets.KAKAO_NATIVE_APP_KEY }} AMPLITUDE_API_KEY: ${{ secrets.AMPLITUDE_API_KEY }} GOOGLE_ADS_API_ID: ${{ secrets.GOOGLE_ADS_API_ID }} - GOOGLE_ADS_API_ID_MANIFEST: ${{ secrets.GOOGLE_ADS_API_ID }} + GOOGLE_ADS_API_ID_MANIFEST: ${{ secrets.GOOGLE_ADS_API_ID_MANIFEST }} run: | echo "dev.base.url=\"$HFM_BASE_URL\"" >> local.properties echo "kakao.native.app.key.manifest=\"$KAKAO_NATIVE_APP_KEY_MANIFEST\"" >> local.properties diff --git a/.github/workflows/android_ci.yml b/.github/workflows/android_ci.yml index b24c778..bb65213 100644 --- a/.github/workflows/android_ci.yml +++ b/.github/workflows/android_ci.yml @@ -55,7 +55,7 @@ jobs: KAKAO_REST_API_KEY: ${{ secrets.KAKAO_REST_API_KEY }} AMPLITUDE_API_KEY: ${{ secrets.AMPLITUDE_API_KEY }} GOOGLE_ADS_API_ID: ${{ secrets.GOOGLE_ADS_API_ID }} - GOOGLE_ADS_API_ID_MANIFEST: ${{ secrets.GOOGLE_ADS_API_ID }} + GOOGLE_ADS_API_ID_MANIFEST: ${{ secrets.GOOGLE_ADS_API_ID_MANIFEST }} run: | echo "dev.base.url=\"$BASE_URL\"" >> local.properties echo "kakao.base.url=\"$KAKAO_BASE_URL\"" >> local.properties diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/type/TagType.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/type/TagType.kt index 438b1c9..8b55c34 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/type/TagType.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/type/TagType.kt @@ -104,7 +104,7 @@ enum class TagType( PLACE_CARD_TIME( backgroundColor = defaultDateRoadColors.gray200, contentColor = defaultDateRoadColors.black, - paddingHorizontal = 14, + paddingHorizontal = 0, paddingVertical = 5, textStyle = defaultDateRoadTypography.bodyMed13, roundedCornerShape = 10 diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/card/DateRoadPlaceCard.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/card/DateRoadPlaceCard.kt index 4a43b1c..f1fa731 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/card/DateRoadPlaceCard.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/component/card/DateRoadPlaceCard.kt @@ -37,7 +37,7 @@ fun DateRoadPlaceCard( place: Place, onIconClick: (() -> Unit)? = null ) { - val paddingValues = Modifier.padding(start = placeCardType.startPadding, end = 17.dp) + val paddingValues = Modifier.padding(start = placeCardType.startPadding, end = 13.dp) Row( modifier = modifier @@ -75,7 +75,7 @@ fun DateRoadPlaceCard( Spacer(modifier = Modifier.width(10.dp)) DateRoadTextTag( - modifier = Modifier.width(74.dp), + modifier = Modifier.width(61.dp), textContent = place.duration, tagContentType = TagType.PLACE_CARD_TIME ) diff --git a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryScreen.kt b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryScreen.kt index 9fa90ea..b2d7f09 100644 --- a/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryScreen.kt +++ b/app/src/main/java/org/sopt/teamdateroad/presentation/ui/pointhistory/PointHistoryScreen.kt @@ -1,21 +1,30 @@ package org.sopt.teamdateroad.presentation.ui.pointhistory +import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -40,7 +49,6 @@ import org.sopt.teamdateroad.presentation.ui.component.dialog.DateRoadOneButtonD import org.sopt.teamdateroad.presentation.ui.component.tabbar.DateRoadTabBar import org.sopt.teamdateroad.presentation.ui.component.tabbar.DateRoadTabTitle import org.sopt.teamdateroad.presentation.ui.component.topbar.DateRoadBasicTopBar -import org.sopt.teamdateroad.presentation.ui.component.view.DateRoadEmptyView import org.sopt.teamdateroad.presentation.ui.component.view.DateRoadErrorView import org.sopt.teamdateroad.presentation.ui.component.view.DateRoadIdleView import org.sopt.teamdateroad.presentation.ui.component.view.DateRoadLoadingView @@ -204,12 +212,35 @@ fun PointHistoryScreen( PointHistoryTabType.USED_HISTORY -> pointHistoryUiState.pointHistory.used } if (pointHistory.isEmpty()) { - DateRoadEmptyView( - emptyViewType = when (pointHistoryUiState.pointHistoryTabType) { - PointHistoryTabType.USED_HISTORY -> EmptyViewType.POINT_HISTORY_USED_HISTORY - PointHistoryTabType.GAINED_HISTORY -> EmptyViewType.POINT_HISTORY_GAINED_HISTORY - } - ) + val emptyViewType = when (pointHistoryUiState.pointHistoryTabType) { + PointHistoryTabType.USED_HISTORY -> EmptyViewType.POINT_HISTORY_USED_HISTORY + PointHistoryTabType.GAINED_HISTORY -> EmptyViewType.POINT_HISTORY_GAINED_HISTORY + } + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier.fillMaxSize() + ) { + Image( + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth(), + painter = painterResource(id = emptyViewType.imageRes), + contentDescription = null, + contentScale = ContentScale.FillWidth + ) + Spacer(modifier = Modifier.height(57.dp)) + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + text = stringResource(id = emptyViewType.titleRes), + color = DateRoadTheme.colors.gray300, + style = DateRoadTheme.typography.titleBold18, + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(20.dp)) + } } else { LazyColumn { items(pointHistory.size) { index -> diff --git a/app/src/main/res/drawable/img_empty_point_history_gained_history.xml b/app/src/main/res/drawable/img_empty_point_history_gained_history.xml index 44d3f84..70288ff 100644 --- a/app/src/main/res/drawable/img_empty_point_history_gained_history.xml +++ b/app/src/main/res/drawable/img_empty_point_history_gained_history.xml @@ -1,85 +1,89 @@ - - - - - - - - - - - - - - - - - - - - - + android:viewportHeight="212"> + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/img_empty_point_history_used_history.xml b/app/src/main/res/drawable/img_empty_point_history_used_history.xml index 935a2f4..6f91cc6 100644 --- a/app/src/main/res/drawable/img_empty_point_history_used_history.xml +++ b/app/src/main/res/drawable/img_empty_point_history_used_history.xml @@ -1,85 +1,89 @@ - - - - - - - - - - - - - - - - - - - - - + android:viewportHeight="212"> + + + + + + + + + + + + + + + + + + + + + + + + From 161f65a26823f445dde37d5663b1b4f03fbe6daa Mon Sep 17 00:00:00 2001 From: YaeChan Park <102402485+dpcks0509@users.noreply.github.com> Date: Tue, 13 May 2025 15:33:36 +0900 Subject: [PATCH 49/49] [release] 1.0.2 version (#52) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 1.0.1 -> 1.0.2 version up * feat: ci/cd v4๋กœ ๋‹ค์šด๊ทธ๋ ˆ์ด๋“œ --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fdb3cce..cc4dbfc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,8 +2,8 @@ compileSdk = "34" minSdk = "28" targetSdk = "34" -versionCode = "3" -versionName = "1.0.1" +versionCode = "4" +versionName = "1.0.2" jvmTarget = "1.8" kotlinCompilerExtensionVersion = "1.5.13"