Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
0b1c98b
[BOOK-298] feat: 비회원 도서 검색 API 연동
easyhooon Aug 27, 2025
04fea0f
[BOOK-298] feat: Guest Login 홈 화면 진입 시 화면 분기 처리 구현
easyhooon Aug 27, 2025
d66bbfb
[BOOK-298] feat: 도서 상태 업데이트 API 호출시 Guest Mode 분기 처리
easyhooon Aug 27, 2025
c927738
[BOOK-298] feat: 로그인 FullScreenDialog 구성
easyhooon Aug 27, 2025
4ea2bd0
[BOOK-298] feat: UiText 도입
easyhooon Aug 28, 2025
0d53f60
[BOOK-298] feat: Guest 모드 로그인 화면 Redirect 및 로그인 후 원래 화면으로 돌아오기 구현
easyhooon Aug 28, 2025
bd64507
[BOOK-298] feat: 도서 검색 화면 진입, 도서 검색 이벤트 로그 수집 분리
easyhooon Aug 28, 2025
0655dbf
[BOOK-298] feat: ReedTextButton 추가
easyhooon Aug 28, 2025
d107b0a
[BOOK-298] chore: Login 화면 ReedCloseAppBar visible 분기 처리
easyhooon Aug 28, 2025
bda573a
[BOOK-298] chore: code style check success
easyhooon Aug 28, 2025
e081cd5
[BOOK-298] chore: ReedTextButton colors ButtonDefaults.textButtonColo…
easyhooon Aug 28, 2025
6c87639
[BOOK-298] fix: 로그아웃/회원탈퇴 화면 전환 이전에 다이얼로그/바텀시트가 닫히도록 flag 호출 순서 변경
easyhooon Aug 28, 2025
8416732
[BOOK-298] refactor: BookRegisteredState enum class, BookSummaryModel…
easyhooon Aug 28, 2025
38edf2c
[BOOK-298] chore: 로그인 화면 리다이렉트로 진입시 누락된 로그인 버튼 하당 패딩 적용
easyhooon Aug 28, 2025
74f4d91
[BOOK-298] chore: 토끼 리뷰 반영
easyhooon Aug 28, 2025
e7ece1c
[BOOK-298] refactor: Guest Mode 홈/내 서재 화면 API 호출되지 않는 방식으로 변경
easyhooon Aug 28, 2025
84ef4e5
[BOOK-298] chore: 사용하지 않는 라이브러리 의존성 제거
easyhooon Aug 28, 2025
94317d8
[BOOK-298] fix: 내 서재 화면 rememberEffect key userState로 변경
easyhooon Aug 28, 2025
74f49c2
[BOOK-298] chore: 중첩 조건문 제거
easyhooon Aug 28, 2025
26825c5
[BOOK-298] chore: 도서 검색 결과 없음 로그 수집 제거
easyhooon Aug 28, 2025
ca25219
[BOOK-298] chore: app version update
easyhooon Aug 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

present에 String을 그대로 넣는 부분이 찝찝했었는데 (토끼한테 매번 잡히는 것도 그렇구) 너무 좋네요 👍👍

data class DirectString(val value: String) : UiText()

class StringResource(
@StringRes val resId: Int,
vararg val args: Any,
) : UiText()
Comment on lines +14 to +15
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

null 포맷 인자 허용을 위해 args 타입을 Any?로 변경하세요.

nullable 인자를 넘겨야 하는 리소스 포맷팅(use-case 꽤 있습니다)에서 현재 시그니처(vararg Any)는 null 전달이 불가능합니다.
[Suggest fix]

-        vararg val args: Any,
+        vararg val args: Any?,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
vararg val args: Any,
) : UiText()
vararg val args: Any?,
) : UiText()
🤖 Prompt for AI Agents
In core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/UiText.kt
around lines 14 to 15, the vararg parameter is currently declared as Any which
disallows passing null format arguments; change the signature to use vararg val
args: Any? so callers can pass nullable values, and update any related call
sites or overloads if necessary to accept and propagate nulls safely (e.g.,
ensure formatting logic handles null entries without throwing).


@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)
}
}
Original file line number Diff line number Diff line change
@@ -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
Comment on lines 3 to +4
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

중복 상태 API 노출(autoLoginState vs userState) — 정리 필요

새 UserState로 전환하면서 기존 autoLoginState가 남아 API가 중복/혼선을 일으킵니다. 제거하거나 @deprecated로 마이그레이션 경로를 명확히 하세요.

