Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
2 changes: 2 additions & 0 deletions core/common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ dependencies {
projects.core.model,
projects.core.network,

libs.kotlinx.collections.immutable,

libs.logger,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.ninecraft.booket.core.common.constants

import com.ninecraft.booket.core.common.R

enum class BookStatus(val value: String) {
BEFORE_READING("BEFORE_READING"),
READING("READING"),
COMPLETED("COMPLETED"),
;

fun getDisplayNameRes(): Int {
return when (this) {
BEFORE_READING -> R.string.book_status_before
READING -> R.string.book_status_reading
COMPLETED -> R.string.book_status_completed
}
}

companion object Companion {
fun fromValue(value: String): BookStatus? {
return entries.find { it.value == value }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.ninecraft.booket.core.common.extensions

import androidx.compose.ui.graphics.Color
import com.ninecraft.booket.core.model.Emotion

fun Emotion.toTextColor(): Color {
return when (this) {
Emotion.WARM -> Color(0xFFE3931B)
Emotion.JOY -> Color(0xFFEE6B33)
Emotion.TENSION -> Color(0xFF9A55E4)
Emotion.SADNESS -> Color(0xFF2872E9)
}
}

fun Emotion.toBackgroundColor(): Color {
return when (this) {
Emotion.WARM -> Color(0xFFFFF5D3)
Emotion.JOY -> Color(0xFFFFEBE3)
Emotion.TENSION -> Color(0xFFF3E8FF)
Emotion.SADNESS -> Color(0xFFE1ECFF)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.ninecraft.booket.core.common.util

import com.ninecraft.booket.core.model.EmotionModel

data class EmotionAnalysisResult(
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.

감정 분석 클라에서 하는거였나요...????????

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.

API 가 어떻게 나올지 몰라서 일단 구현했는데, API 연동 이후 필요없으면 제거하면 될것 같아여

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.

아깝네요...

Copy link
Copy Markdown
Contributor Author

@easyhooon easyhooon Jul 31, 2025

Choose a reason for hiding this comment

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

아깝네요...

굳이 지울 필요 없이, 각 상황에 대한 프리뷰를 구성하는데 사용할 수 있을 듯 합니다.
적용완료 bc78d8c

val topEmotions: List<EmotionModel>,
val displayType: EmotionDisplayType,
)

enum class EmotionDisplayType {
SINGLE, // 1개 감정이 1위
DUAL, // 2개 감정이 공동 1위
BALANCED, // 3개 이상 감정이 공동 1위
}

fun analyzeEmotions(emotions: List<EmotionModel>): EmotionAnalysisResult {
if (emotions.isEmpty()) {
return EmotionAnalysisResult(emptyList(), EmotionDisplayType.BALANCED)
}

val maxCount = emotions.maxOf { it.count }
val topEmotions = emotions.filter { it.count == maxCount }

val displayType = when (topEmotions.size) {
1 -> EmotionDisplayType.SINGLE
2 -> EmotionDisplayType.DUAL
else -> EmotionDisplayType.BALANCED
}

return EmotionAnalysisResult(topEmotions, displayType)
}
8 changes: 8 additions & 0 deletions core/common/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="book_status_before">읽기 전</string>
<string name="book_status_reading">읽는 중</string>
<string name="book_status_completed">독서 완료</string>
<string name="record_sort_page_ascending">페이지순</string>
<string name="record_sort_recent_register">최신 등록순</string>
</resources>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.ninecraft.booket.core.model

import androidx.compose.runtime.Stable

@Stable
data class EmotionModel(
val type: Emotion,
val count: Int,
)

enum class Emotion(
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.

아아.. 만드셨군요... 편히 쓰라고 core:designsystem에 저도 만들어놨는데 ^_ㅠ
EmotionTag.kt

enum class EmotionTag(val label: String, val bgColor: Color, val textColor: Color, val graphic: Int) {
    WARMTH("따뜻함", WarmthBgColor, WarmthTextColor, 0),
    JOY("즐거움", JoyBgColor, JoyTextColor, 0),
    TENSION("긴장감", TensionBgColor, TensionTextColor, 0),
    SADNESS("슬픔", SadnessBgColor, SadnessTextColor, 0),
}

val displayName: String,
) {
WARM("따뜻함"),
JOY("즐거움"),
TENSION("긴장감"),
SADNESS("슬픔"),
}
Comment on lines +11 to +18
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Jul 31, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

모델 계층에 하드코딩된 한글 문자열 → 리소스화 필요

Emotion.displayName을 직접 한글로 보관하면 다국어 지원이 불가능합니다.
모델에는 @StringRes ID(또는 별도 매핑 함수)만 보관하고, UI 계층에서 stringResource()로 해석하도록 바꾸는 편이 좋습니다.

-enum class Emotion(
-    val displayName: String,
-) {
-    WARM("따뜻함"),
-    JOY("즐거움"),
-    TENSION("긴장감"),
-    SADNESS("슬픔"),
-}
+enum class Emotion(@StringRes val labelResId: Int) {
+    WARM(R.string.emotion_warm),
+    JOY(R.string.emotion_joy),
+    TENSION(R.string.emotion_tension),
+    SADNESS(R.string.emotion_sadness),
+}

리소스 파일(core/common/.../strings.xml)에 emotion_warm 등 키를 추가해 주세요.

📝 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
enum class Emotion(
val displayName: String,
) {
WARM("따뜻함"),
JOY("즐거움"),
TENSION("긴장감"),
SADNESS("슬픔"),
}
import androidx.annotation.StringRes
enum class Emotion(@StringRes val labelResId: Int) {
WARM(R.string.emotion_warm),
JOY(R.string.emotion_joy),
TENSION(R.string.emotion_tension),
SADNESS(R.string.emotion_sadness),
}
🤖 Prompt for AI Agents
In core/model/src/main/kotlin/com/ninecraft/booket/core/model/EmotionModel.kt
around lines 11 to 18, the Emotion enum currently hardcodes Korean strings in
displayName, which prevents multilingual support. Remove the direct Korean
strings and instead store string resource IDs (e.g., @StringRes Int) in the
enum. Then, add corresponding keys like emotion_warm, emotion_joy, etc., to the
strings.xml resource file under core/common. Update the UI layer to resolve
these resource IDs using stringResource() or equivalent for proper localization.

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.

core:model 은 kotlin 모듈이라 R 참조 불가능, 그리고 일단은 다국어 지원 예정은 없어

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 말씀하신대로 맞습니다! core:model이 순수 Kotlin 모듈이라면 Android 리소스 접근이 불가능하고, 다국어 지원 계획이 없다면 현재 구조가 적절하네요. 제안을 철회하겠습니다.


✏️ Learnings added
Learnt from: easyhooon
PR: YAPP-Github/Reed-Android#88
File: core/model/src/main/kotlin/com/ninecraft/booket/core/model/EmotionModel.kt:11-18
Timestamp: 2025-07-31T16:58:59.378Z
Learning: Reed-Android 프로젝트에서 core:model 모듈은 순수 Kotlin 모듈이므로 Android 리소스(R.string 등)에 접근할 수 없다.

Learnt from: easyhooon
PR: YAPP-Github/Reed-Android#88
File: core/model/src/main/kotlin/com/ninecraft/booket/core/model/EmotionModel.kt:11-18
Timestamp: 2025-07-31T16:58:59.378Z
Learning: Reed-Android 프로젝트는 현재 다국어 지원 계획이 없어서 모델에 한글 문자열을 직접 포함하는 것이 허용된다.

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.


🧠 Learnings used
Learnt from: easyhooon
PR: YAPP-Github/Reed-Android#61
File: feature/webview/build.gradle.kts:17-21
Timestamp: 2025-07-20T12:34:23.786Z
Learning: Reed-Android 프로젝트에서는 `booket.android.feature` convention plugin을 사용하여 feature 모듈들의 공통 의존성을 관리한다. 이 plugin은 Circuit, Compose, 그리고 core 모듈들의 의존성을 자동으로 포함하므로, 각 feature 모듈의 build.gradle.kts에서는 특별한 의존성(예: libs.logger, libs.kakao.auth)만 별도로 선언하면 된다.

Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,13 @@ import javax.inject.Singleton
private const val MaxTimeoutMillis = 15_000L

private val jsonRule = Json {
// 기본값도 JSON에 포함하여 직렬화
encodeDefaults = true
// JSON에 정의되지 않은 키는 무시 (역직렬화 시 에러 방지)
ignoreUnknownKeys = true
// JSON을 보기 좋게 들여쓰기하여 포맷팅
prettyPrint = true
// 엄격하지 않은 파싱 (따옴표 없는 키, 후행 쉼표 등 허용)
isLenient = true
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
Expand Down Expand Up @@ -68,6 +69,7 @@ fun LoadStateFooter(
}
}

@Immutable
sealed interface FooterState {
data object Idle : FooterState
data object Loading : FooterState
Expand Down
2 changes: 2 additions & 0 deletions feature/detail/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ ksp {

dependencies {
implementations(
libs.kotlinx.collections.immutable,

libs.logger,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import com.ninecraft.booket.core.common.constants.BookStatus
import com.ninecraft.booket.core.common.utils.handleException
import com.ninecraft.booket.core.data.api.repository.BookRepository
import com.ninecraft.booket.feature.screens.BookDetailScreen
import com.ninecraft.booket.feature.screens.LoginScreen
import com.ninecraft.booket.feature.screens.RecordDetailScreen
import com.ninecraft.booket.feature.screens.RecordScreen
import com.orhanobut.logger.Logger
import com.slack.circuit.codegen.annotations.CircuitInject
import com.slack.circuit.retained.rememberRetained
Expand All @@ -30,6 +33,10 @@ class BookDetailPresenter @AssistedInject constructor(
override fun present(): BookDetailUiState {
val scope = rememberCoroutineScope()

var isBookUpdateBottomSheetVisible by rememberRetained { mutableStateOf(false) }
var isRecordSortBottomSheetVisible by rememberRetained { mutableStateOf(false) }
var currentBookStatus by rememberRetained { mutableStateOf(BookStatus.READING) }
var currentRecordSort by rememberRetained { mutableStateOf(RecordSort.PAGE_ASCENDING) }
var sideEffect by rememberRetained { mutableStateOf<BookDetailSideEffect?>(null) }

fun upsertBook(bookIsbn: String, bookStatus: String) {
Expand Down Expand Up @@ -57,29 +64,58 @@ class BookDetailPresenter @AssistedInject constructor(

fun handleEvent(event: BookDetailUiEvent) {
when (event) {
BookDetailUiEvent.InitSideEffect -> {
is BookDetailUiEvent.InitSideEffect -> {
sideEffect = null
}

BookDetailUiEvent.OnBackClicked -> {
is BookDetailUiEvent.OnBackClick -> {
navigator.pop()
}

BookDetailUiEvent.OnBeforeReadingClick -> {
upsertBook(screen.isbn, "BEFORE_READING")
is BookDetailUiEvent.OnBookStatusButtonClick -> {
isBookUpdateBottomSheetVisible = true
}

BookDetailUiEvent.OnReadingClick -> {
upsertBook(screen.isbn, "READING")
is BookDetailUiEvent.OnRegisterRecordButtonClick -> {
navigator.goTo(RecordScreen(""))
}

BookDetailUiEvent.OnCompletedClick -> {
upsertBook(screen.isbn, "COMPLETED")
is BookDetailUiEvent.OnRecordSortButtonClick -> {
isRecordSortBottomSheetVisible = true
}

is BookDetailUiEvent.OnBookUpdateBottomSheetDismiss -> {
isBookUpdateBottomSheetVisible = false
}

is BookDetailUiEvent.OnBookStatusItemSelected -> {
currentBookStatus = event.bookStatus
}

is BookDetailUiEvent.OnBookStatusUpdateButtonClick -> {
upsertBook(screen.isbn, currentBookStatus.value)
}

is BookDetailUiEvent.OnRecordSortBottomSheetDismiss -> {
isRecordSortBottomSheetVisible = false
}

is BookDetailUiEvent.OnRecordSortItemSelected -> {
currentRecordSort = event.sortType
isRecordSortBottomSheetVisible = false
}

is BookDetailUiEvent.OnRecordItemClick -> {
navigator.goTo(RecordDetailScreen(event.recordId))
}
}
}

return BookDetailUiState(
isBookUpdateBottomSheetVisible = isBookUpdateBottomSheetVisible,
isRecordSortBottomSheetVisible = isRecordSortBottomSheetVisible,
currentBookStatus = currentBookStatus,
currentRecordSort = currentRecordSort,
sideEffect = sideEffect,
eventSink = ::handleEvent,
)
Expand Down
Loading
Loading