From dd648994ce53631ab5cb847170b57557ec477cb6 Mon Sep 17 00:00:00 2001 From: easyhooon Date: Sat, 23 Aug 2025 03:26:24 +0900 Subject: [PATCH 01/42] =?UTF-8?q?[BOOK-259]=20chore:=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=EC=95=A1=EC=85=98=20=ED=95=A8=EC=88=98=20~ed=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../booket/feature/settings/osslicenses/OssLicensesPresenter.kt | 2 +- .../booket/feature/settings/osslicenses/OssLicensesUi.kt | 2 +- .../booket/feature/settings/osslicenses/OssLicensesUiState.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/osslicenses/OssLicensesPresenter.kt b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/osslicenses/OssLicensesPresenter.kt index 106d9f2f..e9f23d27 100644 --- a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/osslicenses/OssLicensesPresenter.kt +++ b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/osslicenses/OssLicensesPresenter.kt @@ -17,7 +17,7 @@ class OssLicensesPresenter @AssistedInject constructor( override fun present(): OssLicensesUiState { fun handleEvent(event: OssLicensesUiEvent) { when (event) { - is OssLicensesUiEvent.OnBackClicked -> { + is OssLicensesUiEvent.OnBackClick -> { navigator.pop() } } diff --git a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/osslicenses/OssLicensesUi.kt b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/osslicenses/OssLicensesUi.kt index 3c31ff3e..dc804a52 100644 --- a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/osslicenses/OssLicensesUi.kt +++ b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/osslicenses/OssLicensesUi.kt @@ -71,7 +71,7 @@ internal fun OssLicenses( ReedBackTopAppBar( title = stringResource(R.string.oss_licenses_title), onBackClick = { - state.eventSink(OssLicensesUiEvent.OnBackClicked) + state.eventSink(OssLicensesUiEvent.OnBackClick) }, ) LazyColumn { diff --git a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/osslicenses/OssLicensesUiState.kt b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/osslicenses/OssLicensesUiState.kt index 5f829072..30a82745 100644 --- a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/osslicenses/OssLicensesUiState.kt +++ b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/osslicenses/OssLicensesUiState.kt @@ -8,5 +8,5 @@ data class OssLicensesUiState( ) : CircuitUiState sealed interface OssLicensesUiEvent : CircuitUiEvent { - data object OnBackClicked : OssLicensesUiEvent + data object OnBackClick : OssLicensesUiEvent } From aa59266c16ff5e38b17c643d4eadd23365807b2d Mon Sep 17 00:00:00 2001 From: easyhooon Date: Sat, 23 Aug 2025 03:29:01 +0900 Subject: [PATCH 02/42] =?UTF-8?q?[BOOK-259]=20refactor:=20=EB=82=B4?= =?UTF-8?q?=EB=B6=80=20=EB=B8=94=EB=9F=AD=EC=97=90=20=EC=BD=94=EB=A3=A8?= =?UTF-8?q?=ED=8B=B4=20=EC=8A=A4=EC=BD=94=ED=94=84=EB=A5=BC=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20LaunchedEffe?= =?UTF-8?q?ct=20->=20RememberedEffect=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../booket/feature/detail/book/BookDetailPresenter.kt | 4 ++-- .../booket/feature/detail/card/RecordCardSideEffect.kt | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt index 4d00094f..8468fb3a 100644 --- a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt +++ b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt @@ -1,7 +1,6 @@ package com.ninecraft.booket.feature.detail.book import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -25,6 +24,7 @@ import com.ninecraft.booket.feature.screens.RecordScreen import com.ninecraft.booket.feature.screens.arguments.RecordEditArgs import com.ninecraft.booket.feature.screens.extensions.delayedGoTo import com.orhanobut.logger.Logger +import com.skydoves.compose.effects.RememberedEffect import com.slack.circuit.codegen.annotations.CircuitInject import com.slack.circuit.retained.rememberRetained import com.slack.circuit.runtime.Navigator @@ -241,7 +241,7 @@ class BookDetailPresenter @AssistedInject constructor( } } - LaunchedEffect(Unit) { + RememberedEffect(Unit) { initialLoad() } diff --git a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/card/RecordCardSideEffect.kt b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/card/RecordCardSideEffect.kt index 2a98cee7..8fe43db6 100644 --- a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/card/RecordCardSideEffect.kt +++ b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/card/RecordCardSideEffect.kt @@ -2,7 +2,6 @@ package com.ninecraft.booket.feature.detail.card import android.widget.Toast import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.graphics.layer.GraphicsLayer import androidx.compose.ui.platform.LocalContext import com.ninecraft.booket.core.common.extensions.externalShareForBitmap @@ -41,13 +40,13 @@ internal fun HandleRecordCardSideEffects( } } - LaunchedEffect(state.isCapturing) { + RememberedEffect(state.isCapturing) { if (state.isCapturing) { eventSink(RecordCardUiEvent.SaveRecordCard(recordCardGraphicsLayer.toImageBitmap())) } } - LaunchedEffect(state.isSharing) { + RememberedEffect(state.isSharing) { if (state.isSharing) { eventSink(RecordCardUiEvent.ShareRecordCard(recordCardGraphicsLayer.toImageBitmap())) } From 958a988a04dc62aa14a460081b2b159036c89652 Mon Sep 17 00:00:00 2001 From: JI HUN LEE <51016231+easyhooon@users.noreply.github.com> Date: Sat, 23 Aug 2025 04:34:56 +0900 Subject: [PATCH 03/42] Create README.md --- README.md | 132 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..936e27fb --- /dev/null +++ b/README.md @@ -0,0 +1,132 @@ +# Reed-Android +[![Kotlin](https://img.shields.io/badge/Kotlin-2.2.0-blue.svg)](https://kotlinlang.org) +[![Gradle](https://img.shields.io/badge/gradle-8.11.1-green.svg)](https://gradle.org/) +[![Android Studio](https://img.shields.io/badge/Android%20Studio-2025.1.2%20%28Narwhal%29-green)](https://developer.android.com/studio) +[![minSdkVersion](https://img.shields.io/badge/minSdkVersion-28-red)](https://developer.android.com/distribute/best-practices/develop/target-sdk) +[![targetSdkVersion](https://img.shields.io/badge/targetSdkVersion-35-orange)](https://developer.android.com/distribute/best-practices/develop/target-sdk) +![CodeRabbit Pull Request Reviews](https://img.shields.io/coderabbit/prs/github/YAPP-Github/Reed-Android?utm_source=oss&utm_medium=github&utm_campaign=YAPP-Github%2FReed-Android&labelColor=171717&color=FF570A&link=https%3A%2F%2Fcoderabbit.ai&label=CodeRabbit+Reviews) +
+ +Reed(리드) - 문장과 감정을 함께 담는 독서 기록 [PlayStore](https://play.google.com/store/apps/details?id=com.ninecraft.booket&hl=ko) +reed_graphic + +

+ + + +

+

+ + +

+ +## Features + +## TroubleShooting +- [[Compose] M3 ModalBottomSheet 드래그(터치 이벤트) 막는 법](https://velog.io/@mraz3068/Compose-M3-ModalBottomSheet-Drag-Disabled) +- [Circuit 찍먹해보기(부제: Circuit 희망편)](https://speakerdeck.com/easyhooon/circuit-jjigmeoghaebogi-buje-circuit-hyimangpyeon) +- [Circuit 찍먹해보기(부제: Circuit 절망편)](https://speakerdeck.com/easyhooon/circuit-jjigmeoghaebogi-buje-circuit-jeolmangpyeon) +- [Jetpack Compose에서 CameraX + MLKit으로 OCR을 구현해보자](https://velog.io/@syoon513/Jetpack-Compose%EC%97%90%EC%84%9C-CameraX-MLKit%EC%9C%BC%EB%A1%9C-OCR%EC%9D%84-%EA%B5%AC%ED%98%84%ED%95%B4%EB%B3%B4%EC%9E%90) +- [[Android] 일회성 이벤트를 StateFlow, Compose의 State로 처리할 때 주의해야할 점](https://velog.io/@mraz3068/Handle-One-Time-Event-As-State) +- [Circuit Navigation 사용 시 feature 모듈간의 참조는 어떻게 해결했을까?](https://velog.io/@syoon513/Circuit-Navigation-%EC%82%AC%EC%9A%A9-%EC%8B%9C-feature-%EB%AA%A8%EB%93%88%EA%B0%84-%EC%88%9C%ED%99%98-%EC%B0%B8%EC%A1%B0%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%95%B4%EA%B2%B0%ED%96%88%EC%9D%84%EA%B9%8C) +- [Coroutine 에러 처리 패턴: 여러 API 호출을 한 번에 성공/실패 판정하기](https://velog.io/@syoon513/Coroutine-%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC) +- [[Circuit] ImpressionEffect](https://velog.io/@mraz3068/Circuit-ImpressionEffect) + +## Development + +### Required + +- IDE : Android Studio 최신 버전 +- JDK : Java 17을 실행할 수 있는 JDK + - (권장) Android Studio 설치 시 Embeded 된 JDK (Open JDK) + - Java 17을 사용하는 JDK (Open JDK, AdoptOpenJDK, GraalVM) +- Kotlin Language : 2.2.0 + +### Language + +- Kotlin + +### Libraries + +- AndroidX + - Activity Compose + - Core + - DataStore + - StartUp + - Splash + - Camera + +- Kotlin Libraries (Coroutine, Serialization, Immutable Collection) +- Compose + - Material3 + +- [Circuit](https://github.com/slackhq/circuit) +- Dagger Hilt +- Retrofit, OkHttp +- Coil-Compose, [Landscapist](https://github.com/skydoves/landscapist) +- Google-ML Kit +- Lottie-Compose +- Firebase(Analytics, Crashlytics, Remote Config) +- Kakao-Auth +- [Logger](https://github.com/orhanobut/logger) +- [Compose-Stable-Marker](https://github.com/skydoves/compose-stable-marker) +- [Landscapist](https://github.com/skydoves/landscapist), Coil-Compose +- [ComposeExtensions](https://github.com/taehwandev/ComposeExtensions) +- [compose-effects](https://github.com/skydoves/compose-effects) +- [compose-shadow](https://github.com/adamglin0/compose-shadow) + +#### Test & Code analysis + +- Ktlint +- Detekt + +#### Gradle Dependency + +- Gradle Version Catalog + +## Architecture +- Android App Architecture +- MVI + +## Developers + +|Android|Android| +|:---:|:---:| +|[이지훈](https://github.com/easyhooon)|[이서윤](https://github.com/seoyoon513)| +||| + +## Module +image + +## Package Structure +``` +├── app +│   └── application +├── build-logic +├── core +│   ├── common +│   ├── data +│   ├── datastore +│   ├── designsystem +│   ├── model +│   ├── network +│   ├── ocr +│   └── ui +├── feature +│   ├── detail +│   ├── edit +│   ├── home +│   ├── library +│   ├── login +│   ├── main +│   ├── onboarding +│   ├── record +│   ├── screens +│   ├── settings +│   ├── splash +│   └── webview +├── gradle +    └── libs.versions.toml + +``` +
From 17b9b5f5ecdb63880277abb748a91791b65f222a Mon Sep 17 00:00:00 2001 From: easyhooon Date: Sat, 23 Aug 2025 04:35:25 +0900 Subject: [PATCH 04/42] =?UTF-8?q?[BOOK-259]=20chore:=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=EC=9D=98=EC=A1=B4=EC=84=B1=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradle/libs.versions.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 484f649a..e27f7bc6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,6 @@ android-gradle-plugin = "8.9.3" ## AndroidX androidx-core = "1.16.0" -androidx-lifecycle = "2.9.2" androidx-activity-compose = "1.10.1" androidx-startup = "1.2.0" androidx-splash = "1.0.1" @@ -89,7 +88,6 @@ kotlin-gradle-plugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-p compose-compiler-gradle-plugin = { group = "org.jetbrains.kotlin", name = "compose-compiler-gradle-plugin", version.ref = "kotlin" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core" } -androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "androidx-lifecycle" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidx-activity-compose" } androidx-splash = { group = "androidx.core", name = "core-splashscreen", version.ref = "androidx-splash" } androidx-startup = { group = "androidx.startup", name = "startup-runtime", version.ref = "androidx-startup" } From bc7a4bb038f1966067f3c88cce0019d508c371c6 Mon Sep 17 00:00:00 2001 From: easyhooon Date: Sat, 23 Aug 2025 05:00:20 +0900 Subject: [PATCH 05/42] =?UTF-8?q?[BOOK-259]=20chore:=20LaunchedEffect?= =?UTF-8?q?=EB=A1=9C=20=EB=A1=AD=EB=B0=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit toImageBitmap() 은 suspend 함수임; --- .../booket/feature/detail/card/RecordCardSideEffect.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/card/RecordCardSideEffect.kt b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/card/RecordCardSideEffect.kt index 8fe43db6..2a98cee7 100644 --- a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/card/RecordCardSideEffect.kt +++ b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/card/RecordCardSideEffect.kt @@ -2,6 +2,7 @@ package com.ninecraft.booket.feature.detail.card import android.widget.Toast import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.graphics.layer.GraphicsLayer import androidx.compose.ui.platform.LocalContext import com.ninecraft.booket.core.common.extensions.externalShareForBitmap @@ -40,13 +41,13 @@ internal fun HandleRecordCardSideEffects( } } - RememberedEffect(state.isCapturing) { + LaunchedEffect(state.isCapturing) { if (state.isCapturing) { eventSink(RecordCardUiEvent.SaveRecordCard(recordCardGraphicsLayer.toImageBitmap())) } } - RememberedEffect(state.isSharing) { + LaunchedEffect(state.isSharing) { if (state.isSharing) { eventSink(RecordCardUiEvent.ShareRecordCard(recordCardGraphicsLayer.toImageBitmap())) } From d609cb336889a9d1562250931e5acb1658ba8656 Mon Sep 17 00:00:00 2001 From: JI HUN LEE <51016231+easyhooon@users.noreply.github.com> Date: Tue, 26 Aug 2025 19:37:25 +0900 Subject: [PATCH 06/42] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 936e27fb..8a202dc4 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Reed(리드) - 문장과 감정을 함께 담는 독서 기록 [PlayStore](https - [Circuit Navigation 사용 시 feature 모듈간의 참조는 어떻게 해결했을까?](https://velog.io/@syoon513/Circuit-Navigation-%EC%82%AC%EC%9A%A9-%EC%8B%9C-feature-%EB%AA%A8%EB%93%88%EA%B0%84-%EC%88%9C%ED%99%98-%EC%B0%B8%EC%A1%B0%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%95%B4%EA%B2%B0%ED%96%88%EC%9D%84%EA%B9%8C) - [Coroutine 에러 처리 패턴: 여러 API 호출을 한 번에 성공/실패 판정하기](https://velog.io/@syoon513/Coroutine-%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC) - [[Circuit] ImpressionEffect](https://velog.io/@mraz3068/Circuit-ImpressionEffect) +- [Coroutine CancellationException 따로 처리해야하는 케이스](https://velog.io/@mraz3068/Coroutine-CancellationException-UseCase) ## Development From 0b1c98b45abe2efc57f4c7be31edf3cc65856fd8 Mon Sep 17 00:00:00 2001 From: easyhooon Date: Wed, 27 Aug 2025 15:43:26 +0900 Subject: [PATCH 07/42] =?UTF-8?q?[BOOK-298]=20feat:=20=EB=B9=84=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=20=EB=8F=84=EC=84=9C=20=EA=B2=80=EC=83=89=20API=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/data/api/repository/UserRepository.kt | 2 ++ .../impl/repository/DefaultBookRepository.kt | 17 +++++++++++++---- .../booket/core/network/TokenInterceptor.kt | 1 + .../booket/core/network/service/ReedService.kt | 13 +++++++++++++ 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/UserRepository.kt b/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/UserRepository.kt index 049c19b3..0516bc6a 100644 --- a/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/UserRepository.kt +++ b/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/UserRepository.kt @@ -13,4 +13,6 @@ interface UserRepository { val onboardingState: Flow suspend fun setOnboardingCompleted(isCompleted: Boolean) + + val isLoggedIn: Flow } diff --git a/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultBookRepository.kt b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultBookRepository.kt index cf64e2f2..a43b41ff 100644 --- a/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultBookRepository.kt +++ b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultBookRepository.kt @@ -5,6 +5,7 @@ import com.ninecraft.booket.core.data.api.repository.BookRepository import com.ninecraft.booket.core.data.impl.mapper.toModel import com.ninecraft.booket.core.datastore.api.datasource.BookRecentSearchDataSource import com.ninecraft.booket.core.datastore.api.datasource.LibraryRecentSearchDataSource +import com.ninecraft.booket.core.datastore.api.datasource.TokenDataSource import com.ninecraft.booket.core.network.request.BookUpsertRequest import com.ninecraft.booket.core.network.service.ReedService import javax.inject.Inject @@ -13,6 +14,7 @@ internal class DefaultBookRepository @Inject constructor( private val service: ReedService, private val bookRecentSearchDataSource: BookRecentSearchDataSource, private val libraryRecentSearchDataSource: LibraryRecentSearchDataSource, + private val tokenDataSource: TokenDataSource, ) : BookRepository { override val bookRecentSearches = bookRecentSearchDataSource.recentSearches override val libraryRecentSearches = libraryRecentSearchDataSource.recentSearches @@ -21,10 +23,17 @@ internal class DefaultBookRepository @Inject constructor( query: String, start: Int, ) = runSuspendCatching { - val result = service.searchBook( - query = query, - start = start, - ).toModel() + val result = if (tokenDataSource.getAccessToken().isEmpty()) { + service.searchBookAsGuest( + query = query, + start = start, + ).toModel() + } else { + service.searchBook( + query = query, + start = start, + ).toModel() + } bookRecentSearchDataSource.addRecentSearch(query) result diff --git a/core/network/src/main/kotlin/com/ninecraft/booket/core/network/TokenInterceptor.kt b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/TokenInterceptor.kt index a3544d49..a49e6fe8 100644 --- a/core/network/src/main/kotlin/com/ninecraft/booket/core/network/TokenInterceptor.kt +++ b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/TokenInterceptor.kt @@ -13,6 +13,7 @@ internal class TokenInterceptor @Inject constructor( private val noAuthEndpoints = setOf( "api/v1/auth/signin", "api/v1/auth/refresh", + "api/v1/books/guest/search", ) override fun intercept(chain: Interceptor.Chain): Response { diff --git a/core/network/src/main/kotlin/com/ninecraft/booket/core/network/service/ReedService.kt b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/service/ReedService.kt index f2b0b280..84c3a888 100644 --- a/core/network/src/main/kotlin/com/ninecraft/booket/core/network/service/ReedService.kt +++ b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/service/ReedService.kt @@ -50,6 +50,19 @@ interface ReedService { @GET("api/v1/users/me") suspend fun getUserProfile(): UserProfileResponse + // Book endpoints (no auth required) + @GET("api/v1/books/guest/search") + suspend fun searchBookAsGuest( + @Query("query") query: String, + @Query("queryType") queryType: String = "Title", + @Query("searchTarget") searchTarget: String = "Book", + @Query("maxResults") maxResults: Int = 20, + @Query("start") start: Int = 1, + @Query("sort") sort: String = "Accuracy", + @Query("cover") cover: String? = "Big", + @Query("categoryId") categoryId: Int = 0, + ): BookSearchResponse + // Book endpoints (auth required) @GET("api/v1/books/search") suspend fun searchBook( From 04fea0fc02b408c6930ab868e30d7d74448b9044 Mon Sep 17 00:00:00 2001 From: easyhooon Date: Wed, 27 Aug 2025 17:00:17 +0900 Subject: [PATCH 08/42] =?UTF-8?q?[BOOK-298]=20feat:=20Guest=20Login=20?= =?UTF-8?q?=ED=99=88=20=ED=99=94=EB=A9=B4=20=EC=A7=84=EC=9E=85=20=EC=8B=9C?= =?UTF-8?q?=20=ED=99=94=EB=A9=B4=20=EB=B6=84=EA=B8=B0=20=EC=B2=98=EB=A6=AC?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/api/repository/AuthRepository.kt | 5 ++ .../data/api/repository/UserRepository.kt | 2 - .../impl/repository/DefaultAuthRepository.kt | 16 +++-- .../component/button/ReedTextButton.kt | 22 +++++++ .../ninecraft/booket/core/model/UserState.kt | 6 ++ .../booket/feature/home/HomePresenter.kt | 11 +++- .../ninecraft/booket/feature/home/HomeUi.kt | 33 ++++++++-- .../booket/feature/home/HomeUiState.kt | 1 + .../booket/feature/login/LoginPresenter.kt | 4 ++ .../ninecraft/booket/feature/login/LoginUi.kt | 64 ++++++++++++------- .../booket/feature/login/LoginUiState.kt | 1 + feature/login/src/main/res/values/strings.xml | 1 + 12 files changed, 131 insertions(+), 35 deletions(-) create mode 100644 core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ReedTextButton.kt create mode 100644 core/model/src/main/kotlin/com/ninecraft/booket/core/model/UserState.kt diff --git a/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/AuthRepository.kt b/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/AuthRepository.kt index 35bfbb11..c354e076 100644 --- a/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/AuthRepository.kt +++ b/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/AuthRepository.kt @@ -1,6 +1,7 @@ package com.ninecraft.booket.core.data.api.repository import com.ninecraft.booket.core.model.AutoLoginState +import com.ninecraft.booket.core.model.UserState import kotlinx.coroutines.flow.Flow interface AuthRepository { @@ -11,4 +12,8 @@ interface AuthRepository { suspend fun withdraw(): Result val autoLoginState: Flow + + val userState: Flow + + suspend fun getCurrentUserState(): UserState } diff --git a/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/UserRepository.kt b/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/UserRepository.kt index 0516bc6a..049c19b3 100644 --- a/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/UserRepository.kt +++ b/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/UserRepository.kt @@ -13,6 +13,4 @@ interface UserRepository { val onboardingState: Flow suspend fun setOnboardingCompleted(isCompleted: Boolean) - - val isLoggedIn: Flow } diff --git a/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultAuthRepository.kt b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultAuthRepository.kt index c165bba7..1f10c20f 100644 --- a/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultAuthRepository.kt +++ b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultAuthRepository.kt @@ -4,6 +4,7 @@ import com.ninecraft.booket.core.common.utils.runSuspendCatching import com.ninecraft.booket.core.data.api.repository.AuthRepository import com.ninecraft.booket.core.datastore.api.datasource.TokenDataSource import com.ninecraft.booket.core.model.AutoLoginState +import com.ninecraft.booket.core.model.UserState import com.ninecraft.booket.core.network.request.LoginRequest import com.ninecraft.booket.core.network.service.ReedService import kotlinx.coroutines.flow.map @@ -48,9 +49,16 @@ internal class DefaultAuthRepository @Inject constructor( override val autoLoginState = tokenDataSource.accessToken .map { accessToken -> - when { - accessToken.isBlank() -> AutoLoginState.NOT_LOGGED_IN - else -> AutoLoginState.LOGGED_IN - } + if (accessToken.isBlank()) AutoLoginState.NOT_LOGGED_IN else AutoLoginState.LOGGED_IN } + + override val userState = tokenDataSource.accessToken + .map { accessToken -> + if (accessToken.isBlank()) UserState.Guest else UserState.LoggedIn + } + + override suspend fun getCurrentUserState(): UserState { + val accessToken = tokenDataSource.getAccessToken() + return if (accessToken.isBlank()) UserState.Guest else UserState.LoggedIn + } } diff --git a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ReedTextButton.kt b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ReedTextButton.kt new file mode 100644 index 00000000..952d70bf --- /dev/null +++ b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ReedTextButton.kt @@ -0,0 +1,22 @@ +package com.ninecraft.booket.core.designsystem.component.button + +import androidx.compose.material3.Button +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +fun ReedTextButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + colorStyle: ReedButtonColorStyle = ReedButtonColorStyle.PRIMARY, + content: @Composable () -> Unit, +) { + Button( + onClick = onClick, + modifier = modifier, + enabled = enabled, + ) { + content() + } +} diff --git a/core/model/src/main/kotlin/com/ninecraft/booket/core/model/UserState.kt b/core/model/src/main/kotlin/com/ninecraft/booket/core/model/UserState.kt new file mode 100644 index 00000000..43ec0824 --- /dev/null +++ b/core/model/src/main/kotlin/com/ninecraft/booket/core/model/UserState.kt @@ -0,0 +1,6 @@ +package com.ninecraft.booket.core.model + +sealed interface UserState { + data object Guest : UserState + data object LoggedIn : UserState +} diff --git a/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomePresenter.kt b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomePresenter.kt index dbfc9623..ebf3491e 100644 --- a/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomePresenter.kt +++ b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomePresenter.kt @@ -6,8 +6,10 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import com.ninecraft.booket.core.common.analytics.AnalyticsHelper +import com.ninecraft.booket.core.data.api.repository.AuthRepository import com.ninecraft.booket.core.data.api.repository.BookRepository import com.ninecraft.booket.core.model.RecentBookModel +import com.ninecraft.booket.core.model.UserState import com.ninecraft.booket.feature.screens.BookDetailScreen import com.ninecraft.booket.feature.screens.HomeScreen import com.ninecraft.booket.feature.screens.RecordScreen @@ -15,6 +17,7 @@ import com.ninecraft.booket.feature.screens.SearchScreen import com.ninecraft.booket.feature.screens.SettingsScreen import com.skydoves.compose.effects.RememberedEffect import com.slack.circuit.codegen.annotations.CircuitInject +import com.slack.circuit.retained.collectAsRetainedState import com.slack.circuit.retained.rememberRetained import com.slack.circuit.runtime.Navigator import com.slack.circuit.runtime.presenter.Presenter @@ -29,14 +32,15 @@ import kotlinx.coroutines.launch class HomePresenter @AssistedInject constructor( @Assisted private val navigator: Navigator, - private val repository: BookRepository, + private val bookRepository: BookRepository, + private val authRepository: AuthRepository, private val analyticsHelper: AnalyticsHelper, ) : Presenter { @Composable override fun present(): HomeUiState { val scope = rememberCoroutineScope() - + val userState by authRepository.userState.collectAsRetainedState(initial = UserState.Guest) var uiState by rememberRetained { mutableStateOf(UiState.Idle) } var recentBooks by rememberRetained { mutableStateOf(persistentListOf()) } @@ -46,7 +50,7 @@ class HomePresenter @AssistedInject constructor( uiState = UiState.Loading } - repository.getHome() + bookRepository.getHome() .onSuccess { result -> uiState = UiState.Success recentBooks = result.recentBooks.toPersistentList() @@ -99,6 +103,7 @@ class HomePresenter @AssistedInject constructor( return HomeUiState( uiState = uiState, recentBooks = recentBooks, + isGuestMode = userState is UserState.Guest, eventSink = ::handleEvent, ) } diff --git a/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUi.kt b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUi.kt index 82221807..70b053e3 100644 --- a/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUi.kt +++ b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUi.kt @@ -165,14 +165,39 @@ internal fun HomeContent( } Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing7)) } + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing6)) } } is UiState.Error -> { - ReedErrorUi( - exception = state.uiState.exception, - onRetryClick = { state.eventSink(HomeUiEvent.OnRetryClick) }, - ) + if (state.isGuestMode) { + Column( + modifier = modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), + ) { + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing6)) + Text( + text = stringResource(R.string.home_content_label_reading_now), + modifier = Modifier.padding(start = ReedTheme.spacing.spacing5), + color = ReedTheme.colors.contentSecondary, + style = ReedTheme.typography.headline2Medium, + ) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing3)) + EmptyBookCard( + onBookRegisterClick = { + state.eventSink(HomeUiEvent.OnBookRegisterClick) + }, + modifier = Modifier.padding(horizontal = ReedTheme.spacing.spacing5), + ) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing6)) + } + } else { + ReedErrorUi( + exception = state.uiState.exception, + onRetryClick = { state.eventSink(HomeUiEvent.OnRetryClick) }, + ) + } } } } diff --git a/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUiState.kt b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUiState.kt index 9b3115ee..c90d001a 100644 --- a/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUiState.kt +++ b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUiState.kt @@ -20,6 +20,7 @@ sealed interface UiState { data class HomeUiState( val uiState: UiState = UiState.Idle, val recentBooks: ImmutableList = persistentListOf(), + val isGuestMode: Boolean = false, val sideEffect: HomeSideEffect? = null, val eventSink: (HomeUiEvent) -> Unit, ) : CircuitUiState diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt index 1fbc323f..b745a970 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt @@ -90,6 +90,10 @@ class LoginPresenter @AssistedInject constructor( } } } + + is LoginUiEvent.OnGuestLoginButtonClick -> { + navigator.resetRoot(HomeScreen) + } } } diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt index c56d466f..7ae0fb53 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.unit.dp import com.ninecraft.booket.core.designsystem.DevicePreview import com.ninecraft.booket.core.designsystem.component.button.ReedButton import com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle +import com.ninecraft.booket.core.designsystem.component.button.ReedTextButton import com.ninecraft.booket.core.designsystem.component.button.largeButtonStyle import com.ninecraft.booket.core.designsystem.theme.ReedTheme import com.ninecraft.booket.core.designsystem.theme.White @@ -52,7 +53,7 @@ internal fun LoginUi( modifier = modifier .fillMaxSize() .background(White) - .systemBarsPadding(), + .padding(innerPadding), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { @@ -77,28 +78,47 @@ internal fun LoginUi( style = ReedTheme.typography.headline2SemiBold, ) } - ReedButton( - onClick = { - state.eventSink(LoginUiEvent.OnKakaoLoginButtonClick) - }, - sizeStyle = largeButtonStyle, - colorStyle = ReedButtonColorStyle.KAKAO, - modifier = Modifier - .fillMaxWidth() - .padding( - start = ReedTheme.spacing.spacing5, - end = ReedTheme.spacing.spacing5, - bottom = ReedTheme.spacing.spacing8, - ), - text = stringResource(id = R.string.kakao_login), - leadingIcon = { - Icon( - imageVector = ImageVector.vectorResource(id = R.drawable.ic_kakao), - contentDescription = "Kakao Icon", - tint = Color.Unspecified, + Column { + ReedButton( + onClick = { + state.eventSink(LoginUiEvent.OnKakaoLoginButtonClick) + }, + sizeStyle = largeButtonStyle, + colorStyle = ReedButtonColorStyle.KAKAO, + modifier = Modifier + .fillMaxWidth() + .padding( + start = ReedTheme.spacing.spacing5, + end = ReedTheme.spacing.spacing5, + ), + text = stringResource(id = R.string.kakao_login), + leadingIcon = { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_kakao), + contentDescription = "Kakao Icon", + tint = Color.Unspecified, + ) + }, + ) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing4)) + ReedTextButton( + onClick = { + state.eventSink(LoginUiEvent.OnGuestLoginButtonClick) + }, + modifier = Modifier + .fillMaxWidth() + .padding( + start = ReedTheme.spacing.spacing5, + end = ReedTheme.spacing.spacing5, + ), + ) { + Text( + text = stringResource(id = R.string.guest_login), + color = ReedTheme.colors.contentSecondary, + style = ReedTheme.typography.body1Medium, ) - }, - ) + } + } } if (state.isLoading) { diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUiState.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUiState.kt index fea02984..fcfd5ed3 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUiState.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUiState.kt @@ -24,4 +24,5 @@ sealed interface LoginUiEvent : CircuitUiEvent { data object OnKakaoLoginButtonClick : LoginUiEvent data class Login(val accessToken: String) : LoginUiEvent data class LoginFailure(val message: String) : LoginUiEvent + data object OnGuestLoginButtonClick : LoginUiEvent } diff --git a/feature/login/src/main/res/values/strings.xml b/feature/login/src/main/res/values/strings.xml index efbe84fc..51ac38df 100644 --- a/feature/login/src/main/res/values/strings.xml +++ b/feature/login/src/main/res/values/strings.xml @@ -5,6 +5,7 @@ 약관 동의 후\n독서 기록을 남겨보세요 약관 전체 동의 시작하기 + 회원가입 없이 둘러보기 (필수)서비스 이용약관 (필수)개인정보처리방침 From d66bbfbf03178830b170cbebad5985d73258d58f Mon Sep 17 00:00:00 2001 From: easyhooon Date: Wed, 27 Aug 2025 17:00:47 +0900 Subject: [PATCH 09/42] =?UTF-8?q?[BOOK-298]=20feat:=20=EB=8F=84=EC=84=9C?= =?UTF-8?q?=20=EC=83=81=ED=83=9C=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20?= =?UTF-8?q?API=20=ED=98=B8=EC=B6=9C=EC=8B=9C=20Guest=20Mode=20=EB=B6=84?= =?UTF-8?q?=EA=B8=B0=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/api/repository/BookRepository.kt | 5 ++ .../core/data/impl/mapper/ResponseToModel.kt | 31 ++++++++++++ .../impl/repository/DefaultBookRepository.kt | 30 +++++++----- .../response/GuestBookSearchResponse.kt | 47 +++++++++++++++++++ .../core/network/service/ReedService.kt | 3 +- .../search/book/BookSearchPresenter.kt | 16 ++++++- .../feature/search/book/BookSearchUiState.kt | 1 + 7 files changed, 118 insertions(+), 15 deletions(-) create mode 100644 core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/GuestBookSearchResponse.kt diff --git a/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/BookRepository.kt b/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/BookRepository.kt index b29faf03..b2a853de 100644 --- a/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/BookRepository.kt +++ b/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/BookRepository.kt @@ -12,6 +12,11 @@ interface BookRepository { val bookRecentSearches: Flow> val libraryRecentSearches: Flow> + suspend fun searchBookAsGuest( + query: String, + start: Int, + ): Result + suspend fun searchBook( query: String, start: Int, diff --git a/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/mapper/ResponseToModel.kt b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/mapper/ResponseToModel.kt index 85de7a76..ba0e0e96 100644 --- a/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/mapper/ResponseToModel.kt +++ b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/mapper/ResponseToModel.kt @@ -26,6 +26,8 @@ import com.ninecraft.booket.core.network.response.BookSearchResponse import com.ninecraft.booket.core.network.response.BookSummary import com.ninecraft.booket.core.network.response.BookUpsertResponse import com.ninecraft.booket.core.network.response.Category +import com.ninecraft.booket.core.network.response.GuestBookSearchResponse +import com.ninecraft.booket.core.network.response.GuestBookSummary import com.ninecraft.booket.core.network.response.HomeResponse import com.ninecraft.booket.core.network.response.LibraryBookSummary import com.ninecraft.booket.core.network.response.LibraryBooks @@ -79,6 +81,35 @@ internal fun BookSummary.toModel(): BookSummaryModel { ) } +internal fun GuestBookSearchResponse.toModel(): BookSearchModel { + return BookSearchModel( + version = version, + title = title, + pubDate = pubDate, + totalResults = totalResults, + startIndex = startIndex, + itemsPerPage = itemsPerPage, + query = query, + searchCategoryId = searchCategoryId, + searchCategoryName = searchCategoryName, + lastPage = lastPage, + books = books.map { it.toModel() }, + ) +} + +internal fun GuestBookSummary.toModel(): BookSummaryModel { + return BookSummaryModel( + isbn13 = isbn13, + title = title.decodeHtmlEntities(), + author = author, + publisher = publisher, + coverImageUrl = coverImageUrl, + link = link, + userBookStatus = "BEFORE_REGISTRATION", + key = "$title-$isbn13", + ) +} + internal fun BookDetailResponse.toModel(): BookDetailModel { return BookDetailModel( version = version, diff --git a/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultBookRepository.kt b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultBookRepository.kt index a43b41ff..2f4580c5 100644 --- a/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultBookRepository.kt +++ b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultBookRepository.kt @@ -5,7 +5,6 @@ import com.ninecraft.booket.core.data.api.repository.BookRepository import com.ninecraft.booket.core.data.impl.mapper.toModel import com.ninecraft.booket.core.datastore.api.datasource.BookRecentSearchDataSource import com.ninecraft.booket.core.datastore.api.datasource.LibraryRecentSearchDataSource -import com.ninecraft.booket.core.datastore.api.datasource.TokenDataSource import com.ninecraft.booket.core.network.request.BookUpsertRequest import com.ninecraft.booket.core.network.service.ReedService import javax.inject.Inject @@ -14,26 +13,31 @@ internal class DefaultBookRepository @Inject constructor( private val service: ReedService, private val bookRecentSearchDataSource: BookRecentSearchDataSource, private val libraryRecentSearchDataSource: LibraryRecentSearchDataSource, - private val tokenDataSource: TokenDataSource, ) : BookRepository { override val bookRecentSearches = bookRecentSearchDataSource.recentSearches override val libraryRecentSearches = libraryRecentSearchDataSource.recentSearches + override suspend fun searchBookAsGuest( + query: String, + start: Int, + ) = runSuspendCatching { + val result = service.searchBookAsGuest( + query = query, + start = start, + ).toModel() + + bookRecentSearchDataSource.addRecentSearch(query) + result + } + override suspend fun searchBook( query: String, start: Int, ) = runSuspendCatching { - val result = if (tokenDataSource.getAccessToken().isEmpty()) { - service.searchBookAsGuest( - query = query, - start = start, - ).toModel() - } else { - service.searchBook( - query = query, - start = start, - ).toModel() - } + val result = service.searchBook( + query = query, + start = start, + ).toModel() bookRecentSearchDataSource.addRecentSearch(query) result diff --git a/core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/GuestBookSearchResponse.kt b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/GuestBookSearchResponse.kt new file mode 100644 index 00000000..e7c9cfcc --- /dev/null +++ b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/GuestBookSearchResponse.kt @@ -0,0 +1,47 @@ +package com.ninecraft.booket.core.network.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class GuestBookSearchResponse( + @SerialName("version") + val version: String, + @SerialName("title") + val title: String, + @SerialName("pubDate") + val pubDate: String, + @SerialName("totalResults") + val totalResults: Int, + @SerialName("startIndex") + val startIndex: Int, + @SerialName("itemsPerPage") + val itemsPerPage: Int, + @SerialName("query") + val query: String, + @SerialName("searchCategoryId") + val searchCategoryId: Int, + @SerialName("searchCategoryName") + val searchCategoryName: String, + @SerialName("lastPage") + val lastPage: Boolean, + @SerialName("books") + val books: List, +) + +@Serializable +data class GuestBookSummary( + @SerialName("isbn13") + val isbn13: String, + @SerialName("title") + val title: String, + @SerialName("author") + val author: String, + @SerialName("publisher") + val publisher: String, + @SerialName("coverImageUrl") + val coverImageUrl: String, + @SerialName("link") + val link: String, +) + diff --git a/core/network/src/main/kotlin/com/ninecraft/booket/core/network/service/ReedService.kt b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/service/ReedService.kt index 84c3a888..00dc717b 100644 --- a/core/network/src/main/kotlin/com/ninecraft/booket/core/network/service/ReedService.kt +++ b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/service/ReedService.kt @@ -8,6 +8,7 @@ import com.ninecraft.booket.core.network.request.TermsAgreementRequest import com.ninecraft.booket.core.network.response.BookDetailResponse import com.ninecraft.booket.core.network.response.BookSearchResponse import com.ninecraft.booket.core.network.response.BookUpsertResponse +import com.ninecraft.booket.core.network.response.GuestBookSearchResponse import com.ninecraft.booket.core.network.response.HomeResponse import com.ninecraft.booket.core.network.response.LibraryResponse import com.ninecraft.booket.core.network.response.LoginResponse @@ -61,7 +62,7 @@ interface ReedService { @Query("sort") sort: String = "Accuracy", @Query("cover") cover: String? = "Big", @Query("categoryId") categoryId: Int = 0, - ): BookSearchResponse + ): GuestBookSearchResponse // Book endpoints (auth required) @GET("api/v1/books/search") diff --git a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchPresenter.kt b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchPresenter.kt index 61cc99e5..814600b3 100644 --- a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchPresenter.kt +++ b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchPresenter.kt @@ -11,9 +11,11 @@ import androidx.compose.runtime.setValue import com.ninecraft.booket.core.common.analytics.AnalyticsHelper import com.ninecraft.booket.core.common.constants.BookStatus import com.ninecraft.booket.core.common.utils.handleException +import com.ninecraft.booket.core.data.api.repository.AuthRepository import com.ninecraft.booket.core.data.api.repository.BookRepository import com.ninecraft.booket.core.model.BookSearchModel import com.ninecraft.booket.core.model.BookSummaryModel +import com.ninecraft.booket.core.model.UserState import com.ninecraft.booket.core.ui.component.FooterState import com.ninecraft.booket.feature.screens.LoginScreen import com.ninecraft.booket.feature.screens.RecordScreen @@ -37,6 +39,7 @@ import kotlinx.coroutines.launch class BookSearchPresenter @AssistedInject constructor( @Assisted private val navigator: Navigator, private val repository: BookRepository, + private val authRepository: AuthRepository, private val analyticsHelper: AnalyticsHelper, ) : Presenter { companion object { @@ -53,6 +56,7 @@ class BookSearchPresenter @AssistedInject constructor( @Composable override fun present(): BookSearchUiState { val scope = rememberCoroutineScope() + val userState by authRepository.userState.collectAsRetainedState(initial = UserState.Guest) var uiState by rememberRetained { mutableStateOf(UiState.Idle) } var footerState by rememberRetained { mutableStateOf(FooterState.Idle) } val queryState = rememberTextFieldState() @@ -76,7 +80,11 @@ class BookSearchPresenter @AssistedInject constructor( footerState = FooterState.Loading } - repository.searchBook(query = query, start = startIndex) + if (userState is UserState.Guest) { + repository.searchBookAsGuest(query = query, start = startIndex) + } else { + repository.searchBook(query = query, start = startIndex) + } .onSuccess { result -> searchResult = result books = if (startIndex == START_INDEX) { @@ -113,6 +121,11 @@ class BookSearchPresenter @AssistedInject constructor( } fun upsertBook(isbn13: String, bookStatus: String) { + if (userState is UserState.Guest) { + sideEffect = BookSearchSideEffect.ShowToast("로그인 필요한 기능입니다.") + return + } + scope.launch { repository.upsertBook(isbn13, bookStatus) .onSuccess { @@ -246,6 +259,7 @@ class BookSearchPresenter @AssistedInject constructor( isBookRegisterBottomSheetVisible = isBookRegisterBottomSheetVisible, selectedBookStatus = selectedBookStatus, isBookRegisterSuccessBottomSheetVisible = isBookRegisterSuccessBottomSheetVisible, + isGuestMode = userState is UserState.Guest, sideEffect = sideEffect, eventSink = ::handleEvent, ) diff --git a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUiState.kt b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUiState.kt index 5f023961..97e7ede8 100644 --- a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUiState.kt +++ b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUiState.kt @@ -31,6 +31,7 @@ data class BookSearchUiState( val isBookRegisterBottomSheetVisible: Boolean = false, val selectedBookStatus: BookStatus? = null, val isBookRegisterSuccessBottomSheetVisible: Boolean = false, + val isGuestMode: Boolean = false, val sideEffect: BookSearchSideEffect? = null, val eventSink: (BookSearchUiEvent) -> Unit, ) : CircuitUiState { From c9277384f62420137d69d6dac1f023a162865ff2 Mon Sep 17 00:00:00 2001 From: easyhooon Date: Wed, 27 Aug 2025 17:47:34 +0900 Subject: [PATCH 10/42] =?UTF-8?q?[BOOK-298]=20feat:=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20FullScreenDialog=20=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/res/drawable/ic_kakao.xml | 0 .../main/res/drawable/img_reed_logo_big.webp | Bin .../src/main/res/values/strings.xml | 2 + .../ninecraft/booket/feature/login/LoginUi.kt | 10 +- feature/login/src/main/res/values/strings.xml | 2 - feature/search/build.gradle.kts | 1 + .../search/book/BookSearchPresenter.kt | 10 +- .../feature/search/book/BookSearchUi.kt | 38 ++++-- .../feature/search/book/BookSearchUiState.kt | 3 + .../search/book/component/LoginDialog.kt | 122 ++++++++++++++++++ 10 files changed, 169 insertions(+), 19 deletions(-) rename {feature/login => core/designsystem}/src/main/res/drawable/ic_kakao.xml (100%) rename {feature/login => core/designsystem}/src/main/res/drawable/img_reed_logo_big.webp (100%) create mode 100644 feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/component/LoginDialog.kt diff --git a/feature/login/src/main/res/drawable/ic_kakao.xml b/core/designsystem/src/main/res/drawable/ic_kakao.xml similarity index 100% rename from feature/login/src/main/res/drawable/ic_kakao.xml rename to core/designsystem/src/main/res/drawable/ic_kakao.xml diff --git a/feature/login/src/main/res/drawable/img_reed_logo_big.webp b/core/designsystem/src/main/res/drawable/img_reed_logo_big.webp similarity index 100% rename from feature/login/src/main/res/drawable/img_reed_logo_big.webp rename to core/designsystem/src/main/res/drawable/img_reed_logo_big.webp diff --git a/core/designsystem/src/main/res/values/strings.xml b/core/designsystem/src/main/res/values/strings.xml index 90f1c070..2015de18 100644 --- a/core/designsystem/src/main/res/values/strings.xml +++ b/core/designsystem/src/main/res/values/strings.xml @@ -3,4 +3,6 @@ 이용에 불편을 드려 죄송합니다.\n잠시후 다시 이용해주세요. 알 수 없는 오류가 발생하였습니다. 도서 검색 후 내 서재에 담아보세요 + 책 덮기 전 한 문장을 기록해보세요 + 카카오로 시작하기 diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt index 7ae0fb53..a29fd280 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt @@ -10,7 +10,6 @@ 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.systemBarsPadding import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -34,6 +33,7 @@ import com.ninecraft.booket.core.ui.component.ReedLoadingIndicator import com.ninecraft.booket.feature.screens.LoginScreen import com.slack.circuit.codegen.annotations.CircuitInject import dagger.hilt.android.components.ActivityRetainedComponent +import com.ninecraft.booket.core.designsystem.R as designR @CircuitInject(LoginScreen::class, ActivityRetainedComponent::class) @Composable @@ -67,13 +67,13 @@ internal fun LoginUi( horizontalAlignment = Alignment.CenterHorizontally, ) { Image( - painter = painterResource(R.drawable.img_reed_logo_big), + painter = painterResource(designR.drawable.img_reed_logo_big), contentDescription = "Reed Logo", modifier = Modifier.height(67.14.dp), ) Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing5)) Text( - text = stringResource(R.string.login_reed_slogan), + text = stringResource(designR.string.login_reed_slogan), color = ReedTheme.colors.contentBrand, style = ReedTheme.typography.headline2SemiBold, ) @@ -91,10 +91,10 @@ internal fun LoginUi( start = ReedTheme.spacing.spacing5, end = ReedTheme.spacing.spacing5, ), - text = stringResource(id = R.string.kakao_login), + text = stringResource(id = designR.string.kakao_login), leadingIcon = { Icon( - imageVector = ImageVector.vectorResource(id = R.drawable.ic_kakao), + imageVector = ImageVector.vectorResource(id = designR.drawable.ic_kakao), contentDescription = "Kakao Icon", tint = Color.Unspecified, ) diff --git a/feature/login/src/main/res/values/strings.xml b/feature/login/src/main/res/values/strings.xml index 51ac38df..e732ccbb 100644 --- a/feature/login/src/main/res/values/strings.xml +++ b/feature/login/src/main/res/values/strings.xml @@ -1,7 +1,5 @@ - 책 덮기 전 한 문장을 기록해보세요 - 카카오로 시작하기 약관 동의 후\n독서 기록을 남겨보세요 약관 전체 동의 시작하기 diff --git a/feature/search/build.gradle.kts b/feature/search/build.gradle.kts index ff1f0354..3cbeb726 100644 --- a/feature/search/build.gradle.kts +++ b/feature/search/build.gradle.kts @@ -17,6 +17,7 @@ ksp { dependencies { implementations( libs.kotlinx.collections.immutable, + libs.compose.system.ui.controller, libs.logger, ) diff --git a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchPresenter.kt b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchPresenter.kt index 814600b3..6c12b0e8 100644 --- a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchPresenter.kt +++ b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchPresenter.kt @@ -70,6 +70,7 @@ class BookSearchPresenter @AssistedInject constructor( var isBookRegisterBottomSheetVisible by rememberRetained { mutableStateOf(false) } var selectedBookStatus by rememberRetained { mutableStateOf(null) } var isBookRegisterSuccessBottomSheetVisible by rememberRetained { mutableStateOf(false) } + var isLoginDialogVisible by rememberRetained { mutableStateOf(false) } var sideEffect by rememberRetained { mutableStateOf(null) } fun searchBooks(query: String, startIndex: Int = START_INDEX) { @@ -122,7 +123,7 @@ class BookSearchPresenter @AssistedInject constructor( fun upsertBook(isbn13: String, bookStatus: String) { if (userState is UserState.Guest) { - sideEffect = BookSearchSideEffect.ShowToast("로그인 필요한 기능입니다.") + isLoginDialogVisible = true return } @@ -245,6 +246,12 @@ class BookSearchPresenter @AssistedInject constructor( is BookSearchUiEvent.OnBookRegisterSuccessCancelButtonClick -> { isBookRegisterSuccessBottomSheetVisible = false } + + is BookSearchUiEvent.OnLoginDialogDismiss -> { + isLoginDialogVisible = false + } + + is BookSearchUiEvent.OnKakaoLoginButtonClick -> {} } } @@ -260,6 +267,7 @@ class BookSearchPresenter @AssistedInject constructor( selectedBookStatus = selectedBookStatus, isBookRegisterSuccessBottomSheetVisible = isBookRegisterSuccessBottomSheetVisible, isGuestMode = userState is UserState.Guest, + isLoginDialogVisible = isLoginDialogVisible, sideEffect = sideEffect, eventSink = ::handleEvent, ) diff --git a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUi.kt b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUi.kt index 9e4a9c8f..e71cb4a1 100644 --- a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUi.kt +++ b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUi.kt @@ -37,6 +37,7 @@ import com.ninecraft.booket.feature.search.R import com.ninecraft.booket.feature.search.book.component.BookItem import com.ninecraft.booket.feature.search.book.component.BookRegisterBottomSheet import com.ninecraft.booket.feature.search.book.component.BookRegisterSuccessBottomSheet +import com.ninecraft.booket.feature.search.book.component.LoginDialog import com.ninecraft.booket.feature.search.common.component.RecentSearchTitle import com.ninecraft.booket.feature.search.common.component.SearchItem import com.slack.circuit.codegen.annotations.CircuitInject @@ -57,23 +58,38 @@ internal fun SearchUi( modifier = modifier.fillMaxSize(), containerColor = White, ) { innerPadding -> - Column( + Box( modifier = Modifier .fillMaxSize() .padding(innerPadding), ) { - ReedBackTopAppBar( - title = stringResource(R.string.search_title), - onBackClick = { - state.eventSink(BookSearchUiEvent.OnBackClick) - }, - ) - SearchContent( - state = state, - modifier = Modifier, - ) + Column( + modifier = Modifier.fillMaxSize(), + ) { + ReedBackTopAppBar( + title = stringResource(R.string.search_title), + onBackClick = { + state.eventSink(BookSearchUiEvent.OnBackClick) + }, + ) + SearchContent( + state = state, + modifier = Modifier, + ) + } } } + + if (state.isLoginDialogVisible) { + LoginDialog( + onDismissRequest = { + state.eventSink(BookSearchUiEvent.OnLoginDialogDismiss) + }, + onKakaoLoginButtonClick = { + state.eventSink(BookSearchUiEvent.OnKakaoLoginButtonClick) + }, + ) + } } @OptIn(ExperimentalMaterial3Api::class) diff --git a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUiState.kt b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUiState.kt index 97e7ede8..5a8bd36a 100644 --- a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUiState.kt +++ b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUiState.kt @@ -32,6 +32,7 @@ data class BookSearchUiState( val selectedBookStatus: BookStatus? = null, val isBookRegisterSuccessBottomSheetVisible: Boolean = false, val isGuestMode: Boolean = false, + val isLoginDialogVisible: Boolean = false, val sideEffect: BookSearchSideEffect? = null, val eventSink: (BookSearchUiEvent) -> Unit, ) : CircuitUiState { @@ -61,6 +62,8 @@ sealed interface BookSearchUiEvent : CircuitUiEvent { data object OnBookRegisterButtonClick : BookSearchUiEvent data object OnBookRegisterSuccessOkButtonClick : BookSearchUiEvent data object OnBookRegisterSuccessCancelButtonClick : BookSearchUiEvent + data object OnLoginDialogDismiss : BookSearchUiEvent + data object OnKakaoLoginButtonClick : BookSearchUiEvent } enum class BookRegisteredState(val value: String) { diff --git a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/component/LoginDialog.kt b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/component/LoginDialog.kt new file mode 100644 index 00000000..3c1b6c18 --- /dev/null +++ b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/component/LoginDialog.kt @@ -0,0 +1,122 @@ +package com.ninecraft.booket.feature.search.book.component + +import androidx.compose.foundation.Image +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.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.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.ninecraft.booket.core.designsystem.component.button.ReedButton +import com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle +import com.ninecraft.booket.core.designsystem.component.button.largeButtonStyle +import com.ninecraft.booket.core.designsystem.theme.ReedTheme +import com.ninecraft.booket.core.designsystem.theme.White +import com.ninecraft.booket.core.ui.ReedScaffold +import tech.thdev.compose.exteions.system.ui.controller.rememberSystemUiController +import com.ninecraft.booket.core.designsystem.R as designR + +@Composable +internal fun LoginDialog( + onDismissRequest: () -> Unit, + onKakaoLoginButtonClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val systemUiController = rememberSystemUiController() + + DisposableEffect(systemUiController) { + systemUiController.setSystemBarsColor( + color = Color.Transparent, + darkIcons = true, + isNavigationBarContrastEnforced = true, + ) + + onDispose {} + } + + Dialog( + onDismissRequest = { + onDismissRequest() + }, + properties = DialogProperties( + decorFitsSystemWindows = false, + usePlatformDefaultWidth = false, + ), + ) { + ReedScaffold( + modifier = modifier.fillMaxSize(), + containerColor = White, + ) { innerPadding -> + Column( + modifier = modifier + .fillMaxSize() + .background(White) + .padding(innerPadding), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Box(modifier = modifier.fillMaxSize()) { + Column { + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + painter = painterResource(designR.drawable.img_reed_logo_big), + contentDescription = "Reed Logo", + modifier = Modifier.height(67.14.dp), + ) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing5)) + Text( + text = stringResource(designR.string.login_reed_slogan), + color = ReedTheme.colors.contentBrand, + style = ReedTheme.typography.headline2SemiBold, + ) + } + ReedButton( + onClick = { + onKakaoLoginButtonClick() + }, + sizeStyle = largeButtonStyle, + colorStyle = ReedButtonColorStyle.KAKAO, + modifier = Modifier + .fillMaxWidth() + .padding( + start = ReedTheme.spacing.spacing5, + end = ReedTheme.spacing.spacing5, + ), + text = stringResource(id = designR.string.kakao_login), + leadingIcon = { + Icon( + imageVector = ImageVector.vectorResource(id = designR.drawable.ic_kakao), + contentDescription = "Kakao Icon", + tint = Color.Unspecified, + ) + }, + ) + } + } + } + } + } +} From 4ea2bd04d47c5c5da79947383bafc48ad7188a4a Mon Sep 17 00:00:00 2001 From: easyhooon Date: Thu, 28 Aug 2025 09:09:15 +0900 Subject: [PATCH 11/42] =?UTF-8?q?[BOOK-298]=20feat:=20UiText=20=EB=8F=84?= =?UTF-8?q?=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../booket/core/common/utils/UiText.kt | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/UiText.kt diff --git a/core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/UiText.kt b/core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/UiText.kt new file mode 100644 index 00000000..bfe3bbf5 --- /dev/null +++ b/core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/UiText.kt @@ -0,0 +1,27 @@ +package com.ninecraft.booket.core.common.utils + +import android.content.Context +import androidx.annotation.StringRes +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource + +// https://www.youtube.com/watch?v=mB1Lej0aDus +sealed class UiText { + data class DirectString(val value: String) : UiText() + + class StringResource( + @StringRes val resId: Int, + vararg val args: Any, + ) : UiText() + + @Composable + fun asString() = when (this) { + is DirectString -> value + is StringResource -> stringResource(resId, *args) + } + + fun asString(context: Context) = when (this) { + is DirectString -> value + is StringResource -> context.getString(resId, *args) + } +} From b08e865ecaf80ff0a896cd0587f91a998157e193 Mon Sep 17 00:00:00 2001 From: JI HUN LEE <51016231+easyhooon@users.noreply.github.com> Date: Thu, 28 Aug 2025 11:44:34 +0900 Subject: [PATCH 12/42] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8a202dc4..5a73577e 100644 --- a/README.md +++ b/README.md @@ -63,9 +63,9 @@ Reed(리드) - 문장과 감정을 함께 담는 독서 기록 [PlayStore](https - [Circuit](https://github.com/slackhq/circuit) - Dagger Hilt -- Retrofit, OkHttp +- Retrofit, OkHttp3 - Coil-Compose, [Landscapist](https://github.com/skydoves/landscapist) -- Google-ML Kit +- Google ML Kit - Lottie-Compose - Firebase(Analytics, Crashlytics, Remote Config) - Kakao-Auth From 0d53f6067199f8ea64bb83a67d921434748d510a Mon Sep 17 00:00:00 2001 From: easyhooon Date: Thu, 28 Aug 2025 13:45:32 +0900 Subject: [PATCH 13/42] =?UTF-8?q?[BOOK-298]=20feat:=20Guest=20=EB=AA=A8?= =?UTF-8?q?=EB=93=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?Redirect=20=EB=B0=8F=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=ED=9B=84?= =?UTF-8?q?=20=EC=9B=90=EB=9E=98=20=ED=99=94=EB=A9=B4=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=8F=8C=EC=95=84=EC=98=A4=EA=B8=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/res/values/strings.xml | 3 +- .../detail/book/BookDetailPresenter.kt | 8 +- .../detail/record/RecordDetailPresenter.kt | 4 +- .../edit/record/RecordEditPresenter.kt | 2 +- .../library/HandleLibrarySideEffects.kt | 2 +- .../feature/library/LibraryPresenter.kt | 29 ++++- .../booket/feature/library/LibraryUi.kt | 46 ++++++- .../booket/feature/library/LibraryUiState.kt | 9 +- .../library/src/main/res/values/strings.xml | 3 + .../booket/feature/login/LoginPresenter.kt | 27 +++- .../ninecraft/booket/feature/login/LoginUi.kt | 48 ++++--- .../booket/feature/login/LoginUiState.kt | 4 + .../termsagreement/TermsAgreementPresenter.kt | 24 +++- .../termsagreement/TermsAgreementUiState.kt | 1 + .../login}/src/main/res/drawable/ic_kakao.xml | 0 .../main/res/drawable/img_reed_logo_big.webp | Bin feature/login/src/main/res/values/strings.xml | 2 + .../feature/onboarding/OnboardingPresenter.kt | 2 +- .../register/RecordRegisterPresenter.kt | 2 +- .../booket/feature/screens/Screens.kt | 4 +- .../screens/component/MainBottomBar.kt | 17 --- .../feature/screens/extensions/Navigator.kt | 18 +++ .../search/book/BookSearchPresenter.kt | 34 ++--- .../feature/search/book/BookSearchUi.kt | 43 +++--- .../feature/search/book/BookSearchUiState.kt | 7 +- ...ffect.kt => HandleBookSearchSideEffect.kt} | 7 +- .../search/book/component/LoginDialog.kt | 122 ------------------ .../search/library/LibrarySearchPresenter.kt | 2 +- .../search/src/main/res/values/strings.xml | 1 + .../feature/settings/SettingsPresenter.kt | 19 ++- .../booket/feature/settings/SettingsUi.kt | 34 +++-- .../feature/settings/SettingsUiState.kt | 2 + .../settings/src/main/res/values/strings.xml | 1 + .../booket/splash/SplashPresenter.kt | 4 +- 34 files changed, 275 insertions(+), 256 deletions(-) rename {core/designsystem => feature/login}/src/main/res/drawable/ic_kakao.xml (100%) rename {core/designsystem => feature/login}/src/main/res/drawable/img_reed_logo_big.webp (100%) rename feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/{HandlingBookSearchSideEffect.kt => HandleBookSearchSideEffect.kt} (75%) delete mode 100644 feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/component/LoginDialog.kt diff --git a/core/designsystem/src/main/res/values/strings.xml b/core/designsystem/src/main/res/values/strings.xml index 2015de18..a2793c60 100644 --- a/core/designsystem/src/main/res/values/strings.xml +++ b/core/designsystem/src/main/res/values/strings.xml @@ -3,6 +3,5 @@ 이용에 불편을 드려 죄송합니다.\n잠시후 다시 이용해주세요. 알 수 없는 오류가 발생하였습니다. 도서 검색 후 내 서재에 담아보세요 - 책 덮기 전 한 문장을 기록해보세요 - 카카오로 시작하기 + 로그인이 필요한 기능입니다 diff --git a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt index 4d00094f..f9f401db 100644 --- a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt +++ b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt @@ -135,7 +135,7 @@ class BookDetailPresenter @AssistedInject constructor( exception = e, onError = handleErrorMessage, onLoginRequired = { - navigator.resetRoot(LoginScreen) + navigator.resetRoot(LoginScreen()) }, ) } @@ -160,7 +160,7 @@ class BookDetailPresenter @AssistedInject constructor( exception = exception, onError = handleErrorMessage, onLoginRequired = { - navigator.resetRoot(LoginScreen) + navigator.resetRoot(LoginScreen()) }, ) } @@ -210,7 +210,7 @@ class BookDetailPresenter @AssistedInject constructor( exception = exception, onError = handleErrorMessage, onLoginRequired = { - navigator.resetRoot(LoginScreen) + navigator.resetRoot(LoginScreen()) }, ) } @@ -234,7 +234,7 @@ class BookDetailPresenter @AssistedInject constructor( exception = exception, onError = handleErrorMessage, onLoginRequired = { - navigator.resetRoot(LoginScreen) + navigator.resetRoot(LoginScreen()) }, ) } diff --git a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailPresenter.kt b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailPresenter.kt index f5921683..392891a0 100644 --- a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailPresenter.kt +++ b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailPresenter.kt @@ -69,7 +69,7 @@ class RecordDetailPresenter @AssistedInject constructor( exception = exception, onError = handleErrorMessage, onLoginRequired = { - navigator.resetRoot(LoginScreen) + navigator.resetRoot(LoginScreen()) }, ) } @@ -93,7 +93,7 @@ class RecordDetailPresenter @AssistedInject constructor( exception = exception, onError = handleErrorMessage, onLoginRequired = { - navigator.resetRoot(LoginScreen) + navigator.resetRoot(LoginScreen()) }, ) } diff --git a/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/record/RecordEditPresenter.kt b/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/record/RecordEditPresenter.kt index f420ff45..427ce11e 100644 --- a/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/record/RecordEditPresenter.kt +++ b/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/record/RecordEditPresenter.kt @@ -106,7 +106,7 @@ class RecordEditPresenter @AssistedInject constructor( exception = exception, onError = handleErrorMessage, onLoginRequired = { - navigator.resetRoot(LoginScreen) + navigator.resetRoot(LoginScreen()) }, ) } diff --git a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/HandleLibrarySideEffects.kt b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/HandleLibrarySideEffects.kt index 1b20a035..84681666 100644 --- a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/HandleLibrarySideEffects.kt +++ b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/HandleLibrarySideEffects.kt @@ -15,7 +15,7 @@ internal fun HandleLibrarySideEffects( RememberedEffect(state.sideEffect) { when (state.sideEffect) { is LibrarySideEffect.ShowToast -> { - Toast.makeText(context, state.sideEffect.message, Toast.LENGTH_SHORT).show() + Toast.makeText(context, state.sideEffect.message.asString(context), Toast.LENGTH_SHORT).show() } null -> {} diff --git a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt index 25b609cc..0ba138a5 100644 --- a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt +++ b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt @@ -7,16 +7,21 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import com.ninecraft.booket.core.common.analytics.AnalyticsHelper +import com.ninecraft.booket.core.common.utils.UiText +import com.ninecraft.booket.core.data.api.repository.AuthRepository import com.ninecraft.booket.core.data.api.repository.BookRepository import com.ninecraft.booket.core.model.LibraryBookSummaryModel +import com.ninecraft.booket.core.model.UserState import com.ninecraft.booket.core.ui.component.FooterState import com.ninecraft.booket.feature.screens.BookDetailScreen import com.ninecraft.booket.feature.screens.LibraryScreen import com.ninecraft.booket.feature.screens.LibrarySearchScreen import com.ninecraft.booket.feature.screens.SettingsScreen +import com.ninecraft.booket.feature.screens.extensions.redirectToLogin import com.orhanobut.logger.Logger import com.skydoves.compose.effects.RememberedEffect import com.slack.circuit.codegen.annotations.CircuitInject +import com.slack.circuit.retained.collectAsRetainedState import com.slack.circuit.retained.rememberRetained import com.slack.circuit.runtime.Navigator import com.slack.circuit.runtime.presenter.Presenter @@ -28,10 +33,12 @@ import dagger.hilt.android.components.ActivityRetainedComponent import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.launch +import com.ninecraft.booket.core.designsystem.R as designR class LibraryPresenter @AssistedInject constructor( @Assisted private val navigator: Navigator, - private val repository: BookRepository, + private val bookRepository: BookRepository, + private val authRepository: AuthRepository, private val analyticsHelper: AnalyticsHelper, ) : Presenter { companion object { @@ -42,7 +49,7 @@ class LibraryPresenter @AssistedInject constructor( @Composable override fun present(): LibraryUiState { val scope = rememberCoroutineScope() - + val userState by authRepository.userState.collectAsRetainedState(initial = UserState.Guest) var uiState by rememberRetained { mutableStateOf(UiState.Idle) } var footerState by rememberRetained { mutableStateOf(FooterState.Idle) } var filterChips by rememberRetained { @@ -63,7 +70,7 @@ class LibraryPresenter @AssistedInject constructor( footerState = FooterState.Loading } - repository.filterLibraryBooks(status = status, page = page, size = size) + bookRepository.filterLibraryBooks(status = status, page = page, size = size) .onSuccess { result -> filterChips = filterChips.map { chip -> when (chip.option) { @@ -109,7 +116,14 @@ class LibraryPresenter @AssistedInject constructor( } is LibraryUiEvent.OnLibrarySearchClick -> { - navigator.goTo(LibrarySearchScreen) + if (userState is UserState.Guest) { + scope.launch { + sideEffect = LibrarySideEffect.ShowToast(UiText.StringResource(designR.string.login_required)) + navigator.redirectToLogin() + } + } else { + navigator.goTo(LibrarySearchScreen) + } } is LibraryUiEvent.OnSettingsClick -> { @@ -151,6 +165,12 @@ class LibraryPresenter @AssistedInject constructor( restoreState = true, ) } + + is LibraryUiEvent.OnLoginClick -> { + scope.launch { + navigator.redirectToLogin() + } + } } } @@ -172,6 +192,7 @@ class LibraryPresenter @AssistedInject constructor( filterChips = filterChips, currentFilter = currentFilter, books = books, + isGuestMode = userState is UserState.Guest, sideEffect = sideEffect, eventSink = ::handleEvent, ) diff --git a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUi.kt b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUi.kt index ed1f0512..3fa3a576 100644 --- a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUi.kt +++ b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUi.kt @@ -1,5 +1,7 @@ package com.ninecraft.booket.feature.library +import android.R.attr.onClick +import android.R.id.message import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -17,7 +19,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import com.ninecraft.booket.core.common.utils.isNetworkError import com.ninecraft.booket.core.designsystem.DevicePreview +import com.ninecraft.booket.core.designsystem.component.button.ReedButton +import com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle +import com.ninecraft.booket.core.designsystem.component.button.mediumButtonStyle import com.ninecraft.booket.core.designsystem.theme.ReedTheme import com.ninecraft.booket.core.model.LibraryBookSummaryModel import com.ninecraft.booket.core.ui.ReedScaffold @@ -144,10 +150,42 @@ internal fun LibraryContent( } is UiState.Error -> { - ReedErrorUi( - exception = state.uiState.exception, - onRetryClick = { state.eventSink(LibraryUiEvent.OnRetryClick) }, - ) + if (state.isGuestMode) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = stringResource(R.string.library_login_required_title), + color = ReedTheme.colors.contentPrimary, + textAlign = TextAlign.Center, + style = ReedTheme.typography.headline1SemiBold, + ) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) + Text( + text = stringResource(R.string.library_login_required_description), + color = ReedTheme.colors.contentSecondary, + textAlign = TextAlign.Center, + style = ReedTheme.typography.body1Medium, + ) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing4)) + ReedButton( + onClick = { + state.eventSink(LibraryUiEvent.OnLoginClick) + }, + text = stringResource(R.string.login), + colorStyle = ReedButtonColorStyle.SECONDARY, + sizeStyle = mediumButtonStyle, + ) + } + } + } else { + ReedErrorUi( + exception = state.uiState.exception, + onRetryClick = { state.eventSink(LibraryUiEvent.OnRetryClick) }, + ) + } } } } diff --git a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUiState.kt b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUiState.kt index 83675e2a..213ef73d 100644 --- a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUiState.kt +++ b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUiState.kt @@ -1,6 +1,7 @@ package com.ninecraft.booket.feature.library import androidx.compose.runtime.Immutable +import com.ninecraft.booket.core.common.utils.UiText import com.ninecraft.booket.core.model.LibraryBookSummaryModel import com.ninecraft.booket.core.ui.component.FooterState import com.ninecraft.booket.feature.screens.component.MainTab @@ -9,6 +10,7 @@ import com.slack.circuit.runtime.CircuitUiState import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList +import java.util.UUID @Immutable sealed interface UiState { @@ -25,13 +27,17 @@ data class LibraryUiState( LibraryFilterOption.entries.map { LibraryFilterChip(option = it, count = 0) }.toPersistentList(), val currentFilter: LibraryFilterOption = LibraryFilterOption.TOTAL, val books: ImmutableList = persistentListOf(), + val isGuestMode: Boolean = false, val sideEffect: LibrarySideEffect? = null, val eventSink: (LibraryUiEvent) -> Unit, ) : CircuitUiState @Immutable sealed interface LibrarySideEffect { - data class ShowToast(val message: String) : LibrarySideEffect + data class ShowToast( + val message: UiText, + private val key: String = UUID.randomUUID().toString(), + ) : LibrarySideEffect } sealed interface LibraryUiEvent : CircuitUiEvent { @@ -46,6 +52,7 @@ sealed interface LibraryUiEvent : CircuitUiEvent { data object OnRetryClick : LibraryUiEvent data class OnFilterClick(val filterOption: LibraryFilterOption) : LibraryUiEvent data class OnTabSelected(val tab: MainTab) : LibraryUiEvent + data object OnLoginClick: LibraryUiEvent } data class LibraryFilterChip( diff --git a/feature/library/src/main/res/values/strings.xml b/feature/library/src/main/res/values/strings.xml index 145b3d4a..5bc04e2a 100644 --- a/feature/library/src/main/res/values/strings.xml +++ b/feature/library/src/main/res/values/strings.xml @@ -8,4 +8,7 @@ 완독 아직 등록된 책이 없어요 도서 등록 후 나만의 아카이브를 만들어보세요 + 로그인이 필요해요 + 로그인 후 나만의 서재를 채워보세요 + 로그인 하기 diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt index b745a970..dd634e05 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt @@ -8,13 +8,17 @@ import androidx.compose.runtime.setValue import com.ninecraft.booket.core.common.analytics.AnalyticsHelper import com.ninecraft.booket.core.data.api.repository.AuthRepository import com.ninecraft.booket.core.data.api.repository.UserRepository +import com.ninecraft.booket.core.model.UserState import com.ninecraft.booket.feature.screens.HomeScreen import com.ninecraft.booket.feature.screens.LoginScreen import com.ninecraft.booket.feature.screens.TermsAgreementScreen +import com.ninecraft.booket.feature.screens.extensions.popUntilOrGoTo import com.orhanobut.logger.Logger import com.slack.circuit.codegen.annotations.CircuitInject +import com.slack.circuit.retained.collectAsRetainedState import com.slack.circuit.retained.rememberRetained import com.slack.circuit.runtime.Navigator +import com.slack.circuit.runtime.popUntil import com.slack.circuit.runtime.presenter.Presenter import com.slack.circuitx.effects.ImpressionEffect import dagger.assisted.Assisted @@ -24,6 +28,7 @@ import dagger.hilt.android.components.ActivityRetainedComponent import kotlinx.coroutines.launch class LoginPresenter @AssistedInject constructor( + @Assisted private val screen: LoginScreen, @Assisted private val navigator: Navigator, private val authRepository: AuthRepository, private val userRepository: UserRepository, @@ -37,6 +42,7 @@ class LoginPresenter @AssistedInject constructor( @Composable override fun present(): LoginUiState { val scope = rememberCoroutineScope() + val userState by authRepository.userState.collectAsRetainedState(initial = UserState.Guest) var isLoading by rememberRetained { mutableStateOf(false) } var sideEffect by rememberRetained { mutableStateOf(null) } @@ -45,9 +51,13 @@ class LoginPresenter @AssistedInject constructor( userRepository.getUserProfile() .onSuccess { userProfile -> if (userProfile.termsAgreed) { - navigator.resetRoot(HomeScreen) + if (screen.returnToScreen == null) { + navigator.resetRoot(HomeScreen) + } else { + navigator.popUntil { it == screen.returnToScreen } + } } else { - navigator.resetRoot(TermsAgreementScreen) + navigator.resetRoot(TermsAgreementScreen()) } }.onFailure { exception -> exception.message?.let { Logger.e(it) } @@ -94,15 +104,21 @@ class LoginPresenter @AssistedInject constructor( is LoginUiEvent.OnGuestLoginButtonClick -> { navigator.resetRoot(HomeScreen) } + + is LoginUiEvent.OnCloseButtonClick -> { + navigator.pop() + } } } ImpressionEffect { - analyticsHelper.logScreenView(LoginScreen.name) + analyticsHelper.logScreenView(screen.name) } return LoginUiState( isLoading = isLoading, + isGuestMode = userState is UserState.Guest, + returnToScreen = screen.returnToScreen, sideEffect = sideEffect, eventSink = ::handleEvent, ) @@ -111,6 +127,9 @@ class LoginPresenter @AssistedInject constructor( @CircuitInject(LoginScreen::class, ActivityRetainedComponent::class) @AssistedFactory fun interface Factory { - fun create(navigator: Navigator): LoginPresenter + fun create( + screen: LoginScreen, + navigator: Navigator, + ): LoginPresenter } } diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt index a29fd280..aad18a12 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt @@ -29,6 +29,7 @@ import com.ninecraft.booket.core.designsystem.component.button.largeButtonStyle import com.ninecraft.booket.core.designsystem.theme.ReedTheme import com.ninecraft.booket.core.designsystem.theme.White import com.ninecraft.booket.core.ui.ReedScaffold +import com.ninecraft.booket.core.ui.component.ReedCloseTopAppBar import com.ninecraft.booket.core.ui.component.ReedLoadingIndicator import com.ninecraft.booket.feature.screens.LoginScreen import com.slack.circuit.codegen.annotations.CircuitInject @@ -59,6 +60,11 @@ internal fun LoginUi( ) { Box(modifier = modifier.fillMaxSize()) { Column { + ReedCloseTopAppBar( + onClose = { + state.eventSink(LoginUiEvent.OnCloseButtonClick) + } + ) Column( modifier = Modifier .fillMaxWidth() @@ -67,13 +73,13 @@ internal fun LoginUi( horizontalAlignment = Alignment.CenterHorizontally, ) { Image( - painter = painterResource(designR.drawable.img_reed_logo_big), + painter = painterResource(R.drawable.img_reed_logo_big), contentDescription = "Reed Logo", modifier = Modifier.height(67.14.dp), ) Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing5)) Text( - text = stringResource(designR.string.login_reed_slogan), + text = stringResource(R.string.login_reed_slogan), color = ReedTheme.colors.contentBrand, style = ReedTheme.typography.headline2SemiBold, ) @@ -91,32 +97,34 @@ internal fun LoginUi( start = ReedTheme.spacing.spacing5, end = ReedTheme.spacing.spacing5, ), - text = stringResource(id = designR.string.kakao_login), + text = stringResource(id = R.string.kakao_login), leadingIcon = { Icon( - imageVector = ImageVector.vectorResource(id = designR.drawable.ic_kakao), + imageVector = ImageVector.vectorResource(id = R.drawable.ic_kakao), contentDescription = "Kakao Icon", tint = Color.Unspecified, ) }, ) Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing4)) - ReedTextButton( - onClick = { - state.eventSink(LoginUiEvent.OnGuestLoginButtonClick) - }, - modifier = Modifier - .fillMaxWidth() - .padding( - start = ReedTheme.spacing.spacing5, - end = ReedTheme.spacing.spacing5, - ), - ) { - Text( - text = stringResource(id = R.string.guest_login), - color = ReedTheme.colors.contentSecondary, - style = ReedTheme.typography.body1Medium, - ) + if (state.returnToScreen == null) { + ReedTextButton( + onClick = { + state.eventSink(LoginUiEvent.OnGuestLoginButtonClick) + }, + modifier = Modifier + .fillMaxWidth() + .padding( + start = ReedTheme.spacing.spacing5, + end = ReedTheme.spacing.spacing5, + ), + ) { + Text( + text = stringResource(id = R.string.guest_login), + color = ReedTheme.colors.contentSecondary, + style = ReedTheme.typography.body1Medium, + ) + } } } } diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUiState.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUiState.kt index fcfd5ed3..879edc95 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUiState.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUiState.kt @@ -3,10 +3,13 @@ package com.ninecraft.booket.feature.login import androidx.compose.runtime.Immutable import com.slack.circuit.runtime.CircuitUiEvent import com.slack.circuit.runtime.CircuitUiState +import com.slack.circuit.runtime.screen.Screen import java.util.UUID data class LoginUiState( val isLoading: Boolean = false, + val isGuestMode: Boolean = false, + val returnToScreen: Screen? = null, val sideEffect: LoginSideEffect? = null, val eventSink: (LoginUiEvent) -> Unit, ) : CircuitUiState @@ -25,4 +28,5 @@ sealed interface LoginUiEvent : CircuitUiEvent { data class Login(val accessToken: String) : LoginUiEvent data class LoginFailure(val message: String) : LoginUiEvent data object OnGuestLoginButtonClick : LoginUiEvent + data object OnCloseButtonClick : LoginUiEvent } diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/TermsAgreementPresenter.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/TermsAgreementPresenter.kt index cd68ea36..b5e6a889 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/TermsAgreementPresenter.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/TermsAgreementPresenter.kt @@ -9,15 +9,22 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import com.ninecraft.booket.core.common.analytics.AnalyticsHelper import com.ninecraft.booket.core.common.constants.WebViewConstants +import com.ninecraft.booket.core.data.api.repository.AuthRepository import com.ninecraft.booket.core.data.api.repository.UserRepository +import com.ninecraft.booket.core.model.UserState import com.ninecraft.booket.feature.screens.HomeScreen +import com.ninecraft.booket.feature.screens.LoginScreen import com.ninecraft.booket.feature.screens.TermsAgreementScreen import com.ninecraft.booket.feature.screens.WebViewScreen +import com.ninecraft.booket.feature.screens.extensions.popUntilOrGoTo import com.orhanobut.logger.Logger import com.slack.circuit.codegen.annotations.CircuitInject +import com.slack.circuit.retained.collectAsRetainedState import com.slack.circuit.retained.rememberRetained import com.slack.circuit.runtime.Navigator +import com.slack.circuit.runtime.popUntil import com.slack.circuit.runtime.presenter.Presenter +import com.slack.circuit.runtime.screen.Screen import com.slack.circuitx.effects.ImpressionEffect import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -28,14 +35,17 @@ import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.launch class TermsAgreementPresenter @AssistedInject constructor( + @Assisted private val screen: TermsAgreementScreen, @Assisted private val navigator: Navigator, private val userRepository: UserRepository, + private val authRepository: AuthRepository, private val analyticsHelper: AnalyticsHelper, ) : Presenter { @Composable override fun present(): TermsAgreementUiState { val scope = rememberCoroutineScope() + val userState by authRepository.userState.collectAsRetainedState(initial = UserState.Guest) var sideEffect by rememberRetained { mutableStateOf(null) } var agreedTerms by rememberRetained { @@ -73,7 +83,11 @@ class TermsAgreementPresenter @AssistedInject constructor( scope.launch { userRepository.agreeTerms(true) .onSuccess { - navigator.resetRoot(HomeScreen) + if (userState is UserState.Guest) { + navigator.popUntil { it == screen.returnToScreen } + } else { + navigator.resetRoot(HomeScreen) + } }.onFailure { exception -> exception.message?.let { Logger.e(it) } sideEffect = exception.message?.let { @@ -86,12 +100,13 @@ class TermsAgreementPresenter @AssistedInject constructor( } ImpressionEffect { - analyticsHelper.logScreenView(TermsAgreementScreen.name) + analyticsHelper.logScreenView(screen.name) } return TermsAgreementUiState( isAllAgreed = isAllAgreed, agreedTerms = agreedTerms, + isGuestMode = userState is UserState.Guest, eventSink = ::handleEvent, ) } @@ -99,6 +114,9 @@ class TermsAgreementPresenter @AssistedInject constructor( @CircuitInject(TermsAgreementScreen::class, ActivityRetainedComponent::class) @AssistedFactory fun interface Factory { - fun create(navigator: Navigator): TermsAgreementPresenter + fun create( + screen: TermsAgreementScreen, + navigator: Navigator, + ): TermsAgreementPresenter } } diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/TermsAgreementUiState.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/TermsAgreementUiState.kt index 64c72d49..ee73e0c0 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/TermsAgreementUiState.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/TermsAgreementUiState.kt @@ -9,6 +9,7 @@ import java.util.UUID data class TermsAgreementUiState( val isAllAgreed: Boolean, val agreedTerms: ImmutableList, + val isGuestMode: Boolean = false, val sideEffect: TermsAgreementSideEffect? = null, val eventSink: (TermsAgreementUiEvent) -> Unit, ) : CircuitUiState diff --git a/core/designsystem/src/main/res/drawable/ic_kakao.xml b/feature/login/src/main/res/drawable/ic_kakao.xml similarity index 100% rename from core/designsystem/src/main/res/drawable/ic_kakao.xml rename to feature/login/src/main/res/drawable/ic_kakao.xml diff --git a/core/designsystem/src/main/res/drawable/img_reed_logo_big.webp b/feature/login/src/main/res/drawable/img_reed_logo_big.webp similarity index 100% rename from core/designsystem/src/main/res/drawable/img_reed_logo_big.webp rename to feature/login/src/main/res/drawable/img_reed_logo_big.webp diff --git a/feature/login/src/main/res/values/strings.xml b/feature/login/src/main/res/values/strings.xml index e732ccbb..51ac38df 100644 --- a/feature/login/src/main/res/values/strings.xml +++ b/feature/login/src/main/res/values/strings.xml @@ -1,5 +1,7 @@ + 책 덮기 전 한 문장을 기록해보세요 + 카카오로 시작하기 약관 동의 후\n독서 기록을 남겨보세요 약관 전체 동의 시작하기 diff --git a/feature/onboarding/src/main/kotlin/com/ninecraft/booket/feature/onboarding/OnboardingPresenter.kt b/feature/onboarding/src/main/kotlin/com/ninecraft/booket/feature/onboarding/OnboardingPresenter.kt index 92efa53b..63304a27 100644 --- a/feature/onboarding/src/main/kotlin/com/ninecraft/booket/feature/onboarding/OnboardingPresenter.kt +++ b/feature/onboarding/src/main/kotlin/com/ninecraft/booket/feature/onboarding/OnboardingPresenter.kt @@ -37,7 +37,7 @@ class OnboardingPresenter @AssistedInject constructor( if (event.currentPage == 2) { scope.launch { repository.setOnboardingCompleted(true) - navigator.resetRoot(LoginScreen) + navigator.resetRoot(LoginScreen()) } } else { pagerState.let { state -> diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt index 63fb8fda..26a58a92 100644 --- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt +++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt @@ -146,7 +146,7 @@ class RecordRegisterPresenter @AssistedInject constructor( exception = exception, onError = handleErrorMessage, onLoginRequired = { - navigator.resetRoot(LoginScreen) + navigator.resetRoot(LoginScreen()) }, ) } diff --git a/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/Screens.kt b/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/Screens.kt index f7f1ae61..48a22256 100644 --- a/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/Screens.kt +++ b/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/Screens.kt @@ -16,7 +16,7 @@ data object HomeScreen : ReedScreen(name = ScreenNames.HOME) data object LibraryScreen : ReedScreen(name = ScreenNames.LIBRARY) @Parcelize -data object LoginScreen : ReedScreen(name = ScreenNames.LOGIN) +data class LoginScreen(val returnToScreen: Screen? = null) : ReedScreen(name = ScreenNames.LOGIN) @Parcelize data object SearchScreen : ReedScreen(name = ScreenNames.SEARCH) @@ -25,7 +25,7 @@ data object SearchScreen : ReedScreen(name = ScreenNames.SEARCH) data object LibrarySearchScreen : ReedScreen(name = ScreenNames.LIBRARY_SEARCH) @Parcelize -data object TermsAgreementScreen : ReedScreen(name = ScreenNames.TERMS_AGREEMENT) +data class TermsAgreementScreen(val returnToScreen: Screen? = null) : ReedScreen(name = ScreenNames.TERMS_AGREEMENT) @Parcelize data object SettingsScreen : ReedScreen(name = ScreenNames.SETTINGS) diff --git a/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/component/MainBottomBar.kt b/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/component/MainBottomBar.kt index a23c9bcb..aa29f1e5 100644 --- a/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/component/MainBottomBar.kt +++ b/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/component/MainBottomBar.kt @@ -132,23 +132,6 @@ private fun RowScope.MainBottomBarItem( } } -@Suppress("unused") -fun Navigator.popUntilOrGoTo(screen: Screen) { - if (screen in peekBackStack()) { - popUntil { it == screen } - } else { - goTo(screen) - } -} - -@Composable -fun getCurrentTab(backStack: SaveableBackStack): MainTab? { - val currentScreen = backStack.topRecord?.screen - return currentScreen?.let { screen -> - MainTab.entries.find { it.screen::class == currentScreen::class } - } -} - @ComponentPreview @Composable private fun MainBottomBarPreview() { diff --git a/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/extensions/Navigator.kt b/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/extensions/Navigator.kt index 521365ed..b6437b1b 100644 --- a/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/extensions/Navigator.kt +++ b/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/extensions/Navigator.kt @@ -1,7 +1,10 @@ package com.ninecraft.booket.feature.screens.extensions +import com.ninecraft.booket.feature.screens.LoginScreen import com.ninecraft.booket.feature.screens.ReedScreen import com.slack.circuit.runtime.Navigator +import com.slack.circuit.runtime.popUntil +import com.slack.circuit.runtime.screen.Screen import kotlinx.coroutines.delay suspend fun Navigator.delayedGoTo(screen: ReedScreen, delayMillis: Long = 200L) { @@ -13,3 +16,18 @@ suspend fun Navigator.delayedPop(delayMillis: Long = 200L) { delay(delayMillis) pop() } + +fun Navigator.popUntilOrGoTo(screen: Screen) { + if (screen in peekBackStack()) { + popUntil { it == screen } + } else { + goTo(screen) + } +} + +suspend fun Navigator.redirectToLogin(): Screen? { + val currentScreen = peek() + delayedGoTo(LoginScreen(currentScreen)) + return currentScreen +} + diff --git a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchPresenter.kt b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchPresenter.kt index 6c12b0e8..171753d1 100644 --- a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchPresenter.kt +++ b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchPresenter.kt @@ -1,5 +1,6 @@ package com.ninecraft.booket.feature.search.book +import android.text.TextUtils.replace import androidx.compose.foundation.text.input.clearText import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.runtime.Composable @@ -10,6 +11,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import com.ninecraft.booket.core.common.analytics.AnalyticsHelper import com.ninecraft.booket.core.common.constants.BookStatus +import com.ninecraft.booket.core.common.utils.UiText import com.ninecraft.booket.core.common.utils.handleException import com.ninecraft.booket.core.data.api.repository.AuthRepository import com.ninecraft.booket.core.data.api.repository.BookRepository @@ -21,6 +23,8 @@ import com.ninecraft.booket.feature.screens.LoginScreen import com.ninecraft.booket.feature.screens.RecordScreen import com.ninecraft.booket.feature.screens.SearchScreen import com.ninecraft.booket.feature.screens.extensions.delayedGoTo +import com.ninecraft.booket.feature.screens.extensions.redirectToLogin +import com.ninecraft.booket.feature.search.R import com.orhanobut.logger.Logger import com.slack.circuit.codegen.annotations.CircuitInject import com.slack.circuit.retained.collectAsRetainedState @@ -35,7 +39,9 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.launch +import com.ninecraft.booket.core.designsystem.R as designR +// TODO 도서 검색 화면 진입 로그와, 도서 검색 이벤트 로그를 분리 class BookSearchPresenter @AssistedInject constructor( @Assisted private val navigator: Navigator, private val repository: BookRepository, @@ -122,12 +128,13 @@ class BookSearchPresenter @AssistedInject constructor( } fun upsertBook(isbn13: String, bookStatus: String) { - if (userState is UserState.Guest) { - isLoginDialogVisible = true - return - } - scope.launch { + if (userState is UserState.Guest) { + sideEffect = BookSearchSideEffect.ShowToast(UiText.StringResource(designR.string.login_required)) + navigator.redirectToLogin() + return@launch + } + repository.upsertBook(isbn13, bookStatus) .onSuccess { registeredUserBookId = it.userBookId @@ -147,14 +154,14 @@ class BookSearchPresenter @AssistedInject constructor( analyticsHelper.logEvent(ERROR_REGISTER_BOOK) val handleErrorMessage = { message: String -> Logger.e(message) - sideEffect = BookSearchSideEffect.ShowToast(message) + sideEffect = BookSearchSideEffect.ShowToast(UiText.DirectString(message)) } handleException( exception = exception, onError = handleErrorMessage, onLoginRequired = { - navigator.resetRoot(LoginScreen) + navigator.resetRoot(LoginScreen()) }, ) } @@ -163,6 +170,10 @@ class BookSearchPresenter @AssistedInject constructor( fun handleEvent(event: BookSearchUiEvent) { when (event) { + is BookSearchUiEvent.InitSideEffect -> { + sideEffect = null + } + is BookSearchUiEvent.OnBackClick -> { navigator.pop() } @@ -211,7 +222,7 @@ class BookSearchPresenter @AssistedInject constructor( selectedBookIsbn = event.isbn13 if (selectedBookIsbn.isEmpty()) { - sideEffect = BookSearchSideEffect.ShowToast("isbn이 없는 도서는 등록할 수 없습니다") + sideEffect = BookSearchSideEffect.ShowToast(UiText.StringResource(R.string.error_book_no_isbn)) } else { isBookRegisterBottomSheetVisible = true } @@ -246,12 +257,6 @@ class BookSearchPresenter @AssistedInject constructor( is BookSearchUiEvent.OnBookRegisterSuccessCancelButtonClick -> { isBookRegisterSuccessBottomSheetVisible = false } - - is BookSearchUiEvent.OnLoginDialogDismiss -> { - isLoginDialogVisible = false - } - - is BookSearchUiEvent.OnKakaoLoginButtonClick -> {} } } @@ -267,7 +272,6 @@ class BookSearchPresenter @AssistedInject constructor( selectedBookStatus = selectedBookStatus, isBookRegisterSuccessBottomSheetVisible = isBookRegisterSuccessBottomSheetVisible, isGuestMode = userState is UserState.Guest, - isLoginDialogVisible = isLoginDialogVisible, sideEffect = sideEffect, eventSink = ::handleEvent, ) diff --git a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUi.kt b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUi.kt index e71cb4a1..c740d483 100644 --- a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUi.kt +++ b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUi.kt @@ -37,7 +37,6 @@ import com.ninecraft.booket.feature.search.R import com.ninecraft.booket.feature.search.book.component.BookItem import com.ninecraft.booket.feature.search.book.component.BookRegisterBottomSheet import com.ninecraft.booket.feature.search.book.component.BookRegisterSuccessBottomSheet -import com.ninecraft.booket.feature.search.book.component.LoginDialog import com.ninecraft.booket.feature.search.common.component.RecentSearchTitle import com.ninecraft.booket.feature.search.common.component.SearchItem import com.slack.circuit.codegen.annotations.CircuitInject @@ -52,44 +51,32 @@ internal fun SearchUi( state: BookSearchUiState, modifier: Modifier = Modifier, ) { - HandleBookSearchSideEffects(state = state) + HandleBookSearchSideEffects( + state = state, + eventSink = state.eventSink, + ) ReedScaffold( modifier = modifier.fillMaxSize(), containerColor = White, ) { innerPadding -> - Box( + Column( modifier = Modifier .fillMaxSize() .padding(innerPadding), ) { - Column( - modifier = Modifier.fillMaxSize(), - ) { - ReedBackTopAppBar( - title = stringResource(R.string.search_title), - onBackClick = { - state.eventSink(BookSearchUiEvent.OnBackClick) - }, - ) - SearchContent( - state = state, - modifier = Modifier, - ) - } + ReedBackTopAppBar( + title = stringResource(R.string.search_title), + onBackClick = { + state.eventSink(BookSearchUiEvent.OnBackClick) + }, + ) + SearchContent( + state = state, + modifier = Modifier, + ) } } - - if (state.isLoginDialogVisible) { - LoginDialog( - onDismissRequest = { - state.eventSink(BookSearchUiEvent.OnLoginDialogDismiss) - }, - onKakaoLoginButtonClick = { - state.eventSink(BookSearchUiEvent.OnKakaoLoginButtonClick) - }, - ) - } } @OptIn(ExperimentalMaterial3Api::class) diff --git a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUiState.kt b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUiState.kt index 5a8bd36a..1dc6894a 100644 --- a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUiState.kt +++ b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUiState.kt @@ -3,6 +3,7 @@ package com.ninecraft.booket.feature.search.book import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.runtime.Immutable import com.ninecraft.booket.core.common.constants.BookStatus +import com.ninecraft.booket.core.common.utils.UiText import com.ninecraft.booket.core.model.BookSearchModel import com.ninecraft.booket.core.model.BookSummaryModel import com.ninecraft.booket.core.ui.component.FooterState @@ -32,7 +33,6 @@ data class BookSearchUiState( val selectedBookStatus: BookStatus? = null, val isBookRegisterSuccessBottomSheetVisible: Boolean = false, val isGuestMode: Boolean = false, - val isLoginDialogVisible: Boolean = false, val sideEffect: BookSearchSideEffect? = null, val eventSink: (BookSearchUiEvent) -> Unit, ) : CircuitUiState { @@ -42,12 +42,13 @@ data class BookSearchUiState( @Immutable sealed interface BookSearchSideEffect { data class ShowToast( - val message: String, + val message: UiText, private val key: String = UUID.randomUUID().toString(), ) : BookSearchSideEffect } sealed interface BookSearchUiEvent : CircuitUiEvent { + data object InitSideEffect : BookSearchUiEvent data object OnBackClick : BookSearchUiEvent data class OnRecentSearchClick(val query: String) : BookSearchUiEvent data class OnRecentSearchDeleteClick(val query: String) : BookSearchUiEvent @@ -62,8 +63,6 @@ sealed interface BookSearchUiEvent : CircuitUiEvent { data object OnBookRegisterButtonClick : BookSearchUiEvent data object OnBookRegisterSuccessOkButtonClick : BookSearchUiEvent data object OnBookRegisterSuccessCancelButtonClick : BookSearchUiEvent - data object OnLoginDialogDismiss : BookSearchUiEvent - data object OnKakaoLoginButtonClick : BookSearchUiEvent } enum class BookRegisteredState(val value: String) { diff --git a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/HandlingBookSearchSideEffect.kt b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/HandleBookSearchSideEffect.kt similarity index 75% rename from feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/HandlingBookSearchSideEffect.kt rename to feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/HandleBookSearchSideEffect.kt index 7249bf51..27fd56e1 100644 --- a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/HandlingBookSearchSideEffect.kt +++ b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/HandleBookSearchSideEffect.kt @@ -8,16 +8,21 @@ import com.skydoves.compose.effects.RememberedEffect @Composable internal fun HandleBookSearchSideEffects( state: BookSearchUiState, + eventSink: (BookSearchUiEvent) -> Unit, ) { val context = LocalContext.current RememberedEffect(state.sideEffect) { when (state.sideEffect) { is BookSearchSideEffect.ShowToast -> { - Toast.makeText(context, state.sideEffect.message, Toast.LENGTH_SHORT).show() + Toast.makeText(context, state.sideEffect.message.asString(context), Toast.LENGTH_SHORT).show() } null -> {} } + + if (state.sideEffect != null) { + eventSink(BookSearchUiEvent.InitSideEffect) + } } } diff --git a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/component/LoginDialog.kt b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/component/LoginDialog.kt deleted file mode 100644 index 3c1b6c18..00000000 --- a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/component/LoginDialog.kt +++ /dev/null @@ -1,122 +0,0 @@ -package com.ninecraft.booket.feature.search.book.component - -import androidx.compose.foundation.Image -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.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.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties -import com.ninecraft.booket.core.designsystem.component.button.ReedButton -import com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle -import com.ninecraft.booket.core.designsystem.component.button.largeButtonStyle -import com.ninecraft.booket.core.designsystem.theme.ReedTheme -import com.ninecraft.booket.core.designsystem.theme.White -import com.ninecraft.booket.core.ui.ReedScaffold -import tech.thdev.compose.exteions.system.ui.controller.rememberSystemUiController -import com.ninecraft.booket.core.designsystem.R as designR - -@Composable -internal fun LoginDialog( - onDismissRequest: () -> Unit, - onKakaoLoginButtonClick: () -> Unit, - modifier: Modifier = Modifier, -) { - val systemUiController = rememberSystemUiController() - - DisposableEffect(systemUiController) { - systemUiController.setSystemBarsColor( - color = Color.Transparent, - darkIcons = true, - isNavigationBarContrastEnforced = true, - ) - - onDispose {} - } - - Dialog( - onDismissRequest = { - onDismissRequest() - }, - properties = DialogProperties( - decorFitsSystemWindows = false, - usePlatformDefaultWidth = false, - ), - ) { - ReedScaffold( - modifier = modifier.fillMaxSize(), - containerColor = White, - ) { innerPadding -> - Column( - modifier = modifier - .fillMaxSize() - .background(White) - .padding(innerPadding), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Box(modifier = modifier.fillMaxSize()) { - Column { - Column( - modifier = Modifier - .fillMaxWidth() - .weight(1f), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Image( - painter = painterResource(designR.drawable.img_reed_logo_big), - contentDescription = "Reed Logo", - modifier = Modifier.height(67.14.dp), - ) - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing5)) - Text( - text = stringResource(designR.string.login_reed_slogan), - color = ReedTheme.colors.contentBrand, - style = ReedTheme.typography.headline2SemiBold, - ) - } - ReedButton( - onClick = { - onKakaoLoginButtonClick() - }, - sizeStyle = largeButtonStyle, - colorStyle = ReedButtonColorStyle.KAKAO, - modifier = Modifier - .fillMaxWidth() - .padding( - start = ReedTheme.spacing.spacing5, - end = ReedTheme.spacing.spacing5, - ), - text = stringResource(id = designR.string.kakao_login), - leadingIcon = { - Icon( - imageVector = ImageVector.vectorResource(id = designR.drawable.ic_kakao), - contentDescription = "Kakao Icon", - tint = Color.Unspecified, - ) - }, - ) - } - } - } - } - } -} diff --git a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/library/LibrarySearchPresenter.kt b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/library/LibrarySearchPresenter.kt index 240d20e7..fc4799d0 100644 --- a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/library/LibrarySearchPresenter.kt +++ b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/library/LibrarySearchPresenter.kt @@ -100,7 +100,7 @@ class LibrarySearchPresenter @AssistedInject constructor( exception = exception, onError = handleErrorMessage, onLoginRequired = { - navigator.resetRoot(LoginScreen) + navigator.resetRoot(LoginScreen()) }, ) } diff --git a/feature/search/src/main/res/values/strings.xml b/feature/search/src/main/res/values/strings.xml index fe971294..d8f946d7 100644 --- a/feature/search/src/main/res/values/strings.xml +++ b/feature/search/src/main/res/values/strings.xml @@ -18,4 +18,5 @@ 등록한 책을 검색해보세요 남긴 기록 내 서재에 해당 도서가 없습니다. + isbn이 없는 도서는 등록할 수 없습니다 diff --git a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsPresenter.kt b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsPresenter.kt index fefe62d2..f2ab6ac5 100644 --- a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsPresenter.kt +++ b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsPresenter.kt @@ -10,13 +10,16 @@ import com.ninecraft.booket.core.common.constants.WebViewConstants import com.ninecraft.booket.core.common.utils.handleException import com.ninecraft.booket.core.data.api.repository.AuthRepository import com.ninecraft.booket.core.data.api.repository.RemoteConfigRepository +import com.ninecraft.booket.core.model.UserState import com.ninecraft.booket.feature.screens.LoginScreen import com.ninecraft.booket.feature.screens.OssLicensesScreen import com.ninecraft.booket.feature.screens.SettingsScreen import com.ninecraft.booket.feature.screens.WebViewScreen +import com.ninecraft.booket.feature.screens.extensions.redirectToLogin import com.orhanobut.logger.Logger import com.skydoves.compose.effects.RememberedEffect import com.slack.circuit.codegen.annotations.CircuitInject +import com.slack.circuit.retained.collectAsRetainedState import com.slack.circuit.retained.rememberRetained import com.slack.circuit.runtime.Navigator import com.slack.circuit.runtime.presenter.Presenter @@ -43,6 +46,7 @@ class SettingsPresenter @AssistedInject constructor( @Composable override fun present(): SettingsUiState { val scope = rememberCoroutineScope() + val userState by authRepository.userState.collectAsRetainedState(initial = UserState.Guest) var isLoading by rememberRetained { mutableStateOf(false) } var isLogoutDialogVisible by rememberRetained { mutableStateOf(false) } var isWithdrawBottomSheetVisible by rememberRetained { mutableStateOf(false) } @@ -58,7 +62,7 @@ class SettingsPresenter @AssistedInject constructor( authRepository.logout() .onSuccess { analyticsHelper.logEvent(SETTINGS_LOGOUT_COMPLETE) - navigator.resetRoot(LoginScreen) + navigator.resetRoot(LoginScreen()) } .onFailure { exception -> val handleErrorMessage = { message: String -> @@ -70,7 +74,7 @@ class SettingsPresenter @AssistedInject constructor( exception = exception, onError = handleErrorMessage, onLoginRequired = { - navigator.resetRoot(LoginScreen) + navigator.resetRoot(LoginScreen()) }, ) } @@ -88,7 +92,7 @@ class SettingsPresenter @AssistedInject constructor( authRepository.withdraw() .onSuccess { analyticsHelper.logEvent(SETTINGS_WITHDRAWAL_COMPLETE) - navigator.resetRoot(LoginScreen) + navigator.resetRoot(LoginScreen()) } .onFailure { exception -> val handleErrorMessage = { message: String -> @@ -100,7 +104,7 @@ class SettingsPresenter @AssistedInject constructor( exception = exception, onError = handleErrorMessage, onLoginRequired = { - navigator.resetRoot(LoginScreen) + navigator.resetRoot(LoginScreen()) }, ) } @@ -190,6 +194,12 @@ class SettingsPresenter @AssistedInject constructor( is SettingsUiEvent.OnVersionClick -> { sideEffect = SettingsSideEffect.NavigateToPlayStore } + + is SettingsUiEvent.OnLoginClick -> { + scope.launch { + navigator.redirectToLogin() + } + } } } @@ -208,6 +218,7 @@ class SettingsPresenter @AssistedInject constructor( isWithdrawConfirmed = isWithdrawConfirmed, latestVersion = latestVersion, isOptionalUpdateDialogVisible = isOptionalUpdateDialogVisible, + isGuestMode = userState is UserState.Guest, sideEffect = sideEffect, eventSink = ::handleEvent, ) diff --git a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsUi.kt b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsUi.kt index 37a60a3a..376516dd 100644 --- a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsUi.kt +++ b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsUi.kt @@ -150,18 +150,28 @@ internal fun SettingsUi( }, ) ReedDivider(modifier = Modifier.padding(vertical = ReedTheme.spacing.spacing4)) - SettingItem( - title = stringResource(R.string.settings_logout), - onItemClick = { - state.eventSink(SettingsUiEvent.OnLogoutClick) - }, - ) - SettingItem( - title = stringResource(R.string.settings_withdraw), - onItemClick = { - state.eventSink(SettingsUiEvent.OnWithdrawClick) - }, - ) + if (state.isGuestMode) { + SettingItem( + title = stringResource(R.string.login), + onItemClick = { + state.eventSink(SettingsUiEvent.OnLoginClick) + }, + ) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing4)) + } else { + SettingItem( + title = stringResource(R.string.settings_logout), + onItemClick = { + state.eventSink(SettingsUiEvent.OnLogoutClick) + }, + ) + SettingItem( + title = stringResource(R.string.settings_withdraw), + onItemClick = { + state.eventSink(SettingsUiEvent.OnWithdrawClick) + }, + ) + } } if (state.isLoading) { diff --git a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsUiState.kt b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsUiState.kt index fb7961e5..43b50cb2 100644 --- a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsUiState.kt +++ b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsUiState.kt @@ -13,6 +13,7 @@ data class SettingsUiState( val latestVersion: String = "", val isUpdateAvailable: Boolean = false, val isOptionalUpdateDialogVisible: Boolean = false, + val isGuestMode: Boolean = false, val sideEffect: SettingsSideEffect? = null, val eventSink: (SettingsUiEvent) -> Unit, ) : CircuitUiState @@ -40,4 +41,5 @@ sealed interface SettingsUiEvent : CircuitUiEvent { data object Logout : SettingsUiEvent data object Withdraw : SettingsUiEvent data object OnVersionClick : SettingsUiEvent + data object OnLoginClick : SettingsUiEvent } diff --git a/feature/settings/src/main/res/values/strings.xml b/feature/settings/src/main/res/values/strings.xml index fa897943..1cd987bd 100644 --- a/feature/settings/src/main/res/values/strings.xml +++ b/feature/settings/src/main/res/values/strings.xml @@ -18,4 +18,5 @@ 최신 버전이 출시되었습니다. 최적의 사용 환경을 위해 업데이트해주세요. 업데이트하기 + 로그인 diff --git a/feature/splash/src/main/kotlin/com/ninecraft/booket/splash/SplashPresenter.kt b/feature/splash/src/main/kotlin/com/ninecraft/booket/splash/SplashPresenter.kt index a039dc62..6af98e78 100644 --- a/feature/splash/src/main/kotlin/com/ninecraft/booket/splash/SplashPresenter.kt +++ b/feature/splash/src/main/kotlin/com/ninecraft/booket/splash/SplashPresenter.kt @@ -56,7 +56,7 @@ class SplashPresenter @AssistedInject constructor( if (userProfile.termsAgreed) { navigator.resetRoot(HomeScreen) } else { - navigator.resetRoot(LoginScreen) + navigator.resetRoot(LoginScreen()) } } .onFailure { exception -> @@ -83,7 +83,7 @@ class SplashPresenter @AssistedInject constructor( } AutoLoginState.NOT_LOGGED_IN -> { - navigator.resetRoot(LoginScreen) + navigator.resetRoot(LoginScreen()) } AutoLoginState.IDLE -> { From bd64507667d37a8557ab10e6bc8a9594c40c6e41 Mon Sep 17 00:00:00 2001 From: easyhooon Date: Thu, 28 Aug 2025 14:07:36 +0900 Subject: [PATCH 14/42] =?UTF-8?q?[BOOK-298]=20feat:=20=EB=8F=84=EC=84=9C?= =?UTF-8?q?=20=EA=B2=80=EC=83=89=20=ED=99=94=EB=A9=B4=20=EC=A7=84=EC=9E=85?= =?UTF-8?q?,=20=EB=8F=84=EC=84=9C=20=EA=B2=80=EC=83=89=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=20=EB=A1=9C=EA=B7=B8=20=EC=88=98=EC=A7=91=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LibrarySearch 화면과 분리를 위한 도서 검색화면 prefix Book 추가 --- .../booket/feature/home/HomePresenter.kt | 4 ++-- .../ninecraft/booket/feature/login/LoginUi.kt | 1 - .../booket/feature/screens/ScreenNames.kt | 2 +- .../booket/feature/screens/Screens.kt | 2 +- .../search/book/BookSearchPresenter.kt | 23 +++++++++---------- .../feature/search/book/BookSearchUi.kt | 14 +++++------ 6 files changed, 22 insertions(+), 24 deletions(-) diff --git a/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomePresenter.kt b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomePresenter.kt index ebf3491e..777df64a 100644 --- a/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomePresenter.kt +++ b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomePresenter.kt @@ -13,7 +13,7 @@ import com.ninecraft.booket.core.model.UserState import com.ninecraft.booket.feature.screens.BookDetailScreen import com.ninecraft.booket.feature.screens.HomeScreen import com.ninecraft.booket.feature.screens.RecordScreen -import com.ninecraft.booket.feature.screens.SearchScreen +import com.ninecraft.booket.feature.screens.BookSearchScreen import com.ninecraft.booket.feature.screens.SettingsScreen import com.skydoves.compose.effects.RememberedEffect import com.slack.circuit.codegen.annotations.CircuitInject @@ -67,7 +67,7 @@ class HomePresenter @AssistedInject constructor( } is HomeUiEvent.OnBookRegisterClick -> { - navigator.goTo(SearchScreen) + navigator.goTo(BookSearchScreen) } is HomeUiEvent.OnRecordButtonClick -> { diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt index aad18a12..20c5150d 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt @@ -34,7 +34,6 @@ import com.ninecraft.booket.core.ui.component.ReedLoadingIndicator import com.ninecraft.booket.feature.screens.LoginScreen import com.slack.circuit.codegen.annotations.CircuitInject import dagger.hilt.android.components.ActivityRetainedComponent -import com.ninecraft.booket.core.designsystem.R as designR @CircuitInject(LoginScreen::class, ActivityRetainedComponent::class) @Composable diff --git a/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/ScreenNames.kt b/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/ScreenNames.kt index 1c31c461..338d70a1 100644 --- a/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/ScreenNames.kt +++ b/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/ScreenNames.kt @@ -5,7 +5,7 @@ object ScreenNames { const val HOME = "home_main" const val LIBRARY = "library_main" const val LOGIN = "login_select_method" - const val SEARCH = "search_book_input" + const val BOOK_SEARCH = "search_book_start" const val LIBRARY_SEARCH = "library_search_book" const val TERMS_AGREEMENT = "login_terms_agreement" const val SETTINGS = "settings_main" diff --git a/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/Screens.kt b/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/Screens.kt index 48a22256..05f01d7b 100644 --- a/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/Screens.kt +++ b/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/Screens.kt @@ -19,7 +19,7 @@ data object LibraryScreen : ReedScreen(name = ScreenNames.LIBRARY) data class LoginScreen(val returnToScreen: Screen? = null) : ReedScreen(name = ScreenNames.LOGIN) @Parcelize -data object SearchScreen : ReedScreen(name = ScreenNames.SEARCH) +data object BookSearchScreen : ReedScreen(name = ScreenNames.BOOK_SEARCH) @Parcelize data object LibrarySearchScreen : ReedScreen(name = ScreenNames.LIBRARY_SEARCH) diff --git a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchPresenter.kt b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchPresenter.kt index 171753d1..6745aa25 100644 --- a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchPresenter.kt +++ b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchPresenter.kt @@ -1,6 +1,5 @@ package com.ninecraft.booket.feature.search.book -import android.text.TextUtils.replace import androidx.compose.foundation.text.input.clearText import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.runtime.Composable @@ -21,7 +20,7 @@ import com.ninecraft.booket.core.model.UserState import com.ninecraft.booket.core.ui.component.FooterState import com.ninecraft.booket.feature.screens.LoginScreen import com.ninecraft.booket.feature.screens.RecordScreen -import com.ninecraft.booket.feature.screens.SearchScreen +import com.ninecraft.booket.feature.screens.BookSearchScreen import com.ninecraft.booket.feature.screens.extensions.delayedGoTo import com.ninecraft.booket.feature.screens.extensions.redirectToLogin import com.ninecraft.booket.feature.search.R @@ -31,6 +30,7 @@ import com.slack.circuit.retained.collectAsRetainedState import com.slack.circuit.retained.rememberRetained import com.slack.circuit.runtime.Navigator import com.slack.circuit.runtime.presenter.Presenter +import com.slack.circuitx.effects.ImpressionEffect import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -41,7 +41,6 @@ import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.launch import com.ninecraft.booket.core.designsystem.R as designR -// TODO 도서 검색 화면 진입 로그와, 도서 검색 이벤트 로그를 분리 class BookSearchPresenter @AssistedInject constructor( @Assisted private val navigator: Navigator, private val repository: BookRepository, @@ -50,7 +49,6 @@ class BookSearchPresenter @AssistedInject constructor( ) : Presenter { companion object { private const val START_INDEX = 1 - private const val SEARCH_BOOK_INPUT = "search_book_input" private const val SEARCH_BOOK_RESULT = "search_book_result" private const val SEARCH_BOOK_NO_RESULT = "search_book_noresult" private const val ERROR_SEARCH_LOADING = "error_search_loading" @@ -76,7 +74,6 @@ class BookSearchPresenter @AssistedInject constructor( var isBookRegisterBottomSheetVisible by rememberRetained { mutableStateOf(false) } var selectedBookStatus by rememberRetained { mutableStateOf(null) } var isBookRegisterSuccessBottomSheetVisible by rememberRetained { mutableStateOf(false) } - var isLoginDialogVisible by rememberRetained { mutableStateOf(false) } var sideEffect by rememberRetained { mutableStateOf(null) } fun searchBooks(query: String, startIndex: Int = START_INDEX) { @@ -105,14 +102,13 @@ class BookSearchPresenter @AssistedInject constructor( if (startIndex == START_INDEX) { uiState = UiState.Success + analyticsHelper.logEvent(SEARCH_BOOK_RESULT) + if (result.books.isEmpty()) { + analyticsHelper.logEvent(SEARCH_BOOK_NO_RESULT) + } } else { footerState = if (isLastPage) FooterState.End else FooterState.Idle } - - analyticsHelper.logEvent(SEARCH_BOOK_RESULT) - if (startIndex == START_INDEX && result.books.isEmpty()) { - analyticsHelper.logEvent(SEARCH_BOOK_NO_RESULT) - } } .onFailure { exception -> Logger.d(exception) @@ -195,7 +191,6 @@ class BookSearchPresenter @AssistedInject constructor( is BookSearchUiEvent.OnSearchClick -> { val query = event.query.trim() if (query.isNotEmpty()) { - analyticsHelper.logEvent(SEARCH_BOOK_INPUT) searchBooks(query = query, startIndex = START_INDEX) } } @@ -260,6 +255,10 @@ class BookSearchPresenter @AssistedInject constructor( } } + ImpressionEffect { + analyticsHelper.logScreenView(BookSearchScreen.name) + } + return BookSearchUiState( uiState = uiState, footerState = footerState, @@ -277,7 +276,7 @@ class BookSearchPresenter @AssistedInject constructor( ) } - @CircuitInject(SearchScreen::class, ActivityRetainedComponent::class) + @CircuitInject(BookSearchScreen::class, ActivityRetainedComponent::class) @AssistedFactory fun interface Factory { fun create(navigator: Navigator): BookSearchPresenter diff --git a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUi.kt b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUi.kt index c740d483..c1cef523 100644 --- a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUi.kt +++ b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUi.kt @@ -32,7 +32,7 @@ import com.ninecraft.booket.core.ui.component.LoadStateFooter import com.ninecraft.booket.core.ui.component.ReedBackTopAppBar import com.ninecraft.booket.core.ui.component.ReedErrorUi import com.ninecraft.booket.core.ui.component.ReedLoadingIndicator -import com.ninecraft.booket.feature.screens.SearchScreen +import com.ninecraft.booket.feature.screens.BookSearchScreen import com.ninecraft.booket.feature.search.R import com.ninecraft.booket.feature.search.book.component.BookItem import com.ninecraft.booket.feature.search.book.component.BookRegisterBottomSheet @@ -45,9 +45,9 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.launch import com.ninecraft.booket.core.designsystem.R as designR -@CircuitInject(SearchScreen::class, ActivityRetainedComponent::class) +@CircuitInject(BookSearchScreen::class, ActivityRetainedComponent::class) @Composable -internal fun SearchUi( +internal fun BookSearchUi( state: BookSearchUiState, modifier: Modifier = Modifier, ) { @@ -71,7 +71,7 @@ internal fun SearchUi( state.eventSink(BookSearchUiEvent.OnBackClick) }, ) - SearchContent( + BookSearchContent( state = state, modifier = Modifier, ) @@ -81,7 +81,7 @@ internal fun SearchUi( @OptIn(ExperimentalMaterial3Api::class) @Composable -internal fun SearchContent( +internal fun BookSearchContent( state: BookSearchUiState, modifier: Modifier = Modifier, ) { @@ -282,9 +282,9 @@ internal fun SearchContent( @DevicePreview @Composable -private fun SearchPreview() { +private fun BookSearchPreview() { ReedTheme { - SearchUi( + BookSearchUi( state = BookSearchUiState( eventSink = {}, ), From 0655dbf1c6b817f54816af12261d15a5289454e7 Mon Sep 17 00:00:00 2001 From: easyhooon Date: Thu, 28 Aug 2025 15:51:25 +0900 Subject: [PATCH 15/42] =?UTF-8?q?[BOOK-298]=20feat:=20ReedTextButton=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/button/ButtonColorStyle.kt | 11 ++- .../component/button/ReedTextButton.kt | 93 +++++++++++++++++-- .../ninecraft/booket/feature/login/LoginUi.kt | 27 +++--- 3 files changed, 107 insertions(+), 24 deletions(-) diff --git a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ButtonColorStyle.kt b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ButtonColorStyle.kt index 57db2455..701d16c3 100644 --- a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ButtonColorStyle.kt +++ b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ButtonColorStyle.kt @@ -2,12 +2,13 @@ package com.ninecraft.booket.core.designsystem.component.button import androidx.compose.foundation.BorderStroke import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.ninecraft.booket.core.designsystem.theme.Kakao import com.ninecraft.booket.core.designsystem.theme.ReedTheme enum class ReedButtonColorStyle { - PRIMARY, SECONDARY, TERTIARY, STROKE, KAKAO; + PRIMARY, SECONDARY, TERTIARY, STROKE, TEXT, KAKAO; @Composable fun containerColor(isPressed: Boolean) = when (this) { @@ -15,7 +16,9 @@ enum class ReedButtonColorStyle { SECONDARY -> if (isPressed) ReedTheme.colors.bgSecondaryPressed else ReedTheme.colors.bgSecondary TERTIARY -> if (isPressed) ReedTheme.colors.bgTertiaryPressed else ReedTheme.colors.bgTertiary STROKE -> if (isPressed) ReedTheme.colors.basePrimary else ReedTheme.colors.basePrimary + TEXT -> Color.Transparent KAKAO -> Kakao + } @Composable @@ -24,11 +27,15 @@ enum class ReedButtonColorStyle { SECONDARY -> ReedTheme.colors.contentPrimary TERTIARY -> ReedTheme.colors.contentBrand STROKE -> ReedTheme.colors.contentBrand + TEXT -> ReedTheme.colors.borderBrand KAKAO -> ReedTheme.colors.contentPrimary } @Composable - fun disabledContainerColor() = ReedTheme.colors.bgDisabled + fun disabledContainerColor() = when (this) { + TEXT -> Color.Transparent + else -> ReedTheme.colors.bgDisabled + } @Composable fun disabledContentColor() = ReedTheme.colors.contentDisabled diff --git a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ReedTextButton.kt b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ReedTextButton.kt index 952d70bf..ed950074 100644 --- a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ReedTextButton.kt +++ b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ReedTextButton.kt @@ -1,22 +1,103 @@ package com.ninecraft.booket.core.designsystem.component.button -import androidx.compose.material3.Button +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.ninecraft.booket.core.common.utils.MultipleEventsCutter +import com.ninecraft.booket.core.common.utils.get +import com.ninecraft.booket.core.designsystem.ComponentPreview @Composable fun ReedTextButton( onClick: () -> Unit, + text: String, + sizeStyle: ButtonSizeStyle, + colorStyle: ReedButtonColorStyle, modifier: Modifier = Modifier, enabled: Boolean = true, - colorStyle: ReedButtonColorStyle = ReedButtonColorStyle.PRIMARY, - content: @Composable () -> Unit, + multipleEventsCutterEnabled: Boolean = true, ) { - Button( - onClick = onClick, + val multipleEventsCutter = remember { MultipleEventsCutter.get() } + + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + + TextButton( + onClick = { + if (multipleEventsCutterEnabled) { + multipleEventsCutter.processEvent { onClick() } + } else { + onClick() + } + }, modifier = modifier, enabled = enabled, + colors = ButtonDefaults.buttonColors( + containerColor = colorStyle.containerColor(isPressed), + contentColor = colorStyle.contentColor(), + disabledContentColor = colorStyle.disabledContentColor(), + disabledContainerColor = colorStyle.disabledContainerColor(), + ), + contentPadding = sizeStyle.paddingValues, + ) { + Column( + modifier = Modifier.width(IntrinsicSize.Max), + verticalArrangement = Arrangement.spacedBy(1.dp), + ) { + Text( + text = text, + style = sizeStyle.textStyle.copy( + color = if (enabled) colorStyle.contentColor() else colorStyle.disabledContentColor(), + ), + ) + HorizontalDivider( + thickness = 1.dp, + color = if (enabled) colorStyle.contentColor() else Color.Transparent, + ) + } + } +} + +@ComponentPreview +@Composable +private fun ReedTextButtonPreview() { + Column( + modifier = Modifier.padding(20.dp), + verticalArrangement = Arrangement.spacedBy(20.dp), ) { - content() + FlowRow( + horizontalArrangement = Arrangement.spacedBy(20.dp), + verticalArrangement = Arrangement.spacedBy(20.dp), + ) { + ReedTextButton( + onClick = {}, + colorStyle = ReedButtonColorStyle.TEXT, + sizeStyle = largeButtonStyle, + text = "text button", + ) + + ReedTextButton( + onClick = {}, + enabled = false, + colorStyle = ReedButtonColorStyle.TEXT, + sizeStyle = largeButtonStyle, + text = "text button", + ) + } } } diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt index 20c5150d..bec42e04 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt @@ -26,6 +26,7 @@ import com.ninecraft.booket.core.designsystem.component.button.ReedButton import com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle import com.ninecraft.booket.core.designsystem.component.button.ReedTextButton import com.ninecraft.booket.core.designsystem.component.button.largeButtonStyle +import com.ninecraft.booket.core.designsystem.component.button.smallButtonStyle import com.ninecraft.booket.core.designsystem.theme.ReedTheme import com.ninecraft.booket.core.designsystem.theme.White import com.ninecraft.booket.core.ui.ReedScaffold @@ -62,7 +63,7 @@ internal fun LoginUi( ReedCloseTopAppBar( onClose = { state.eventSink(LoginUiEvent.OnCloseButtonClick) - } + }, ) Column( modifier = Modifier @@ -83,7 +84,10 @@ internal fun LoginUi( style = ReedTheme.typography.headline2SemiBold, ) } - Column { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { ReedButton( onClick = { state.eventSink(LoginUiEvent.OnKakaoLoginButtonClick) @@ -105,25 +109,16 @@ internal fun LoginUi( ) }, ) - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing4)) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) if (state.returnToScreen == null) { ReedTextButton( onClick = { state.eventSink(LoginUiEvent.OnGuestLoginButtonClick) }, - modifier = Modifier - .fillMaxWidth() - .padding( - start = ReedTheme.spacing.spacing5, - end = ReedTheme.spacing.spacing5, - ), - ) { - Text( - text = stringResource(id = R.string.guest_login), - color = ReedTheme.colors.contentSecondary, - style = ReedTheme.typography.body1Medium, - ) - } + text = stringResource(R.string.guest_login), + sizeStyle = smallButtonStyle, + colorStyle = ReedButtonColorStyle.TEXT, + ) } } } From d107b0afcc9fad2b1204dc69cfd6c725a8a012be Mon Sep 17 00:00:00 2001 From: easyhooon Date: Thu, 28 Aug 2025 15:58:06 +0900 Subject: [PATCH 16/42] =?UTF-8?q?[BOOK-298]=20chore:=20Login=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20ReedCloseAppBar=20visible=20=EB=B6=84=EA=B8=B0=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ninecraft/booket/feature/login/LoginPresenter.kt | 2 -- .../com/ninecraft/booket/feature/login/LoginUi.kt | 12 +++++++----- .../ninecraft/booket/feature/login/LoginUiState.kt | 1 - .../termsagreement/TermsAgreementPresenter.kt | 11 +---------- .../feature/termsagreement/TermsAgreementUiState.kt | 1 - 5 files changed, 8 insertions(+), 19 deletions(-) diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt index dd634e05..0229b678 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt @@ -42,7 +42,6 @@ class LoginPresenter @AssistedInject constructor( @Composable override fun present(): LoginUiState { val scope = rememberCoroutineScope() - val userState by authRepository.userState.collectAsRetainedState(initial = UserState.Guest) var isLoading by rememberRetained { mutableStateOf(false) } var sideEffect by rememberRetained { mutableStateOf(null) } @@ -117,7 +116,6 @@ class LoginPresenter @AssistedInject constructor( return LoginUiState( isLoading = isLoading, - isGuestMode = userState is UserState.Guest, returnToScreen = screen.returnToScreen, sideEffect = sideEffect, eventSink = ::handleEvent, diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt index bec42e04..cb5dd828 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt @@ -60,11 +60,13 @@ internal fun LoginUi( ) { Box(modifier = modifier.fillMaxSize()) { Column { - ReedCloseTopAppBar( - onClose = { - state.eventSink(LoginUiEvent.OnCloseButtonClick) - }, - ) + if (state.returnToScreen != null) { + ReedCloseTopAppBar( + onClose = { + state.eventSink(LoginUiEvent.OnCloseButtonClick) + }, + ) + } Column( modifier = Modifier .fillMaxWidth() diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUiState.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUiState.kt index 879edc95..731e2bcb 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUiState.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUiState.kt @@ -8,7 +8,6 @@ import java.util.UUID data class LoginUiState( val isLoading: Boolean = false, - val isGuestMode: Boolean = false, val returnToScreen: Screen? = null, val sideEffect: LoginSideEffect? = null, val eventSink: (LoginUiEvent) -> Unit, diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/TermsAgreementPresenter.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/TermsAgreementPresenter.kt index b5e6a889..a1f4cf04 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/TermsAgreementPresenter.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/TermsAgreementPresenter.kt @@ -9,22 +9,16 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import com.ninecraft.booket.core.common.analytics.AnalyticsHelper import com.ninecraft.booket.core.common.constants.WebViewConstants -import com.ninecraft.booket.core.data.api.repository.AuthRepository import com.ninecraft.booket.core.data.api.repository.UserRepository -import com.ninecraft.booket.core.model.UserState import com.ninecraft.booket.feature.screens.HomeScreen -import com.ninecraft.booket.feature.screens.LoginScreen import com.ninecraft.booket.feature.screens.TermsAgreementScreen import com.ninecraft.booket.feature.screens.WebViewScreen -import com.ninecraft.booket.feature.screens.extensions.popUntilOrGoTo import com.orhanobut.logger.Logger import com.slack.circuit.codegen.annotations.CircuitInject -import com.slack.circuit.retained.collectAsRetainedState import com.slack.circuit.retained.rememberRetained import com.slack.circuit.runtime.Navigator import com.slack.circuit.runtime.popUntil import com.slack.circuit.runtime.presenter.Presenter -import com.slack.circuit.runtime.screen.Screen import com.slack.circuitx.effects.ImpressionEffect import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -38,14 +32,12 @@ class TermsAgreementPresenter @AssistedInject constructor( @Assisted private val screen: TermsAgreementScreen, @Assisted private val navigator: Navigator, private val userRepository: UserRepository, - private val authRepository: AuthRepository, private val analyticsHelper: AnalyticsHelper, ) : Presenter { @Composable override fun present(): TermsAgreementUiState { val scope = rememberCoroutineScope() - val userState by authRepository.userState.collectAsRetainedState(initial = UserState.Guest) var sideEffect by rememberRetained { mutableStateOf(null) } var agreedTerms by rememberRetained { @@ -83,7 +75,7 @@ class TermsAgreementPresenter @AssistedInject constructor( scope.launch { userRepository.agreeTerms(true) .onSuccess { - if (userState is UserState.Guest) { + if (screen.returnToScreen != null) { navigator.popUntil { it == screen.returnToScreen } } else { navigator.resetRoot(HomeScreen) @@ -106,7 +98,6 @@ class TermsAgreementPresenter @AssistedInject constructor( return TermsAgreementUiState( isAllAgreed = isAllAgreed, agreedTerms = agreedTerms, - isGuestMode = userState is UserState.Guest, eventSink = ::handleEvent, ) } diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/TermsAgreementUiState.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/TermsAgreementUiState.kt index ee73e0c0..64c72d49 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/TermsAgreementUiState.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/TermsAgreementUiState.kt @@ -9,7 +9,6 @@ import java.util.UUID data class TermsAgreementUiState( val isAllAgreed: Boolean, val agreedTerms: ImmutableList, - val isGuestMode: Boolean = false, val sideEffect: TermsAgreementSideEffect? = null, val eventSink: (TermsAgreementUiEvent) -> Unit, ) : CircuitUiState From bda573a53669a378af59797fed04806f477c7db8 Mon Sep 17 00:00:00 2001 From: easyhooon Date: Thu, 28 Aug 2025 16:17:28 +0900 Subject: [PATCH 17/42] [BOOK-298] chore: code style check success --- .../booket/core/data/api/repository/AuthRepository.kt | 4 ++-- .../core/data/impl/repository/DefaultAuthRepository.kt | 4 ++-- .../designsystem/component/button/ButtonColorStyle.kt | 1 - .../core/network/response/GuestBookSearchResponse.kt | 1 - .../com/ninecraft/booket/feature/library/LibraryUi.kt | 3 --- .../ninecraft/booket/feature/library/LibraryUiState.kt | 3 ++- .../ninecraft/booket/feature/login/LoginPresenter.kt | 3 --- .../booket/feature/screens/component/MainBottomBar.kt | 4 ---- .../booket/feature/screens/extensions/Navigator.kt | 10 ---------- 9 files changed, 6 insertions(+), 27 deletions(-) diff --git a/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/AuthRepository.kt b/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/AuthRepository.kt index c354e076..c8943668 100644 --- a/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/AuthRepository.kt +++ b/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/AuthRepository.kt @@ -12,8 +12,8 @@ interface AuthRepository { suspend fun withdraw(): Result val autoLoginState: Flow - + val userState: Flow - + suspend fun getCurrentUserState(): UserState } diff --git a/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultAuthRepository.kt b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultAuthRepository.kt index 1f10c20f..9e560650 100644 --- a/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultAuthRepository.kt +++ b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultAuthRepository.kt @@ -51,12 +51,12 @@ internal class DefaultAuthRepository @Inject constructor( .map { accessToken -> if (accessToken.isBlank()) AutoLoginState.NOT_LOGGED_IN else AutoLoginState.LOGGED_IN } - + override val userState = tokenDataSource.accessToken .map { accessToken -> if (accessToken.isBlank()) UserState.Guest else UserState.LoggedIn } - + override suspend fun getCurrentUserState(): UserState { val accessToken = tokenDataSource.getAccessToken() return if (accessToken.isBlank()) UserState.Guest else UserState.LoggedIn diff --git a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ButtonColorStyle.kt b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ButtonColorStyle.kt index 701d16c3..b4ddfb90 100644 --- a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ButtonColorStyle.kt +++ b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ButtonColorStyle.kt @@ -18,7 +18,6 @@ enum class ReedButtonColorStyle { STROKE -> if (isPressed) ReedTheme.colors.basePrimary else ReedTheme.colors.basePrimary TEXT -> Color.Transparent KAKAO -> Kakao - } @Composable diff --git a/core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/GuestBookSearchResponse.kt b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/GuestBookSearchResponse.kt index e7c9cfcc..eb38bfe5 100644 --- a/core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/GuestBookSearchResponse.kt +++ b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/GuestBookSearchResponse.kt @@ -44,4 +44,3 @@ data class GuestBookSummary( @SerialName("link") val link: String, ) - diff --git a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUi.kt b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUi.kt index 3fa3a576..48f5c4fc 100644 --- a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUi.kt +++ b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUi.kt @@ -1,7 +1,5 @@ package com.ninecraft.booket.feature.library -import android.R.attr.onClick -import android.R.id.message import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -19,7 +17,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import com.ninecraft.booket.core.common.utils.isNetworkError import com.ninecraft.booket.core.designsystem.DevicePreview import com.ninecraft.booket.core.designsystem.component.button.ReedButton import com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle diff --git a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUiState.kt b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUiState.kt index 213ef73d..cac40f41 100644 --- a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUiState.kt +++ b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUiState.kt @@ -48,11 +48,12 @@ sealed interface LibraryUiEvent : CircuitUiEvent { val userBookId: String, val isbn13: String, ) : LibraryUiEvent + data object OnLoadMore : LibraryUiEvent data object OnRetryClick : LibraryUiEvent data class OnFilterClick(val filterOption: LibraryFilterOption) : LibraryUiEvent data class OnTabSelected(val tab: MainTab) : LibraryUiEvent - data object OnLoginClick: LibraryUiEvent + data object OnLoginClick : LibraryUiEvent } data class LibraryFilterChip( diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt index 0229b678..d0a4ee03 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt @@ -8,14 +8,11 @@ import androidx.compose.runtime.setValue import com.ninecraft.booket.core.common.analytics.AnalyticsHelper import com.ninecraft.booket.core.data.api.repository.AuthRepository import com.ninecraft.booket.core.data.api.repository.UserRepository -import com.ninecraft.booket.core.model.UserState import com.ninecraft.booket.feature.screens.HomeScreen import com.ninecraft.booket.feature.screens.LoginScreen import com.ninecraft.booket.feature.screens.TermsAgreementScreen -import com.ninecraft.booket.feature.screens.extensions.popUntilOrGoTo import com.orhanobut.logger.Logger import com.slack.circuit.codegen.annotations.CircuitInject -import com.slack.circuit.retained.collectAsRetainedState import com.slack.circuit.retained.rememberRetained import com.slack.circuit.runtime.Navigator import com.slack.circuit.runtime.popUntil diff --git a/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/component/MainBottomBar.kt b/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/component/MainBottomBar.kt index aa29f1e5..e4195a0a 100644 --- a/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/component/MainBottomBar.kt +++ b/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/component/MainBottomBar.kt @@ -30,10 +30,6 @@ import com.adamglin.composeshadow.dropShadow import com.ninecraft.booket.core.designsystem.ComponentPreview import com.ninecraft.booket.core.designsystem.theme.ReedTheme import com.ninecraft.booket.core.designsystem.theme.White -import com.slack.circuit.backstack.SaveableBackStack -import com.slack.circuit.runtime.Navigator -import com.slack.circuit.runtime.popUntil -import com.slack.circuit.runtime.screen.Screen import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList diff --git a/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/extensions/Navigator.kt b/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/extensions/Navigator.kt index b6437b1b..711f4a6e 100644 --- a/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/extensions/Navigator.kt +++ b/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/extensions/Navigator.kt @@ -3,7 +3,6 @@ package com.ninecraft.booket.feature.screens.extensions import com.ninecraft.booket.feature.screens.LoginScreen import com.ninecraft.booket.feature.screens.ReedScreen import com.slack.circuit.runtime.Navigator -import com.slack.circuit.runtime.popUntil import com.slack.circuit.runtime.screen.Screen import kotlinx.coroutines.delay @@ -17,17 +16,8 @@ suspend fun Navigator.delayedPop(delayMillis: Long = 200L) { pop() } -fun Navigator.popUntilOrGoTo(screen: Screen) { - if (screen in peekBackStack()) { - popUntil { it == screen } - } else { - goTo(screen) - } -} - suspend fun Navigator.redirectToLogin(): Screen? { val currentScreen = peek() delayedGoTo(LoginScreen(currentScreen)) return currentScreen } - From e081cd5d84ea6f8d306a1a8394a85a2d98443979 Mon Sep 17 00:00:00 2001 From: easyhooon Date: Thu, 28 Aug 2025 16:24:24 +0900 Subject: [PATCH 18/42] =?UTF-8?q?[BOOK-298]=20chore:=20ReedTextButton=20co?= =?UTF-8?q?lors=20ButtonDefaults.textButtonColors=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit buttonColors -> textButtonColors --- .../booket/core/designsystem/component/button/ReedTextButton.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ReedTextButton.kt b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ReedTextButton.kt index ed950074..c1aa1470 100644 --- a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ReedTextButton.kt +++ b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ReedTextButton.kt @@ -47,7 +47,7 @@ fun ReedTextButton( }, modifier = modifier, enabled = enabled, - colors = ButtonDefaults.buttonColors( + colors = ButtonDefaults.textButtonColors( containerColor = colorStyle.containerColor(isPressed), contentColor = colorStyle.contentColor(), disabledContentColor = colorStyle.disabledContentColor(), From 6c87639e86587ab478a2219f0e74a3abb957fa78 Mon Sep 17 00:00:00 2001 From: easyhooon Date: Thu, 28 Aug 2025 16:38:29 +0900 Subject: [PATCH 19/42] =?UTF-8?q?[BOOK-298]=20fix:=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=95=84=EC=9B=83/=ED=9A=8C=EC=9B=90=ED=83=88=ED=87=B4=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EC=A0=84=ED=99=98=20=EC=9D=B4=EC=A0=84?= =?UTF-8?q?=EC=97=90=20=EB=8B=A4=EC=9D=B4=EC=96=BC=EB=A1=9C=EA=B7=B8/?= =?UTF-8?q?=EB=B0=94=ED=85=80=EC=8B=9C=ED=8A=B8=EA=B0=80=20=EB=8B=AB?= =?UTF-8?q?=ED=9E=88=EB=8F=84=EB=A1=9D=20flag=20=ED=98=B8=EC=B6=9C=20?= =?UTF-8?q?=EC=88=9C=EC=84=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ninecraft/booket/feature/settings/SettingsPresenter.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsPresenter.kt b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsPresenter.kt index f2ab6ac5..007b1aa0 100644 --- a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsPresenter.kt +++ b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsPresenter.kt @@ -80,7 +80,6 @@ class SettingsPresenter @AssistedInject constructor( } } finally { isLoading = false - isLogoutDialogVisible = false } } } @@ -110,7 +109,6 @@ class SettingsPresenter @AssistedInject constructor( } } finally { isLoading = false - isWithdrawBottomSheetVisible = false } } } @@ -184,10 +182,12 @@ class SettingsPresenter @AssistedInject constructor( } is SettingsUiEvent.Logout -> { + isLogoutDialogVisible = false logout() } is SettingsUiEvent.Withdraw -> { + isWithdrawBottomSheetVisible = false withdraw() } From 84167329c11d229402816a5b4a1dddab30188d03 Mon Sep 17 00:00:00 2001 From: easyhooon Date: Thu, 28 Aug 2025 16:55:01 +0900 Subject: [PATCH 20/42] =?UTF-8?q?[BOOK-298]=20refactor:=20BookRegisteredSt?= =?UTF-8?q?ate=20enum=20class,=20BookSummaryModel=20=EB=82=B4=20Computed?= =?UTF-8?q?=20Property=20=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ninecraft/booket/core/model/BookSearchModel.kt | 9 ++++++++- .../booket/feature/search/book/BookSearchUi.kt | 2 +- .../booket/feature/search/book/BookSearchUiState.kt | 11 ----------- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/core/model/src/main/kotlin/com/ninecraft/booket/core/model/BookSearchModel.kt b/core/model/src/main/kotlin/com/ninecraft/booket/core/model/BookSearchModel.kt index cc592841..f1c8b05c 100644 --- a/core/model/src/main/kotlin/com/ninecraft/booket/core/model/BookSearchModel.kt +++ b/core/model/src/main/kotlin/com/ninecraft/booket/core/model/BookSearchModel.kt @@ -27,4 +27,11 @@ data class BookSummaryModel( val link: String = "", val userBookStatus: String = "", val key: String = "", -) +) { + val isRegistered: Boolean + get() = userBookStatus != BEFORE_REGISTRATION + + companion object { + const val BEFORE_REGISTRATION = "BEFORE_REGISTRATION" + } +} diff --git a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUi.kt b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUi.kt index c1cef523..82edef82 100644 --- a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUi.kt +++ b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUi.kt @@ -222,7 +222,7 @@ internal fun BookSearchContent( onBookClick = { book -> state.eventSink(BookSearchUiEvent.OnBookClick(book.isbn13)) }, - enabled = BookRegisteredState.from(state.books[index].userBookStatus) == BookRegisteredState.BEFORE_REGISTRATION, + enabled = !state.books[index].isRegistered, ) HorizontalDivider( modifier = Modifier.fillMaxWidth(), diff --git a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUiState.kt b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUiState.kt index 1dc6894a..09641b05 100644 --- a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUiState.kt +++ b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUiState.kt @@ -64,14 +64,3 @@ sealed interface BookSearchUiEvent : CircuitUiEvent { data object OnBookRegisterSuccessOkButtonClick : BookSearchUiEvent data object OnBookRegisterSuccessCancelButtonClick : BookSearchUiEvent } - -enum class BookRegisteredState(val value: String) { - BEFORE_REGISTRATION("BEFORE_REGISTRATION"), - ; - - companion object { - fun from(value: String?): BookRegisteredState? { - return entries.find { it.value == value } - } - } -} From 38edf2c7c9d270af242acc6cbc3e1b84b82be4e0 Mon Sep 17 00:00:00 2001 From: easyhooon Date: Thu, 28 Aug 2025 17:00:18 +0900 Subject: [PATCH 21/42] =?UTF-8?q?[BOOK-298]=20chore:=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=ED=99=94=EB=A9=B4=20=EB=A6=AC=EB=8B=A4=EC=9D=B4?= =?UTF-8?q?=EB=A0=89=ED=8A=B8=EB=A1=9C=20=EC=A7=84=EC=9E=85=EC=8B=9C=20?= =?UTF-8?q?=EB=88=84=EB=9D=BD=EB=90=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=20=ED=95=98=EB=8B=B9=20=ED=8C=A8=EB=94=A9=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ninecraft/booket/feature/login/LoginUi.kt | 67 +++++++++++++------ 1 file changed, 45 insertions(+), 22 deletions(-) diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt index cb5dd828..ebe42156 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt @@ -90,29 +90,29 @@ internal fun LoginUi( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, ) { - ReedButton( - onClick = { - state.eventSink(LoginUiEvent.OnKakaoLoginButtonClick) - }, - sizeStyle = largeButtonStyle, - colorStyle = ReedButtonColorStyle.KAKAO, - modifier = Modifier - .fillMaxWidth() - .padding( - start = ReedTheme.spacing.spacing5, - end = ReedTheme.spacing.spacing5, - ), - text = stringResource(id = R.string.kakao_login), - leadingIcon = { - Icon( - imageVector = ImageVector.vectorResource(id = R.drawable.ic_kakao), - contentDescription = "Kakao Icon", - tint = Color.Unspecified, - ) - }, - ) - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) if (state.returnToScreen == null) { + ReedButton( + onClick = { + state.eventSink(LoginUiEvent.OnKakaoLoginButtonClick) + }, + sizeStyle = largeButtonStyle, + colorStyle = ReedButtonColorStyle.KAKAO, + modifier = Modifier + .fillMaxWidth() + .padding( + start = ReedTheme.spacing.spacing5, + end = ReedTheme.spacing.spacing5, + ), + text = stringResource(id = R.string.kakao_login), + leadingIcon = { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_kakao), + contentDescription = "Kakao Icon", + tint = Color.Unspecified, + ) + }, + ) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) ReedTextButton( onClick = { state.eventSink(LoginUiEvent.OnGuestLoginButtonClick) @@ -121,6 +121,29 @@ internal fun LoginUi( sizeStyle = smallButtonStyle, colorStyle = ReedButtonColorStyle.TEXT, ) + } else { + ReedButton( + onClick = { + state.eventSink(LoginUiEvent.OnKakaoLoginButtonClick) + }, + sizeStyle = largeButtonStyle, + colorStyle = ReedButtonColorStyle.KAKAO, + modifier = Modifier + .fillMaxWidth() + .padding( + start = ReedTheme.spacing.spacing5, + end = ReedTheme.spacing.spacing5, + ), + text = stringResource(id = R.string.kakao_login), + leadingIcon = { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_kakao), + contentDescription = "Kakao Icon", + tint = Color.Unspecified, + ) + }, + ) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing8)) } } } From 74f4d915850de0c4a732af8d5081a74a786dc135 Mon Sep 17 00:00:00 2001 From: easyhooon Date: Thu, 28 Aug 2025 21:13:17 +0900 Subject: [PATCH 22/42] =?UTF-8?q?[BOOK-298]=20chore:=20=ED=86=A0=EB=81=BC?= =?UTF-8?q?=20=EB=A6=AC=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ninecraft/booket/feature/login/LoginUi.kt | 71 +++++++------------ .../booket/feature/settings/SettingsUi.kt | 2 +- .../settings/src/main/res/values/strings.xml | 2 +- 3 files changed, 27 insertions(+), 48 deletions(-) diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt index ebe42156..359989bf 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt @@ -58,7 +58,7 @@ internal fun LoginUi( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { - Box(modifier = modifier.fillMaxSize()) { + Box(modifier = Modifier.fillMaxSize()) { Column { if (state.returnToScreen != null) { ReedCloseTopAppBar( @@ -90,29 +90,31 @@ internal fun LoginUi( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, ) { + ReedButton( + onClick = { + state.eventSink(LoginUiEvent.OnKakaoLoginButtonClick) + }, + sizeStyle = largeButtonStyle, + colorStyle = ReedButtonColorStyle.KAKAO, + modifier = Modifier + .fillMaxWidth() + .padding( + start = ReedTheme.spacing.spacing5, + end = ReedTheme.spacing.spacing5, + ), + text = stringResource(id = R.string.kakao_login), + leadingIcon = { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_kakao), + contentDescription = "Kakao Icon", + tint = Color.Unspecified, + ) + }, + ) + Spacer( + modifier = Modifier.height(if (state.returnToScreen == null) ReedTheme.spacing.spacing2 else ReedTheme.spacing.spacing8), + ) if (state.returnToScreen == null) { - ReedButton( - onClick = { - state.eventSink(LoginUiEvent.OnKakaoLoginButtonClick) - }, - sizeStyle = largeButtonStyle, - colorStyle = ReedButtonColorStyle.KAKAO, - modifier = Modifier - .fillMaxWidth() - .padding( - start = ReedTheme.spacing.spacing5, - end = ReedTheme.spacing.spacing5, - ), - text = stringResource(id = R.string.kakao_login), - leadingIcon = { - Icon( - imageVector = ImageVector.vectorResource(id = R.drawable.ic_kakao), - contentDescription = "Kakao Icon", - tint = Color.Unspecified, - ) - }, - ) - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) ReedTextButton( onClick = { state.eventSink(LoginUiEvent.OnGuestLoginButtonClick) @@ -121,29 +123,6 @@ internal fun LoginUi( sizeStyle = smallButtonStyle, colorStyle = ReedButtonColorStyle.TEXT, ) - } else { - ReedButton( - onClick = { - state.eventSink(LoginUiEvent.OnKakaoLoginButtonClick) - }, - sizeStyle = largeButtonStyle, - colorStyle = ReedButtonColorStyle.KAKAO, - modifier = Modifier - .fillMaxWidth() - .padding( - start = ReedTheme.spacing.spacing5, - end = ReedTheme.spacing.spacing5, - ), - text = stringResource(id = R.string.kakao_login), - leadingIcon = { - Icon( - imageVector = ImageVector.vectorResource(id = R.drawable.ic_kakao), - contentDescription = "Kakao Icon", - tint = Color.Unspecified, - ) - }, - ) - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing8)) } } } diff --git a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsUi.kt b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsUi.kt index 376516dd..b5989fdd 100644 --- a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsUi.kt +++ b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsUi.kt @@ -152,7 +152,7 @@ internal fun SettingsUi( ReedDivider(modifier = Modifier.padding(vertical = ReedTheme.spacing.spacing4)) if (state.isGuestMode) { SettingItem( - title = stringResource(R.string.login), + title = stringResource(R.string.settings_login), onItemClick = { state.eventSink(SettingsUiEvent.OnLoginClick) }, diff --git a/feature/settings/src/main/res/values/strings.xml b/feature/settings/src/main/res/values/strings.xml index 1cd987bd..7051c68a 100644 --- a/feature/settings/src/main/res/values/strings.xml +++ b/feature/settings/src/main/res/values/strings.xml @@ -18,5 +18,5 @@ 최신 버전이 출시되었습니다. 최적의 사용 환경을 위해 업데이트해주세요. 업데이트하기 - 로그인 + 로그인 From e7ece1c5aa59916e81f792bbfec7b4b89ffdaf3d Mon Sep 17 00:00:00 2001 From: easyhooon Date: Thu, 28 Aug 2025 21:23:17 +0900 Subject: [PATCH 23/42] =?UTF-8?q?[BOOK-298]=20refactor:=20Guest=20Mode=20?= =?UTF-8?q?=ED=99=88/=EB=82=B4=20=EC=84=9C=EC=9E=AC=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=20API=20=ED=98=B8=EC=B6=9C=EB=90=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8A=94=20=EB=B0=A9=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../booket/feature/home/HomePresenter.kt | 6 +- .../ninecraft/booket/feature/home/HomeUi.kt | 136 +++++++++--------- .../feature/library/LibraryPresenter.kt | 12 +- .../booket/feature/library/LibraryUi.kt | 136 +++++++++--------- 4 files changed, 148 insertions(+), 142 deletions(-) diff --git a/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomePresenter.kt b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomePresenter.kt index 777df64a..175b1804 100644 --- a/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomePresenter.kt +++ b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomePresenter.kt @@ -92,8 +92,10 @@ class HomePresenter @AssistedInject constructor( } } - RememberedEffect(true) { - loadHomeContent() + RememberedEffect(userState) { + if (userState !is UserState.Guest) { + loadHomeContent() + } } ImpressionEffect { diff --git a/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUi.kt b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUi.kt index 70b053e3..91e9e13c 100644 --- a/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUi.kt +++ b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUi.kt @@ -94,13 +94,8 @@ internal fun HomeContent( .fillMaxSize() .background(ReedTheme.colors.baseSecondary), ) { - when (state.uiState) { - is UiState.Idle -> {} - is UiState.Loading -> { - ReedLoadingIndicator() - } - - is UiState.Success -> { + if (state.isGuestMode) { + if (state.isGuestMode) { Column( modifier = modifier .fillMaxSize() @@ -114,63 +109,23 @@ internal fun HomeContent( style = ReedTheme.typography.headline2Medium, ) Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing3)) - - if (state.recentBooks.isEmpty()) { - EmptyBookCard( - onBookRegisterClick = { - state.eventSink(HomeUiEvent.OnBookRegisterClick) - }, - modifier = Modifier.padding(horizontal = ReedTheme.spacing.spacing5), - ) - } else { - val pagerState = rememberPagerState(pageCount = { state.recentBooks.size }) - - HorizontalPager( - state = pagerState, - modifier = Modifier.fillMaxWidth(), - contentPadding = PaddingValues(horizontal = ReedTheme.spacing.spacing5), - pageSpacing = ReedTheme.spacing.spacing5, - ) { page -> - BookCard( - recentBookInfo = state.recentBooks[page], - onBookDetailClick = { - state.eventSink( - HomeUiEvent.OnBookDetailClick( - state.recentBooks[page].userBookId, - state.recentBooks[page].isbn13, - ), - ) - }, - onRecordButtonClick = { - state.eventSink(HomeUiEvent.OnRecordButtonClick(state.recentBooks[page].userBookId)) - }, - ) - } - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing5)) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center, - ) { - repeat(pagerState.pageCount) { iteration -> - val color = - if (pagerState.currentPage == iteration) ReedTheme.colors.bgPrimary else ReedTheme.colors.bgSecondaryPressed - Box( - modifier = Modifier - .size(12.dp) - .padding(3.dp) - .clip(CircleShape) - .background(color), - ) - } - } - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing7)) - } + EmptyBookCard( + onBookRegisterClick = { + state.eventSink(HomeUiEvent.OnBookRegisterClick) + }, + modifier = Modifier.padding(horizontal = ReedTheme.spacing.spacing5), + ) Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing6)) } } + } else { + when (state.uiState) { + is UiState.Idle -> {} + is UiState.Loading -> { + ReedLoadingIndicator() + } - is UiState.Error -> { - if (state.isGuestMode) { + is UiState.Success -> { Column( modifier = modifier .fillMaxSize() @@ -184,15 +139,62 @@ internal fun HomeContent( style = ReedTheme.typography.headline2Medium, ) Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing3)) - EmptyBookCard( - onBookRegisterClick = { - state.eventSink(HomeUiEvent.OnBookRegisterClick) - }, - modifier = Modifier.padding(horizontal = ReedTheme.spacing.spacing5), - ) + + if (state.recentBooks.isEmpty()) { + EmptyBookCard( + onBookRegisterClick = { + state.eventSink(HomeUiEvent.OnBookRegisterClick) + }, + modifier = Modifier.padding(horizontal = ReedTheme.spacing.spacing5), + ) + } else { + val pagerState = rememberPagerState(pageCount = { state.recentBooks.size }) + + HorizontalPager( + state = pagerState, + modifier = Modifier.fillMaxWidth(), + contentPadding = PaddingValues(horizontal = ReedTheme.spacing.spacing5), + pageSpacing = ReedTheme.spacing.spacing5, + ) { page -> + BookCard( + recentBookInfo = state.recentBooks[page], + onBookDetailClick = { + state.eventSink( + HomeUiEvent.OnBookDetailClick( + state.recentBooks[page].userBookId, + state.recentBooks[page].isbn13, + ), + ) + }, + onRecordButtonClick = { + state.eventSink(HomeUiEvent.OnRecordButtonClick(state.recentBooks[page].userBookId)) + }, + ) + } + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing5)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + ) { + repeat(pagerState.pageCount) { iteration -> + val color = + if (pagerState.currentPage == iteration) ReedTheme.colors.bgPrimary else ReedTheme.colors.bgSecondaryPressed + Box( + modifier = Modifier + .size(12.dp) + .padding(3.dp) + .clip(CircleShape) + .background(color), + ) + } + } + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing7)) + } Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing6)) } - } else { + } + + is UiState.Error -> { ReedErrorUi( exception = state.uiState.exception, onRetryClick = { state.eventSink(HomeUiEvent.OnRetryClick) }, diff --git a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt index 0ba138a5..ff8e2723 100644 --- a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt +++ b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt @@ -175,11 +175,13 @@ class LibraryPresenter @AssistedInject constructor( } RememberedEffect(Unit) { - filterLibraryBooks( - status = currentFilter.getApiValue(), - page = START_INDEX, - size = PAGE_SIZE, - ) + if (userState !is UserState.Guest) { + filterLibraryBooks( + status = currentFilter.getApiValue(), + page = START_INDEX, + size = PAGE_SIZE, + ) + } } ImpressionEffect { diff --git a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUi.kt b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUi.kt index 48f5c4fc..2ebbe212 100644 --- a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUi.kt +++ b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUi.kt @@ -103,81 +103,81 @@ internal fun LibraryContent( ) Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing1)) - when (state.uiState) { - is UiState.Idle -> { - EmptyResult() + if (state.isGuestMode) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = stringResource(R.string.library_login_required_title), + color = ReedTheme.colors.contentPrimary, + textAlign = TextAlign.Center, + style = ReedTheme.typography.headline1SemiBold, + ) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) + Text( + text = stringResource(R.string.library_login_required_description), + color = ReedTheme.colors.contentSecondary, + textAlign = TextAlign.Center, + style = ReedTheme.typography.body1Medium, + ) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing4)) + ReedButton( + onClick = { + state.eventSink(LibraryUiEvent.OnLoginClick) + }, + text = stringResource(R.string.login), + colorStyle = ReedButtonColorStyle.SECONDARY, + sizeStyle = mediumButtonStyle, + ) + } } + } else { + when (state.uiState) { + is UiState.Idle -> { + EmptyResult() + } - is UiState.Loading -> { - ReedLoadingIndicator() - } + is UiState.Loading -> { + ReedLoadingIndicator() + } - is UiState.Success -> { - if (state.books.isEmpty()) { - EmptyResult() - } else { - InfinityLazyColumn( - modifier = Modifier.fillMaxSize(), - loadMore = { - state.eventSink(LibraryUiEvent.OnLoadMore) - }, - ) { - items(state.books) { - LibraryBookItem( - book = it, - onBookClick = { - state.eventSink(LibraryUiEvent.OnBookClick(it.userBookId, it.isbn13)) - }, - ) - Box( - modifier = modifier - .fillMaxWidth() - .height(1.dp) - .background(ReedTheme.colors.borderPrimary), - ) - } - item { - LoadStateFooter( - footerState = state.footerState, - onRetryClick = { state.eventSink(LibraryUiEvent.OnLoadMore) }, - ) + is UiState.Success -> { + if (state.books.isEmpty()) { + EmptyResult() + } else { + InfinityLazyColumn( + modifier = Modifier.fillMaxSize(), + loadMore = { + state.eventSink(LibraryUiEvent.OnLoadMore) + }, + ) { + items(state.books) { + LibraryBookItem( + book = it, + onBookClick = { + state.eventSink(LibraryUiEvent.OnBookClick(it.userBookId, it.isbn13)) + }, + ) + Box( + modifier = modifier + .fillMaxWidth() + .height(1.dp) + .background(ReedTheme.colors.borderPrimary), + ) + } + item { + LoadStateFooter( + footerState = state.footerState, + onRetryClick = { state.eventSink(LibraryUiEvent.OnLoadMore) }, + ) + } } } } - } - is UiState.Error -> { - if (state.isGuestMode) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text( - text = stringResource(R.string.library_login_required_title), - color = ReedTheme.colors.contentPrimary, - textAlign = TextAlign.Center, - style = ReedTheme.typography.headline1SemiBold, - ) - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) - Text( - text = stringResource(R.string.library_login_required_description), - color = ReedTheme.colors.contentSecondary, - textAlign = TextAlign.Center, - style = ReedTheme.typography.body1Medium, - ) - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing4)) - ReedButton( - onClick = { - state.eventSink(LibraryUiEvent.OnLoginClick) - }, - text = stringResource(R.string.login), - colorStyle = ReedButtonColorStyle.SECONDARY, - sizeStyle = mediumButtonStyle, - ) - } - } - } else { + is UiState.Error -> { ReedErrorUi( exception = state.uiState.exception, onRetryClick = { state.eventSink(LibraryUiEvent.OnRetryClick) }, From 84ef4e54c5c4983a448cdd7430afa786308ba5a6 Mon Sep 17 00:00:00 2001 From: easyhooon Date: Thu, 28 Aug 2025 21:27:39 +0900 Subject: [PATCH 24/42] =?UTF-8?q?[BOOK-298]=20chore:=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=EC=9D=98=EC=A1=B4=EC=84=B1=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- feature/search/build.gradle.kts | 1 - 1 file changed, 1 deletion(-) diff --git a/feature/search/build.gradle.kts b/feature/search/build.gradle.kts index 3cbeb726..ff1f0354 100644 --- a/feature/search/build.gradle.kts +++ b/feature/search/build.gradle.kts @@ -17,7 +17,6 @@ ksp { dependencies { implementations( libs.kotlinx.collections.immutable, - libs.compose.system.ui.controller, libs.logger, ) From 94317d8ca0e87c8a839dc4e5473bf8be02a87998 Mon Sep 17 00:00:00 2001 From: easyhooon Date: Thu, 28 Aug 2025 22:10:37 +0900 Subject: [PATCH 25/42] =?UTF-8?q?[BOOK-298]=20fix:=20=EB=82=B4=20=EC=84=9C?= =?UTF-8?q?=EC=9E=AC=20=ED=99=94=EB=A9=B4=20rememberEffect=20key=20userSta?= =?UTF-8?q?te=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ninecraft/booket/feature/library/LibraryPresenter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt index ff8e2723..712d21d0 100644 --- a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt +++ b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt @@ -174,7 +174,7 @@ class LibraryPresenter @AssistedInject constructor( } } - RememberedEffect(Unit) { + RememberedEffect(userState) { if (userState !is UserState.Guest) { filterLibraryBooks( status = currentFilter.getApiValue(), From 74f49c2a1449eab4be5f7733d7bc4bdd1bb38602 Mon Sep 17 00:00:00 2001 From: easyhooon Date: Thu, 28 Aug 2025 22:34:40 +0900 Subject: [PATCH 26/42] =?UTF-8?q?[BOOK-298]=20chore:=20=EC=A4=91=EC=B2=A9?= =?UTF-8?q?=20=EC=A1=B0=EA=B1=B4=EB=AC=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 머쓱 --- .../ninecraft/booket/feature/home/HomeUi.kt | 42 +++++++++---------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUi.kt b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUi.kt index 91e9e13c..8307716e 100644 --- a/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUi.kt +++ b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUi.kt @@ -95,28 +95,26 @@ internal fun HomeContent( .background(ReedTheme.colors.baseSecondary), ) { if (state.isGuestMode) { - if (state.isGuestMode) { - Column( - modifier = modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()), - ) { - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing6)) - Text( - text = stringResource(R.string.home_content_label_reading_now), - modifier = Modifier.padding(start = ReedTheme.spacing.spacing5), - color = ReedTheme.colors.contentSecondary, - style = ReedTheme.typography.headline2Medium, - ) - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing3)) - EmptyBookCard( - onBookRegisterClick = { - state.eventSink(HomeUiEvent.OnBookRegisterClick) - }, - modifier = Modifier.padding(horizontal = ReedTheme.spacing.spacing5), - ) - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing6)) - } + Column( + modifier = modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), + ) { + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing6)) + Text( + text = stringResource(R.string.home_content_label_reading_now), + modifier = Modifier.padding(start = ReedTheme.spacing.spacing5), + color = ReedTheme.colors.contentSecondary, + style = ReedTheme.typography.headline2Medium, + ) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing3)) + EmptyBookCard( + onBookRegisterClick = { + state.eventSink(HomeUiEvent.OnBookRegisterClick) + }, + modifier = Modifier.padding(horizontal = ReedTheme.spacing.spacing5), + ) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing6)) } } else { when (state.uiState) { From 477bb27dabb6b2dbfd18f1f1320e39bacf733b3a Mon Sep 17 00:00:00 2001 From: JI HUN LEE <51016231+easyhooon@users.noreply.github.com> Date: Thu, 28 Aug 2025 23:11:23 +0900 Subject: [PATCH 27/42] Update README.md --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5a73577e..7443712f 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Reed(리드) - 문장과 감정을 함께 담는 독서 기록 [PlayStore](https - DataStore - StartUp - Splash - - Camera + - CameraX - Kotlin Libraries (Coroutine, Serialization, Immutable Collection) - Compose @@ -65,7 +65,7 @@ Reed(리드) - 문장과 감정을 함께 담는 독서 기록 [PlayStore](https - Dagger Hilt - Retrofit, OkHttp3 - Coil-Compose, [Landscapist](https://github.com/skydoves/landscapist) -- Google ML Kit +- ~~Google ML Kit~~ Google Cloud Vision - Lottie-Compose - Firebase(Analytics, Crashlytics, Remote Config) - Kakao-Auth @@ -106,8 +106,10 @@ Reed(리드) - 문장과 감정을 함께 담는 독서 기록 [PlayStore](https ├── build-logic ├── core │   ├── common -│   ├── data -│   ├── datastore +│   ├── data-api +│   ├── data-impl +│   ├── datastore-api +│   ├── datastore-impl │   ├── designsystem │   ├── model │   ├── network From 26825c5f90a490d4bc20dc0dd047805f7acdc643 Mon Sep 17 00:00:00 2001 From: easyhooon Date: Fri, 29 Aug 2025 00:52:04 +0900 Subject: [PATCH 28/42] =?UTF-8?q?[BOOK-298]=20chore:=20=EB=8F=84=EC=84=9C?= =?UTF-8?q?=20=EA=B2=80=EC=83=89=20=EA=B2=B0=EA=B3=BC=20=EC=97=86=EC=9D=8C?= =?UTF-8?q?=20=EB=A1=9C=EA=B7=B8=20=EC=88=98=EC=A7=91=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../booket/feature/search/book/BookSearchPresenter.kt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchPresenter.kt b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchPresenter.kt index 6745aa25..c473883a 100644 --- a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchPresenter.kt +++ b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchPresenter.kt @@ -18,9 +18,9 @@ import com.ninecraft.booket.core.model.BookSearchModel import com.ninecraft.booket.core.model.BookSummaryModel import com.ninecraft.booket.core.model.UserState import com.ninecraft.booket.core.ui.component.FooterState +import com.ninecraft.booket.feature.screens.BookSearchScreen import com.ninecraft.booket.feature.screens.LoginScreen import com.ninecraft.booket.feature.screens.RecordScreen -import com.ninecraft.booket.feature.screens.BookSearchScreen import com.ninecraft.booket.feature.screens.extensions.delayedGoTo import com.ninecraft.booket.feature.screens.extensions.redirectToLogin import com.ninecraft.booket.feature.search.R @@ -50,7 +50,6 @@ class BookSearchPresenter @AssistedInject constructor( companion object { private const val START_INDEX = 1 private const val SEARCH_BOOK_RESULT = "search_book_result" - private const val SEARCH_BOOK_NO_RESULT = "search_book_noresult" private const val ERROR_SEARCH_LOADING = "error_search_loading" private const val REGISTER_BOOK_OPTION = "register_book_option" private const val REGISTER_BOOK_COMPLETE = "register_book_complete" @@ -103,9 +102,6 @@ class BookSearchPresenter @AssistedInject constructor( if (startIndex == START_INDEX) { uiState = UiState.Success analyticsHelper.logEvent(SEARCH_BOOK_RESULT) - if (result.books.isEmpty()) { - analyticsHelper.logEvent(SEARCH_BOOK_NO_RESULT) - } } else { footerState = if (isLastPage) FooterState.End else FooterState.Idle } From ca25219cf31a9f96596aef23984cd5b5874dd62e Mon Sep 17 00:00:00 2001 From: easyhooon Date: Fri, 29 Aug 2025 00:52:55 +0900 Subject: [PATCH 29/42] [BOOK-298] chore: app version update versionName: 1.1.2 -> 1.2.0 versionCode: 6 -> 7 --- 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 92be2019..d42645a4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,8 +3,8 @@ minSdk = "28" targetSdk = "35" compileSdk = "35" -versionName = "1.1.2" -versionCode = "6" +versionName = "1.2.0" +versionCode = "7" packageName = "com.ninecraft.booket" ## Android gradle plugin From 9fdbfd52160571e3a65bc46f42560cfb39b0261a Mon Sep 17 00:00:00 2001 From: JI HUN LEE <51016231+easyhooon@users.noreply.github.com> Date: Fri, 29 Aug 2025 01:13:04 +0900 Subject: [PATCH 30/42] Update README.md --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7443712f..916f4873 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Reed(리드) - 문장과 감정을 함께 담는 독서 기록 [PlayStore](https - IDE : Android Studio 최신 버전 - JDK : Java 17을 실행할 수 있는 JDK - - (권장) Android Studio 설치 시 Embeded 된 JDK (Open JDK) + - (권장) Android Studio 설치 시 Embedded 된 JDK (Open JDK) - Java 17을 사용하는 JDK (Open JDK, AdoptOpenJDK, GraalVM) - Kotlin Language : 2.2.0 @@ -62,10 +62,9 @@ Reed(리드) - 문장과 감정을 함께 담는 독서 기록 [PlayStore](https - Material3 - [Circuit](https://github.com/slackhq/circuit) +- ~~Google ML Kit~~ Google Cloud Vision - Dagger Hilt - Retrofit, OkHttp3 -- Coil-Compose, [Landscapist](https://github.com/skydoves/landscapist) -- ~~Google ML Kit~~ Google Cloud Vision - Lottie-Compose - Firebase(Analytics, Crashlytics, Remote Config) - Kakao-Auth From 6e782bc0da1f7df5af47ea6174d2215dd6e841e4 Mon Sep 17 00:00:00 2001 From: seoyoon Date: Fri, 29 Aug 2025 18:18:09 +0900 Subject: [PATCH 31/42] =?UTF-8?q?[BOOK-304]=20feat:=20ReedLoadingIndicator?= =?UTF-8?q?=EC=97=90=20delay=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/ui/component/ReedLoadingIndicator.kt | 30 ++++++++++++++----- .../booket/feature/library/LibraryUi.kt | 1 - 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedLoadingIndicator.kt b/core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedLoadingIndicator.kt index 3adbc6b0..88c4a0a7 100644 --- a/core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedLoadingIndicator.kt +++ b/core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedLoadingIndicator.kt @@ -4,23 +4,39 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.CircularProgressIndicator 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.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.ninecraft.booket.core.common.extensions.noRippleClickable import com.ninecraft.booket.core.designsystem.ComponentPreview import com.ninecraft.booket.core.designsystem.theme.ReedTheme +import kotlinx.coroutines.delay @Composable fun ReedLoadingIndicator( modifier: Modifier = Modifier, + delayMillis: Long = 500L, ) { - Box( - modifier = modifier - .fillMaxSize() - .noRippleClickable {}, - contentAlignment = Alignment.Center, - ) { - CircularProgressIndicator(color = ReedTheme.colors.contentBrand) + var showProgressBar by remember { mutableStateOf(false) } + + LaunchedEffect(Unit) { + delay(delayMillis) + showProgressBar = true + } + + if (showProgressBar) { + Box( + modifier = modifier + .fillMaxSize() + .noRippleClickable {}, + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator(color = ReedTheme.colors.contentBrand) + } } } diff --git a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUi.kt b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUi.kt index ed1f0512..4711afd2 100644 --- a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUi.kt +++ b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUi.kt @@ -89,7 +89,6 @@ internal fun LibraryContent( Column( modifier = modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, ) { FilterChipGroup( filterList = state.filterChips, From 0388dd9b35d8d60d6fc80bbe43d5ed2593ab35f3 Mon Sep 17 00:00:00 2001 From: seoyoon Date: Fri, 29 Aug 2025 18:46:35 +0900 Subject: [PATCH 32/42] =?UTF-8?q?[BOOK-304]=20chore:=20MLKit=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=BD=94=EB=93=9C=20=EB=B0=8F=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/ocr/build.gradle.kts | 3 - .../core/ocr/analyzer/LiveTextAnalyzer.kt | 80 ------------------- .../core/ocr/analyzer/StillTextAnalyzer.kt | 75 ----------------- .../booket/core/ocr/analyzer/TextAnalyzer.kt | 7 -- .../ninecraft/booket/core/ocr/di/OcrModule.kt | 20 ----- .../CloudOcrRecognizer.kt | 2 +- .../booket/feature/record/ocr/OcrPresenter.kt | 2 +- gradle/libs.versions.toml | 6 -- 8 files changed, 2 insertions(+), 193 deletions(-) delete mode 100644 core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/analyzer/LiveTextAnalyzer.kt delete mode 100644 core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/analyzer/StillTextAnalyzer.kt delete mode 100644 core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/analyzer/TextAnalyzer.kt delete mode 100644 core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/di/OcrModule.kt rename core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/{analyzer => recognizer}/CloudOcrRecognizer.kt (97%) diff --git a/core/ocr/build.gradle.kts b/core/ocr/build.gradle.kts index 4d70a87e..8fcb5005 100644 --- a/core/ocr/build.gradle.kts +++ b/core/ocr/build.gradle.kts @@ -26,9 +26,6 @@ dependencies { projects.core.common, libs.logger, - libs.androidx.camera.core, - - libs.google.mlkit.text.recognition.korean, ) } diff --git a/core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/analyzer/LiveTextAnalyzer.kt b/core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/analyzer/LiveTextAnalyzer.kt deleted file mode 100644 index 0b5ab9b0..00000000 --- a/core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/analyzer/LiveTextAnalyzer.kt +++ /dev/null @@ -1,80 +0,0 @@ -package com.ninecraft.booket.core.ocr.analyzer - -import androidx.annotation.OptIn -import androidx.camera.core.ExperimentalGetImage -import androidx.camera.core.ImageProxy -import com.google.mlkit.vision.common.InputImage -import com.google.mlkit.vision.text.TextRecognizer -import com.orhanobut.logger.Logger -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine - -/** - * 실시간 카메라 스트림에서 프레임 단위로 텍스트 분석하는 Analyzer 클래스 - * - * ML Kit의 TextRecognizer를 사용하여 `ImageProxy` 객체로부터 텍스트를 추출한다 - * - * @param textRecognizer ML Kit의 TextRecognizer 인스턴스 - * @param onTextDetected 텍스트 인식 성공 시 호출되는 콜백 (인식된 전체 텍스트 전달) - * - * 안정적인 연속 프레임 분석을 위해 CoroutineScope에 [SupervisorJob]을 사용하여 - * 한 프레임 분석에서 예외가 발생해도 다음 프레임 분석에 영향을 주지 않도록 설계 - */ -class LiveTextAnalyzer @AssistedInject constructor( - private val textRecognizer: TextRecognizer, - @Assisted private val onTextDetected: (String) -> Unit, -) : TextAnalyzer { - - companion object { - const val THROTTLE_TIMEOUT_MS = 1_000L // 프레임 처리 간 인터벌 - } - - private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - - @OptIn(ExperimentalGetImage::class) - override fun analyze(imageProxy: ImageProxy) { - scope.launch { - val mediaImage = imageProxy.image ?: run { imageProxy.close(); return@launch } - val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) - - suspendCoroutine { continuation -> - textRecognizer.process(inputImage) - .addOnSuccessListener { visionText -> - onTextDetected(visionText.text) - } - .addOnFailureListener { exception -> - Logger.e(exception.message ?: "Unknown error") - } - .addOnCompleteListener { - continuation.resume(Unit) - } - } - delay(THROTTLE_TIMEOUT_MS) - }.invokeOnCompletion { exception -> - if (exception != null) { - Logger.e(exception.message ?: "Unknown error") - } - imageProxy.close() - } - } - - fun cancel() { - scope.cancel() - } - - @AssistedFactory - interface Factory { - fun create( - onTextDetected: (String) -> Unit, - ): LiveTextAnalyzer - } -} diff --git a/core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/analyzer/StillTextAnalyzer.kt b/core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/analyzer/StillTextAnalyzer.kt deleted file mode 100644 index 39865504..00000000 --- a/core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/analyzer/StillTextAnalyzer.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.ninecraft.booket.core.ocr.analyzer - -import androidx.annotation.OptIn -import androidx.camera.core.ExperimentalGetImage -import androidx.camera.core.ImageProxy -import com.google.mlkit.vision.common.InputImage -import com.google.mlkit.vision.text.TextRecognizer -import com.orhanobut.logger.Logger -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancel -import kotlinx.coroutines.launch -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine - -/** - * 정적인 카메라 이미지에서 텍스트를 분석하는 클래스 - * - * CameraX의 단일 ImageProxy 프레임을 받아 ML Kit을 통해 텍스트를 추출하고 결과를 콜백으로 전달한다. - * - * @param textRecognizer ML Kit의 TextRecognizer 인스턴스 - * @param onTextDetected 텍스트 인식 성공 시 호출되는 콜백 (인식된 전체 텍스트 전달) - * @param onFailure 인식 실패 시 호출되는 콜백 - * - * 분석이 끝난 후 반드시 imageProxy.close() 호출하여 리소스 해제 - */ -class StillTextAnalyzer @AssistedInject constructor( - private val textRecognizer: TextRecognizer, - @Assisted private val onTextDetected: (String) -> Unit, - @Assisted private val onFailure: () -> Unit, -) : TextAnalyzer { - - private val scope = CoroutineScope(Dispatchers.IO) - - @OptIn(ExperimentalGetImage::class) - override fun analyze(imageProxy: ImageProxy) { - scope.launch { - val mediaImage = imageProxy.image ?: run { imageProxy.close(); return@launch } - val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) - - suspendCoroutine { continuation -> - textRecognizer.process(inputImage) - .addOnSuccessListener { visionText -> - onTextDetected(visionText.text) - } - .addOnFailureListener { - onFailure() - } - .addOnCompleteListener { - continuation.resume(Unit) - } - } - }.invokeOnCompletion { exception -> - if (exception != null) { - Logger.e(exception.message ?: "Unknown error") - } - imageProxy.close() - } - } - - fun cancel() { - scope.cancel() - } - - @AssistedFactory - interface Factory { - fun create( - onTextDetected: (String) -> Unit, - onFailure: () -> Unit, - ): StillTextAnalyzer - } -} diff --git a/core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/analyzer/TextAnalyzer.kt b/core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/analyzer/TextAnalyzer.kt deleted file mode 100644 index c4d30b3c..00000000 --- a/core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/analyzer/TextAnalyzer.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.ninecraft.booket.core.ocr.analyzer - -import androidx.camera.core.ImageProxy - -interface TextAnalyzer { - fun analyze(imageProxy: ImageProxy) -} diff --git a/core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/di/OcrModule.kt b/core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/di/OcrModule.kt deleted file mode 100644 index 99ce5740..00000000 --- a/core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/di/OcrModule.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.ninecraft.booket.core.ocr.di - -import com.google.mlkit.vision.text.TextRecognition -import com.google.mlkit.vision.text.TextRecognizer -import com.google.mlkit.vision.text.korean.KoreanTextRecognizerOptions -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -object OcrModule { - - @Provides - @Singleton - fun provideTextRecognizer(): TextRecognizer = - TextRecognition.getClient(KoreanTextRecognizerOptions.Builder().build()) -} diff --git a/core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/analyzer/CloudOcrRecognizer.kt b/core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/recognizer/CloudOcrRecognizer.kt similarity index 97% rename from core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/analyzer/CloudOcrRecognizer.kt rename to core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/recognizer/CloudOcrRecognizer.kt index 538095db..9035c5cf 100644 --- a/core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/analyzer/CloudOcrRecognizer.kt +++ b/core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/recognizer/CloudOcrRecognizer.kt @@ -1,4 +1,4 @@ -package com.ninecraft.booket.core.ocr.analyzer +package com.ninecraft.booket.core.ocr.recognizer import android.net.Uri import android.util.Base64 diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrPresenter.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrPresenter.kt index 86291fc4..7ddb581d 100644 --- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrPresenter.kt +++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrPresenter.kt @@ -7,7 +7,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import com.ninecraft.booket.core.common.utils.handleException -import com.ninecraft.booket.core.ocr.analyzer.CloudOcrRecognizer +import com.ninecraft.booket.core.ocr.recognizer.CloudOcrRecognizer import com.ninecraft.booket.core.common.analytics.AnalyticsHelper import com.ninecraft.booket.feature.screens.OcrScreen import com.orhanobut.logger.Logger diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 92be2019..8363380a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -60,9 +60,6 @@ landscapist = "2.5.1" ## Lottie lottie = "6.6.6" -## OCR -mlkit-text = "16.0.1" - ## Extension # https://github.com/jisungbin/dependency-handler-extensions gradle-dependency-handler-extensions = "1.1.0" @@ -149,9 +146,6 @@ kakao-auth = { group = "com.kakao.sdk", name = "v2-user", version.ref = "kakao-c lottie-compose = { module = "com.airbnb.android:lottie-compose", version.ref = "lottie" } -google-mlkit-text-recognition = { group = "com.google.mlkit", name = "text-recognition", version.ref = "mlkit-text" } -google-mlkit-text-recognition-korean = { group = "com.google.mlkit", name = "text-recognition-korean", version.ref = "mlkit-text" } - androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" } androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "androidx-test-runner" } From 902ad8ed99525c76b1d89459dd1a87de6609762e Mon Sep 17 00:00:00 2001 From: seoyoon Date: Fri, 29 Aug 2025 18:56:19 +0900 Subject: [PATCH 33/42] =?UTF-8?q?[BOOK-304]=20fix:=20BookItem=20=EC=98=81?= =?UTF-8?q?=EC=97=AD=EA=B9=8C=EC=A7=80=20=EC=8A=A4=ED=81=AC=EB=A1=A4=20?= =?UTF-8?q?=EB=B2=94=EC=9C=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feature/edit/record/RecordEditUi.kt | 177 +++++++++--------- 1 file changed, 91 insertions(+), 86 deletions(-) diff --git a/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/record/RecordEditUi.kt b/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/record/RecordEditUi.kt index 0a7972c2..9f1257eb 100644 --- a/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/record/RecordEditUi.kt +++ b/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/record/RecordEditUi.kt @@ -85,114 +85,119 @@ internal fun RecordEditUi( @Composable private fun ColumnScope.RecordEditContent(state: RecordEditUiState) { - BookItem( - imageUrl = state.recordInfo.bookCoverImageUrl, - bookTitle = state.recordInfo.bookTitle, - author = state.recordInfo.author, - publisher = state.recordInfo.bookPublisher, - ) - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) - HorizontalDivider( - modifier = Modifier.fillMaxWidth(), - thickness = ReedTheme.border.border1, - color = ReedTheme.colors.dividerSm, - ) Column( modifier = Modifier .weight(1f) - .padding(horizontal = ReedTheme.spacing.spacing5) .verticalScroll(rememberScrollState()), ) { - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing6)) - Text( - text = stringResource(R.string.edit_record_page_label), - color = ReedTheme.colors.contentPrimary, - style = ReedTheme.typography.body1Medium, - ) - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) - ReedRecordTextField( - recordState = state.recordPageState, - recordHintRes = R.string.edit_record_page_hint, - inputTransformation = digitOnlyInputTransformation, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - lineLimits = TextFieldLineLimits.SingleLine, - isError = state.isPageError, - errorMessage = stringResource(R.string.edit_record_page_input_error), - onClear = { - state.eventSink(RecordEditUiEvent.OnClearClick) - }, - modifier = Modifier - .fillMaxWidth() - .height(50.dp), - ) - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing8)) - Text( - text = stringResource(R.string.edit_record_quote_label), - color = ReedTheme.colors.contentPrimary, - style = ReedTheme.typography.body1Medium, + BookItem( + imageUrl = state.recordInfo.bookCoverImageUrl, + bookTitle = state.recordInfo.bookTitle, + author = state.recordInfo.author, + publisher = state.recordInfo.bookPublisher, ) Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) - ReedRecordTextField( - recordState = state.recordQuoteState, - recordHintRes = R.string.edit_record_quote_hint, - modifier = Modifier - .fillMaxWidth() - .height(140.dp), - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Text, - imeAction = ImeAction.Default, - ), - ) - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing8)) - Text( - text = stringResource(R.string.edit_record_impression_label), - color = ReedTheme.colors.contentPrimary, - style = ReedTheme.typography.body1Medium, + HorizontalDivider( + modifier = Modifier.fillMaxWidth(), + thickness = ReedTheme.border.border1, + color = ReedTheme.colors.dividerSm, ) - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) - ReedRecordTextField( - recordState = state.recordImpressionState, - recordHintRes = R.string.edit_record_impression_hint, + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing6)) + Column( modifier = Modifier .fillMaxWidth() - .height(140.dp), - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Text, - imeAction = ImeAction.Default, - ), - ) - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing8)) - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, + .padding(horizontal = ReedTheme.spacing.spacing5), ) { Text( - text = stringResource(R.string.edit_record_emotion_label), + text = stringResource(R.string.edit_record_page_label), color = ReedTheme.colors.contentPrimary, style = ReedTheme.typography.body1Medium, ) - Spacer(modifier = Modifier.weight(1f)) - Row( - modifier = Modifier.clickable { - state.eventSink(RecordEditUiEvent.OnEmotionEditClick) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) + ReedRecordTextField( + recordState = state.recordPageState, + recordHintRes = R.string.edit_record_page_hint, + inputTransformation = digitOnlyInputTransformation, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + lineLimits = TextFieldLineLimits.SingleLine, + isError = state.isPageError, + errorMessage = stringResource(R.string.edit_record_page_input_error), + onClear = { + state.eventSink(RecordEditUiEvent.OnClearClick) }, + modifier = Modifier + .fillMaxWidth() + .height(50.dp), + ) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing8)) + Text( + text = stringResource(R.string.edit_record_quote_label), + color = ReedTheme.colors.contentPrimary, + style = ReedTheme.typography.body1Medium, + ) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) + ReedRecordTextField( + recordState = state.recordQuoteState, + recordHintRes = R.string.edit_record_quote_hint, + modifier = Modifier + .fillMaxWidth() + .height(140.dp), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Default, + ), + ) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing8)) + Text( + text = stringResource(R.string.edit_record_impression_label), + color = ReedTheme.colors.contentPrimary, + style = ReedTheme.typography.body1Medium, + ) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) + ReedRecordTextField( + recordState = state.recordImpressionState, + recordHintRes = R.string.edit_record_impression_hint, + modifier = Modifier + .fillMaxWidth() + .height(140.dp), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Default, + ), + ) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing8)) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, ) { - val emotion = state.recordInfo.emotionTags.firstOrNull() ?: "" - Text( - text = emotion, - color = ReedTheme.colors.contentSecondary, + text = stringResource(R.string.edit_record_emotion_label), + color = ReedTheme.colors.contentPrimary, style = ReedTheme.typography.body1Medium, ) - Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing1)) - Icon( - imageVector = ImageVector.vectorResource(designR.drawable.ic_chevron_right), - contentDescription = "Chevron Right Icon", - tint = ReedTheme.colors.contentSecondary, - ) + Spacer(modifier = Modifier.weight(1f)) + Row( + modifier = Modifier.clickable { + state.eventSink(RecordEditUiEvent.OnEmotionEditClick) + }, + ) { + val emotion = state.recordInfo.emotionTags.firstOrNull() ?: "" + + Text( + text = emotion, + color = ReedTheme.colors.contentSecondary, + style = ReedTheme.typography.body1Medium, + ) + Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing1)) + Icon( + imageVector = ImageVector.vectorResource(designR.drawable.ic_chevron_right), + contentDescription = "Chevron Right Icon", + tint = ReedTheme.colors.contentSecondary, + ) + } } + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing16)) } - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing16)) } ReedButton( onClick = { From 7a9ad692160a7b7bb930e4676accb6383f36636e Mon Sep 17 00:00:00 2001 From: seoyoon Date: Fri, 29 Aug 2025 19:09:56 +0900 Subject: [PATCH 34/42] =?UTF-8?q?[BOOK-304]=20refactor:=20=EB=B0=94?= =?UTF-8?q?=ED=85=80=EC=8B=9C=ED=8A=B8=EB=A1=9C=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=20=EC=8B=9C=20delayedGoTo=EB=A5=BC=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EC=A7=80=20=EC=95=8A=EA=B3=A0=20hid?= =?UTF-8?q?e()=20=ED=9B=84=20=EC=A0=84=ED=99=98=EB=90=98=EB=8F=84=EB=A1=9D?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ui에서 바텀시트의 hide() 호출 후 화면 이동 이벤트를 sink하도록 변경 - 바텀시트가 완전히 닫힌 후 화면이 전환되어 애니메이션이 자연스럽게 연결됨 --- .../feature/detail/book/BookDetailPresenter.kt | 17 +++++++---------- .../booket/feature/detail/book/BookDetailUi.kt | 5 ++++- .../detail/record/RecordDetailPresenter.kt | 17 +++++++---------- .../feature/detail/record/RecordDetailUi.kt | 5 ++++- .../feature/search/book/BookSearchPresenter.kt | 5 +---- .../booket/feature/search/book/BookSearchUi.kt | 7 ++++++- 6 files changed, 29 insertions(+), 27 deletions(-) diff --git a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt index 4d00094f..4e31bc7d 100644 --- a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt +++ b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt @@ -23,7 +23,6 @@ import com.ninecraft.booket.feature.screens.RecordDetailScreen import com.ninecraft.booket.feature.screens.RecordEditScreen import com.ninecraft.booket.feature.screens.RecordScreen import com.ninecraft.booket.feature.screens.arguments.RecordEditArgs -import com.ninecraft.booket.feature.screens.extensions.delayedGoTo import com.orhanobut.logger.Logger import com.slack.circuit.codegen.annotations.CircuitInject import com.slack.circuit.retained.rememberRetained @@ -304,15 +303,13 @@ class BookDetailPresenter @AssistedInject constructor( is BookDetailUiEvent.OnShareRecordClick -> { isRecordMenuBottomSheetVisible = false - scope.launch { - navigator.delayedGoTo( - RecordCardScreen( - quote = selectedRecordInfo.quote, - bookTitle = selectedRecordInfo.bookTitle, - emotionTag = selectedRecordInfo.emotionTags[0], - ), - ) - } + navigator.goTo( + RecordCardScreen( + quote = selectedRecordInfo.quote, + bookTitle = selectedRecordInfo.bookTitle, + emotionTag = selectedRecordInfo.emotionTags[0], + ), + ) } is BookDetailUiEvent.OnEditRecordClick -> { diff --git a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUi.kt b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUi.kt index 0c1a6d7d..cc27a011 100644 --- a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUi.kt +++ b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUi.kt @@ -136,7 +136,10 @@ internal fun BookDetailUi( }, sheetState = recordMenuBottomSheetState, onShareRecordClick = { - state.eventSink(BookDetailUiEvent.OnShareRecordClick) + coroutineScope.launch { + recordMenuBottomSheetState.hide() + state.eventSink(BookDetailUiEvent.OnShareRecordClick) + } }, onEditRecordClick = { coroutineScope.launch { diff --git a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailPresenter.kt b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailPresenter.kt index f5921683..d5b37e9e 100644 --- a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailPresenter.kt +++ b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailPresenter.kt @@ -14,7 +14,6 @@ import com.ninecraft.booket.feature.screens.RecordCardScreen import com.ninecraft.booket.feature.screens.RecordDetailScreen import com.ninecraft.booket.feature.screens.RecordEditScreen import com.ninecraft.booket.feature.screens.arguments.RecordEditArgs -import com.ninecraft.booket.feature.screens.extensions.delayedGoTo import com.orhanobut.logger.Logger import com.skydoves.compose.effects.RememberedEffect import com.slack.circuit.codegen.annotations.CircuitInject @@ -124,15 +123,13 @@ class RecordDetailPresenter @AssistedInject constructor( is RecordDetailUiEvent.OnShareRecordClick -> { isRecordMenuBottomSheetVisible = false - scope.launch { - navigator.delayedGoTo( - RecordCardScreen( - quote = recordDetailInfo.quote, - bookTitle = recordDetailInfo.bookTitle, - emotionTag = recordDetailInfo.emotionTags[0], - ), - ) - } + navigator.goTo( + RecordCardScreen( + quote = recordDetailInfo.quote, + bookTitle = recordDetailInfo.bookTitle, + emotionTag = recordDetailInfo.emotionTags[0], + ), + ) } is RecordDetailUiEvent.OnEditRecordClick -> { diff --git a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUi.kt b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUi.kt index ebd2bea1..48248dd9 100644 --- a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUi.kt +++ b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUi.kt @@ -83,7 +83,10 @@ internal fun RecordDetailUi( }, sheetState = recordMenuBottomSheetState, onShareRecordClick = { - state.eventSink(RecordDetailUiEvent.OnShareRecordClick) + coroutineScope.launch { + recordMenuBottomSheetState.hide() + state.eventSink(RecordDetailUiEvent.OnShareRecordClick) + } }, onEditRecordClick = { coroutineScope.launch { diff --git a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchPresenter.kt b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchPresenter.kt index 61cc99e5..9ec88d11 100644 --- a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchPresenter.kt +++ b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchPresenter.kt @@ -18,7 +18,6 @@ import com.ninecraft.booket.core.ui.component.FooterState import com.ninecraft.booket.feature.screens.LoginScreen import com.ninecraft.booket.feature.screens.RecordScreen import com.ninecraft.booket.feature.screens.SearchScreen -import com.ninecraft.booket.feature.screens.extensions.delayedGoTo import com.orhanobut.logger.Logger import com.slack.circuit.codegen.annotations.CircuitInject import com.slack.circuit.retained.collectAsRetainedState @@ -224,9 +223,7 @@ class BookSearchPresenter @AssistedInject constructor( is BookSearchUiEvent.OnBookRegisterSuccessOkButtonClick -> { isBookRegisterSuccessBottomSheetVisible = false - scope.launch { - navigator.delayedGoTo(RecordScreen(registeredUserBookId)) - } + navigator.goTo(RecordScreen(registeredUserBookId)) } is BookSearchUiEvent.OnBookRegisterSuccessCancelButtonClick -> { diff --git a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUi.kt b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUi.kt index 9e4a9c8f..0a434064 100644 --- a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUi.kt +++ b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUi.kt @@ -271,7 +271,12 @@ internal fun SearchContent( state.eventSink(BookSearchUiEvent.OnBookRegisterSuccessBottomSheetDismiss) } }, - onOKButtonClick = { state.eventSink(BookSearchUiEvent.OnBookRegisterSuccessOkButtonClick) }, + onOKButtonClick = { + coroutineScope.launch { + bookRegisterSuccessBottomSheetState.hide() + state.eventSink(BookSearchUiEvent.OnBookRegisterSuccessOkButtonClick) + } + }, ) } } From 343afd2154b4a2185d988a68c642aacd64f4aee8 Mon Sep 17 00:00:00 2001 From: seoyoon Date: Fri, 29 Aug 2025 19:16:41 +0900 Subject: [PATCH 35/42] =?UTF-8?q?[BOOK-304]=20fix:=20=EC=8B=9C=EC=8A=A4?= =?UTF-8?q?=ED=85=9C=20=EC=B9=B4=EB=A9=94=EB=9D=BC=20=EA=B6=8C=ED=95=9C=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=20=EC=8B=9C=20PermissionDialog=EA=B0=80=20?= =?UTF-8?q?=EB=8F=99=EC=8B=9C=EC=97=90=20=EB=9C=A8=EB=8A=94=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/ninecraft/booket/feature/record/ocr/OcrUi.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrUi.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrUi.kt index 4a77fefb..0673c1ba 100644 --- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrUi.kt +++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrUi.kt @@ -35,6 +35,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -120,7 +121,7 @@ private fun CameraPreview( ) { _ -> } // 최초 진입 시 권한 요청 - RememberedEffect(Unit) { + LaunchedEffect(Unit) { if (!isGranted) { state.eventSink(OcrUiEvent.OnHidePermissionDialog) permissionLauncher.launch(permission) From 67043a1ffa65d68d1a405ec548e68e7796ae57e0 Mon Sep 17 00:00:00 2001 From: seoyoon Date: Fri, 29 Aug 2025 19:31:46 +0900 Subject: [PATCH 36/42] =?UTF-8?q?[BOOK-304]=20fix:=20TextField=20=ED=8F=AC?= =?UTF-8?q?=EC=BB=A4=EC=8A=A4=20=EB=90=98=EC=A7=80=20=EC=95=8A=EC=9D=80=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=EC=97=90=EC=84=9C=20=EA=B0=90=EC=83=81?= =?UTF-8?q?=EB=AC=B8=20=EC=84=A0=ED=83=9D=20=EC=8B=9C=20=ED=8F=AC=EC=BB=A4?= =?UTF-8?q?=EC=8B=B1=20=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ninecraft/booket/feature/record/step/EmotionStep.kt | 3 +-- .../ninecraft/booket/feature/record/step/ImpressionStep.kt | 4 ++-- .../com/ninecraft/booket/feature/record/step/QuoteStep.kt | 3 +-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/EmotionStep.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/EmotionStep.kt index 58f22329..36b7b33e 100644 --- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/EmotionStep.kt +++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/EmotionStep.kt @@ -27,7 +27,6 @@ import androidx.compose.ui.unit.dp import com.ninecraft.booket.core.common.extensions.clickableSingle import com.ninecraft.booket.core.designsystem.ComponentPreview import com.ninecraft.booket.core.designsystem.EmotionTag -import com.ninecraft.booket.core.designsystem.RecordStep import com.ninecraft.booket.core.designsystem.component.button.ReedButton import com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle import com.ninecraft.booket.core.designsystem.component.button.largeButtonStyle @@ -113,7 +112,7 @@ fun EmotionStep( .padding(bottom = ReedTheme.spacing.spacing4), enabled = state.isNextButtonEnabled, text = stringResource(R.string.record_next_button), - multipleEventsCutterEnabled = state.currentStep == RecordStep.IMPRESSION, + multipleEventsCutterEnabled = false, ) } } diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/ImpressionStep.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/ImpressionStep.kt index 346e51b8..948ac648 100644 --- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/ImpressionStep.kt +++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/ImpressionStep.kt @@ -38,7 +38,6 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import com.ninecraft.booket.core.designsystem.ComponentPreview -import com.ninecraft.booket.core.designsystem.RecordStep import com.ninecraft.booket.core.designsystem.component.button.ReedButton import com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle import com.ninecraft.booket.core.designsystem.component.button.largeButtonStyle @@ -163,7 +162,7 @@ fun ImpressionStep( ), enabled = state.isNextButtonEnabled, text = stringResource(R.string.record_next_button), - multipleEventsCutterEnabled = state.currentStep == RecordStep.IMPRESSION, + multipleEventsCutterEnabled = true, ) } @@ -190,6 +189,7 @@ fun ImpressionStep( coroutineScope.launch { impressionGuideBottomSheetState.hide() state.eventSink(RecordRegisterUiEvent.OnImpressionGuideConfirmed) + focusRequester.requestFocus() } }, ) diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/QuoteStep.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/QuoteStep.kt index ecdd2a26..b9318765 100644 --- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/QuoteStep.kt +++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/QuoteStep.kt @@ -36,7 +36,6 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import com.ninecraft.booket.core.designsystem.ComponentPreview -import com.ninecraft.booket.core.designsystem.RecordStep import com.ninecraft.booket.core.designsystem.component.button.ReedButton import com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle import com.ninecraft.booket.core.designsystem.component.button.largeButtonStyle @@ -178,7 +177,7 @@ internal fun QuoteStep( ), enabled = state.isNextButtonEnabled, text = stringResource(R.string.record_next_button), - multipleEventsCutterEnabled = state.currentStep == RecordStep.IMPRESSION, + multipleEventsCutterEnabled = false, ) } } From 08252b1cd83b1f0c1dc9c6eb181d144395756275 Mon Sep 17 00:00:00 2001 From: seoyoon Date: Fri, 29 Aug 2025 19:33:10 +0900 Subject: [PATCH 37/42] [BOOK-304] chore: code style check success --- .../kotlin/com/ninecraft/booket/feature/library/LibraryUi.kt | 1 - .../main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrUi.kt | 1 - 2 files changed, 2 deletions(-) diff --git a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUi.kt b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUi.kt index 4711afd2..3d23b06c 100644 --- a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUi.kt +++ b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUi.kt @@ -1,7 +1,6 @@ package com.ninecraft.booket.feature.library 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.Spacer diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrUi.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrUi.kt index 0673c1ba..1cdee8da 100644 --- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrUi.kt +++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrUi.kt @@ -69,7 +69,6 @@ import com.ninecraft.booket.feature.record.R import com.ninecraft.booket.feature.record.ocr.component.CameraFrame import com.ninecraft.booket.feature.record.ocr.component.SentenceBox import com.ninecraft.booket.feature.screens.OcrScreen -import com.skydoves.compose.effects.RememberedEffect import com.slack.circuit.codegen.annotations.CircuitInject import dagger.hilt.android.components.ActivityRetainedComponent import tech.thdev.compose.exteions.system.ui.controller.rememberSystemUiController From 5be1d31e12fd8398a99234cef5e350f894c2c46d Mon Sep 17 00:00:00 2001 From: seoyoon Date: Fri, 29 Aug 2025 19:44:43 +0900 Subject: [PATCH 38/42] =?UTF-8?q?[BOOK-304]=20fix:=20=EA=B8=B0=EB=A1=9D=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EC=8B=9C=20=EC=A4=91=EB=B3=B5=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=EB=90=98=EB=8A=94=20=EB=AC=B8=EC=A0=9C=EB=A5=BC=20?= =?UTF-8?q?=EB=A7=89=EA=B8=B0=20=EC=9C=84=ED=95=B4=20=EB=A1=9C=EB=94=A9=20?= =?UTF-8?q?=EC=9D=B8=EB=94=94=EC=BC=80=EC=9D=B4=ED=84=B0=20delay=EB=A5=BC?= =?UTF-8?q?=200L=EB=A1=9C=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../booket/feature/record/register/RecordRegisterUi.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUi.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUi.kt index 3ae920d7..ac3a7278 100644 --- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUi.kt +++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUi.kt @@ -85,7 +85,7 @@ internal fun RecordRegisterUi( } if (state.isLoading) { - ReedLoadingIndicator() + ReedLoadingIndicator(delayMillis = 0L) } if (state.isExitDialogVisible) { From 09933a965df1be9a834b0871a8e7362d0c6f5ebd Mon Sep 17 00:00:00 2001 From: seoyoon Date: Sat, 30 Aug 2025 01:42:41 +0900 Subject: [PATCH 39/42] Update README.md --- README.md | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 916f4873..131a8e3c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -# Reed-Android +# Reed - 문장과 감정을 함께 담는 독서 기록 + [![Kotlin](https://img.shields.io/badge/Kotlin-2.2.0-blue.svg)](https://kotlinlang.org) [![Gradle](https://img.shields.io/badge/gradle-8.11.1-green.svg)](https://gradle.org/) [![Android Studio](https://img.shields.io/badge/Android%20Studio-2025.1.2%20%28Narwhal%29-green)](https://developer.android.com/studio) @@ -6,9 +7,11 @@ [![targetSdkVersion](https://img.shields.io/badge/targetSdkVersion-35-orange)](https://developer.android.com/distribute/best-practices/develop/target-sdk) ![CodeRabbit Pull Request Reviews](https://img.shields.io/coderabbit/prs/github/YAPP-Github/Reed-Android?utm_source=oss&utm_medium=github&utm_campaign=YAPP-Github%2FReed-Android&labelColor=171717&color=FF570A&link=https%3A%2F%2Fcoderabbit.ai&label=CodeRabbit+Reviews)
+reed_graphic -Reed(리드) - 문장과 감정을 함께 담는 독서 기록 [PlayStore](https://play.google.com/store/apps/details?id=com.ninecraft.booket&hl=ko) -reed_graphic + + +

@@ -21,6 +24,17 @@ Reed(리드) - 문장과 감정을 함께 담는 독서 기록 [PlayStore](https

## Features +| 홈 | 도서 검색 및 등록 | 내서재 | +|:---:|:---:|:---:| +| 홈 | 도서 검색 및 등록 | 내서재 | + +| OCR | 기록 등록 | 도서 & 기록 상세 | +|:---:|:---:|:---:| +| OCR | 기록 등록 | 도서 & 기록 상세 | + +| 기록 카드 공유 | +|:---:| +| 기록 카드 공유 | ## TroubleShooting - [[Compose] M3 ModalBottomSheet 드래그(터치 이벤트) 막는 법](https://velog.io/@mraz3068/Compose-M3-ModalBottomSheet-Drag-Disabled) @@ -40,7 +54,7 @@ Reed(리드) - 문장과 감정을 함께 담는 독서 기록 [PlayStore](https - IDE : Android Studio 최신 버전 - JDK : Java 17을 실행할 수 있는 JDK - (권장) Android Studio 설치 시 Embedded 된 JDK (Open JDK) - - Java 17을 사용하는 JDK (Open JDK, AdoptOpenJDK, GraalVM) + - Java 17을 사용하는 JDK (Open JDK, AdoptOpenJDK, GraalVM) - Kotlin Language : 2.2.0 ### Language @@ -50,16 +64,16 @@ Reed(리드) - 문장과 감정을 함께 담는 독서 기록 [PlayStore](https ### Libraries - AndroidX - - Activity Compose - - Core - - DataStore - - StartUp - - Splash - - CameraX + - Activity Compose + - Core + - DataStore + - StartUp + - Splash + - CameraX - Kotlin Libraries (Coroutine, Serialization, Immutable Collection) - Compose - - Material3 + - Material3 - [Circuit](https://github.com/slackhq/circuit) - ~~Google ML Kit~~ Google Cloud Vision From 85fe843a080cd0986d33aa9bd2eb88d6927cd86d Mon Sep 17 00:00:00 2001 From: easyhooon Date: Sat, 30 Aug 2025 09:21:17 +0900 Subject: [PATCH 40/42] =?UTF-8?q?[BOOK-304]=20chore:=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=EC=9D=98=EC=A1=B4=EC=84=B1=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradle/libs.versions.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ae8daee0..e5622db3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -33,7 +33,6 @@ ksp = "2.2.0-2.0.2" kotlin = "2.2.0" kotlinx-coroutines = "1.10.2" kotlinx-serialization-json = "1.9.0" -kotlinx-datetime = "0.7.1" kotlinx-collections-immutable = "0.4.0" ## Hilt From e079b915202c9dbd50a1858abec351da2610f7f0 Mon Sep 17 00:00:00 2001 From: easyhooon Date: Mon, 1 Sep 2025 00:42:56 +0900 Subject: [PATCH 41/42] =?UTF-8?q?[BOOK-304]=20feat:=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=ED=99=94=EB=A9=B4=EC=97=90=EC=84=9C=20=EC=95=BD?= =?UTF-8?q?=EA=B4=80=EB=8F=99=EC=9D=98=ED=99=94=EB=A9=B4=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=EC=8B=9C=20returnToScreen=20=EC=A0=84=EB=8B=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt index d0a4ee03..f0f90cc6 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt @@ -53,7 +53,7 @@ class LoginPresenter @AssistedInject constructor( navigator.popUntil { it == screen.returnToScreen } } } else { - navigator.resetRoot(TermsAgreementScreen()) + navigator.resetRoot(TermsAgreementScreen(screen.returnToScreen)) } }.onFailure { exception -> exception.message?.let { Logger.e(it) } From 657f67d37966fac6d9fb580faba20bea9978584a Mon Sep 17 00:00:00 2001 From: seoyoon Date: Mon, 1 Sep 2025 19:38:59 +0900 Subject: [PATCH 42/42] =?UTF-8?q?[BOOK-304]=20refactor:=20remember=20+=20L?= =?UTF-8?q?aunchedEffect=EB=A5=BC=20produceState=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/ui/component/ReedLoadingIndicator.kt | 11 ++--- .../booket/feature/record/ocr/OcrUi.kt | 46 +++++++++---------- 2 files changed, 25 insertions(+), 32 deletions(-) diff --git a/core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedLoadingIndicator.kt b/core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedLoadingIndicator.kt index 88c4a0a7..cd8f9ade 100644 --- a/core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedLoadingIndicator.kt +++ b/core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedLoadingIndicator.kt @@ -4,11 +4,8 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.CircularProgressIndicator 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.setValue +import androidx.compose.runtime.produceState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.ninecraft.booket.core.common.extensions.noRippleClickable @@ -21,11 +18,9 @@ fun ReedLoadingIndicator( modifier: Modifier = Modifier, delayMillis: Long = 500L, ) { - var showProgressBar by remember { mutableStateOf(false) } - - LaunchedEffect(Unit) { + val showProgressBar by produceState(initialValue = false, key1 = delayMillis) { delay(delayMillis) - showProgressBar = true + value = true } if (showProgressBar) { diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrUi.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrUi.kt index 1cdee8da..76af3913 100644 --- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrUi.kt +++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrUi.kt @@ -37,9 +37,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -101,16 +100,31 @@ private fun CameraPreview( /** * Camera Permission Request */ - var isGranted by remember { - mutableStateOf( - ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED, - ) + val isGranted by produceState( + initialValue = ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED, + key1 = lifecycleOwner, // lifecycle 변경 시 재설정 + ) { + // 최초 동기화 + value = ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED + + // 포그라운드 복귀 시 OS 권한 동기화 + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_RESUME) { + value = ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED + if (value) { + state.eventSink(OcrUiEvent.OnHidePermissionDialog) + } else { + state.eventSink(OcrUiEvent.OnShowPermissionDialog) + } + } + } + lifecycleOwner.lifecycle.addObserver(observer) + awaitDispose { lifecycleOwner.lifecycle.removeObserver(observer) } } + val permissionLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.RequestPermission(), ) { granted -> - isGranted = granted - if (!granted) { state.eventSink(OcrUiEvent.OnShowPermissionDialog) } @@ -127,22 +141,6 @@ private fun CameraPreview( } } - // 앱이 포그라운드로 북귀할 때 OS 권한 동기화 - DisposableEffect(Unit) { - val observer = LifecycleEventObserver { _, event -> - if (event == Lifecycle.Event.ON_RESUME) { - isGranted = ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED - if (isGranted) { - state.eventSink(OcrUiEvent.OnHidePermissionDialog) - } else { - state.eventSink(OcrUiEvent.OnShowPermissionDialog) - } - } - } - lifecycleOwner.lifecycle.addObserver(observer) - onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } - } - /** * Camera Controller */