권장 수정(완전 제거):

-import com.ninecraft.booket.core.model.AutoLoginState
 import com.ninecraft.booket.core.model.UserState
@@
-    val autoLoginState: Flow<AutoLoginState>
-
     val userState: Flow<UserState>
 
     suspend fun getCurrentUserState(): UserState

대안: 유지가 필요하다면

@Deprecated("Use userState instead", ReplaceWith("userState"))
val autoLoginState: Flow<AutoLoginState>

Also applies to: 14-19

🤖 Prompt for AI Agents
In
core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/AuthRepository.kt
around lines 3-4 and also 14-19, the old autoLoginState API is left alongside
the new userState causing duplicate/confusing state exposure; either remove
autoLoginState entirely or mark it deprecated to provide a clear migration path.
If removing: delete the autoLoginState property and any references, keeping only
userState. If retaining for compatibility: annotate the autoLoginState property
with @Deprecated("Use userState instead", ReplaceWith("userState")) and ensure
its implementation delegates to userState so behavior stays consistent; update
imports and usages accordingly.

import kotlinx.coroutines.flow.Flow

interface AuthRepository {
Expand All @@ -11,4 +12,8 @@ interface AuthRepository {
suspend fun withdraw(): Result<Unit>

val autoLoginState: Flow<AutoLoginState>

val userState: Flow<UserState>

suspend fun getCurrentUserState(): UserState
}
Comment on lines +16 to 19
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Aug 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

구현/호출부 전반 검증 요청

DefaultAuthRepository 구현과 호출부가 전부 userState/getCurrentUserState로 이행되었는지 확인해 주세요.


🏁 Script executed:

#!/bin/bash
# 남은 사용처 확인
rg -nC2 '\bautoLoginState\b'

# 구현 존재 확인
rg -nC3 'class\s+DefaultAuthRepository\b|override\s+val\s+userState|override\s+suspend\s+fun\s+getCurrentUserState'

Length of output: 6579


SplashPresenter에서 authRepository.autoLoginState 사용을 모두 제거하고 userState/getCurrentUserState로 교체하세요.
– 파일: feature/splash/src/main/kotlin/com/ninecraft/booket/splash/SplashPresenter.kt (라인 48, 80, 130)

🤖 Prompt for AI Agents
In
core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/AuthRepository.kt
around lines 16-19 and in
feature/splash/src/main/kotlin/com/ninecraft/booket/splash/SplashPresenter.kt at
lines 48, 80, 130, replace all uses of authRepository.autoLoginState with the
new API: use authRepository.userState (Flow<UserState>) when you need an
observable stream (collect it or convert to a StateFlow/LiveData in the
presenter scope), and call suspend fun authRepository.getCurrentUserState() from
a coroutine when you need the current snapshot. Update the three call sites to
either collect userState with lifecycle/coroutine scope (e.g.,
presenterScope.launch { authRepository.userState.collect { ... } }) or call
getCurrentUserState() inside a coroutine and handle the returned UserState, and
remove any references/imports to autoLoginState.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

자동 로그인 여부와, 로그인 여부는 다름

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@easyhooon 맞습니다. 자동 로그인 설정과 현재 로그인 상태는 별개의 개념이네요.

autoLoginState는 사용자가 자동 로그인을 설정했는지의 여부를 나타내고, userState는 현재 사용자가 게스트인지 로그인된 상태인지를 나타내는 것으로 이해됩니다. 두 상태는 독립적으로 관리되어야 하므로 이전 제안을 철회하겠습니다.


✏️ Learnings added
Learnt from: easyhooon
PR: YAPP-Github/Reed-Android#174
File: core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/AuthRepository.kt:16-19
Timestamp: 2025-08-28T12:26:12.459Z
Learning: In AuthRepository, autoLoginState and userState serve different purposes and should not be conflated:
- autoLoginState: tracks whether automatic login is enabled (user preference/setting)
- userState: tracks current authentication state (Guest vs LoggedIn)
These are orthogonal concepts that need to be maintained separately.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ interface BookRepository {
val bookRecentSearches: Flow<List<String>>
val libraryRecentSearches: Flow<List<String>>

suspend fun searchBookAsGuest(
query: String,
start: Int,
): Result<BookSearchModel>

suspend fun searchBook(
query: String,
start: Int,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
)
}
Comment on lines +100 to +111
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

키 계산 시 디코딩 불일치로 동일 도서 중복 키 가능성

title을 디코딩하여 표시하면서 key는 원본 title로 생성되어, HTML 엔티티 포함 타이틀에서 키 불일치가 발생할 수 있습니다. 디코딩된 타이틀 기반으로 키를 생성해 로그인/게스트 간 키 일관성을 확보하세요.

 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",
-    )
+    val decodedTitle = title.decodeHtmlEntities()
+    return BookSummaryModel(
+        isbn13 = isbn13,
+        title = decodedTitle,
+        author = author,
+        publisher = publisher,
+        coverImageUrl = coverImageUrl,
+        link = link,
+        userBookStatus = "BEFORE_REGISTRATION",
+        key = "$decodedTitle-$isbn13",
+    )
 }

또한 상수 문자열 "BEFORE_REGISTRATION"는 상수로 추출해 오타를 방지하세요(예: private const val BEFORE_REGISTRATION = "BEFORE_REGISTRATION").

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 GuestBookSummary.toModel(): BookSummaryModel {
val decodedTitle = title.decodeHtmlEntities()
return BookSummaryModel(
isbn13 = isbn13,
title = decodedTitle,
author = author,
publisher = publisher,
coverImageUrl = coverImageUrl,
link = link,
userBookStatus = "BEFORE_REGISTRATION",
key = "$decodedTitle-$isbn13",
)
}
🤖 Prompt for AI Agents
In
core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/mapper/ResponseToModel.kt
around lines 100 to 111, the model uses decoded title for display but builds the
key from the raw title and hardcodes the status string; change key generation to
use the decoded title (so key = "$decodedTitle-$isbn13") to avoid duplicate keys
for titles with HTML entities, and extract the status string into a private
const val BEFORE_REGISTRATION = "BEFORE_REGISTRATION" (then reference
BEFORE_REGISTRATION for userBookStatus) to prevent typos and centralize the
constant.


internal fun BookDetailResponse.toModel(): BookDetailModel {
return BookDetailModel(
version = version,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@ internal class DefaultBookRepository @Inject constructor(
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
}
Comment on lines +20 to +31
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

runSuspendCatching가 suspend 블록을 지원하는지 확인 필요

현재 블록 내부에서 Retrofit suspend API를 호출합니다. runSuspendCatching가 suspend 블록 시그니처(suspend () -> T)를 지원하지 않으면 컴파일/런타임 이슈가 발생합니다. 아래 스크립트로 선언을 확인해 주세요. 필요한 경우 runSuspendCatching를 suspend inline으로 변경해야 합니다.

필요 시 변경 예시(참고: 선언부 파일에서 적용):

-@OptIn(ExperimentalContracts::class)
-@Suppress("WRONG_INVOCATION_KIND", "TooGenericExceptionCaught")
-inline fun <T> runSuspendCatching(block: () -> T): Result<T> {
+@OptIn(ExperimentalContracts::class)
+@Suppress("TooGenericExceptionCaught")
+suspend inline fun <T> runSuspendCatching(
+    crossinline block: suspend () -> T
+): Result<T> {
     contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
     return try {
-        Result.success(block())
+        Result.success(block())
     } catch (rethrown: CancellationException) {
         throw rethrown
     } catch (exception: Throwable) {
         Result.failure(exception)
     }
 }

🏁 Script executed:

#!/bin/bash
# runSuspendCatching 선언부가 suspend 블록을 받는지 확인
rg -nP --glob 'core/common/**' 'runSuspendCatching\s*\(' -C3
# 기대: 선언부에 'suspend' 키워드와 'block: suspend \(\) ->' 형태가 포함되어야 함

Length of output: 1058


runSuspendCatching에 suspend 블록 시그니처 지원 추가 필요
현재 선언( core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/RunSuspendCatching.kt )이 non-suspend 블록만 받도록 되어 있어, Retrofit의 suspend API 호출 시 컴파일 오류가 발생합니다. 선언부를 아래와 같이 수정하세요:

- @OptIn(ExperimentalContracts::class)
- @Suppress("WRONG_INVOCATION_KIND", "TooGenericExceptionCaught")
- inline fun <T> runSuspendCatching(block: () -> T): Result<T> {
+ @OptIn(ExperimentalContracts::class)
+ @Suppress("TooGenericExceptionCaught")
+ suspend inline fun <T> runSuspendCatching(
+     crossinline block: suspend () -> T
+ ): Result<T> {
     contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
     return try {
         Result.success(block())
🤖 Prompt for AI Agents
In
core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultBookRepository.kt
around lines 20 to 31, the call to runSuspendCatching currently fails because
the utility only accepts non‑suspend blocks; update the declaration in
core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/RunSuspendCatching.kt
so it accepts a suspend lambda (e.g., change the block parameter type to suspend
() -> T and make the wrapper function suspend as needed), preserving error
handling semantics and return typing so Retrofit suspend calls compile.


override suspend fun searchBook(
query: String,
start: Int,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,21 @@ 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) {
PRIMARY -> if (isPressed) ReedTheme.colors.bgPrimaryPressed else ReedTheme.colors.bgPrimary
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
}

Expand All @@ -24,11 +26,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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package com.ninecraft.booket.core.designsystem.component.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,
multipleEventsCutterEnabled: Boolean = true,
) {
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.textButtonColors(
containerColor = colorStyle.containerColor(isPressed),
contentColor = colorStyle.contentColor(),
disabledContentColor = colorStyle.disabledContentColor(),
disabledContainerColor = colorStyle.disabledContainerColor(),
),
Comment on lines +37 to +55
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

isPressed가 항상 false가 되는 버그 — interactionSource 미전달

interactionSourceTextButton에 전달하지 않아 collectIsPressedAsState()가 눌림 상태를 반영하지 못합니다. Pressed 색상이 적용되지 않습니다.

아래 수정으로 연결해 주세요:

     TextButton(
         onClick = {
             if (multipleEventsCutterEnabled) {
                 multipleEventsCutter.processEvent { onClick() }
             } else {
                 onClick()
             }
         },
         modifier = modifier,
         enabled = enabled,
+        interactionSource = interactionSource,
         colors = ButtonDefaults.textButtonColors(
             containerColor = colorStyle.containerColor(isPressed),
             contentColor = colorStyle.contentColor(),
             disabledContentColor = colorStyle.disabledContentColor(),
             disabledContainerColor = colorStyle.disabledContainerColor(),
         ),
         contentPadding = sizeStyle.paddingValues,
     ) {
🤖 Prompt for AI Agents
In
core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ReedTextButton.kt
around lines 37 to 55, the remembered interactionSource is not passed into the
TextButton, so collectIsPressedAsState() never reflects press state; update the
TextButton call to supply the existing interactionSource parameter
(interactionSource = interactionSource) so the button receives the same
MutableInteractionSource and isPressed correctly tracks press events for color
changes.

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),
) {
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",
)
}
}
}
1 change: 1 addition & 0 deletions core/designsystem/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
<string name="server_error_message">이용에 불편을 드려 죄송합니다.\n잠시후 다시 이용해주세요.</string>
<string name="unknown_error_message">알 수 없는 오류가 발생하였습니다.</string>
<string name="search_book_hint">도서 검색 후 내 서재에 담아보세요</string>
<string name="login_required">로그인이 필요한 기능입니다</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,11 @@ data class BookSummaryModel(
val link: String = "",
val userBookStatus: String = "",
val key: String = "",
)
) {
val isRegistered: Boolean
get() = userBookStatus != BEFORE_REGISTRATION
Comment on lines +31 to +32
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

기본값으로 인해 isRegistered가 true로 잘못 계산됩니다.

현재 userBookStatus의 기본값이 빈 문자열("")이라 isRegistered가 기본적으로 true가 됩니다. 기본 상태가 미등록임을 의도했다면 userBookStatus의 기본값을 BEFORE_REGISTRATION으로 지정해야 합니다.

다음과 같이 기본값을 조정해 주세요(선택 영역 밖 변경이므로 참고용 스니펫):

data class BookSummaryModel(
    // ...
-   val userBookStatus: String = "",
+   val userBookStatus: String = BEFORE_REGISTRATION,
    // ...
)
🤖 Prompt for AI Agents
In core/model/src/main/kotlin/com/ninecraft/booket/core/model/BookSearchModel.kt
around lines 31-32, isRegistered is computed from userBookStatus which currently
defaults to an empty string causing isRegistered to be true by default; change
the default value of userBookStatus in the data class (e.g., BookSummaryModel)
to BEFORE_REGISTRATION instead of "" so that isRegistered evaluates correctly,
and update any constructors/tests/instantiations that rely on the old default to
pass the new default value if needed.


companion object {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍👍

const val BEFORE_REGISTRATION = "BEFORE_REGISTRATION"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.ninecraft.booket.core.model

sealed interface UserState {
data object Guest : UserState
data object LoggedIn : UserState
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
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<GuestBookSummary>,
)

@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,
)
Loading
Loading