Skip to content

feat: 도서 상세 화면 Ui 구현#88

Merged
easyhooon merged 15 commits intodevelopfrom
BOOK-188-feature/#81
Aug 1, 2025
Merged

feat: 도서 상세 화면 Ui 구현#88
easyhooon merged 15 commits intodevelopfrom
BOOK-188-feature/#81

Conversation

@easyhooon
Copy link
Copy Markdown
Contributor

@easyhooon easyhooon commented Jul 31, 2025

🔗 관련 이슈

📙 작업 설명

  • 도서 상세 화면 Ui 구현
  • SideEffect @immutable annotation 추가

🧪 테스트 내역 (선택)

  • 주요 기능 정상 동작 확인
  • 브라우저/기기에서 동작 확인
  • 엣지 케이스 테스트 완료
  • 기존 기능 영향 없음

📸 스크린샷 또는 시연 영상 (선택)

KakaoTalk_Video_2025-08-01-01-48-27.mp4

💬 추가 설명 or 리뷰 포인트 (선택)

  • 아직 API가 나오지 않아서 기존에 사용되었던 기록 관련 Model인 RecordRegisterModel을 사용해서 구현해봤습니다.

  • 페이지,최신순 정렬같은 비즈니스 로직은 API 연동할때 같이 구현할게요(임시로 구현한거라 DTO 구조 달라지면 로직도 수정될것같아서)

  • 각각의 Composable 함수가 각 화면의 UiState를 파라미터로 들고 있는데, UiState내에 포함된 sideEffect에 의해 restartable skippable 판정을 받지 못했었습니다. (그외 다른 요인으로는 compose 모듈외에 다른 모듈의 data class를 사용해서 unstable 판정을 받는다던지...) 따라서 sideEffect에 @immutable annotation들을 붙혀주었고(문제가 없다고 판단) restartable skippable 판정을 받을 수 있었습니다.

image
  • 참고로 UiState에는 eventSink도 포함이 되어있는데, CircuitUiEvent는 내부적으로 @immutable 어노테이션이 붙어있어서 stable 판정을 받습니다.
image image image
  • 다른 브랜치에서 확인을 해봤는데 UiState에 내부 프로퍼티에 unstable이 포함되어 있거나, runtime이 있어도 상관 없이, UiState가 stable 판정을 받아서 Ui 컴포저블들이 restartable skippable 판정을 받긴하네여, CircuitUiState가 내부적으로 @stable annotaiton이 붙기도 하고 Strong Skipping Mode가 default로 활성화되어서 그런듯합니다.
    @immutable 딱히 명시적으로 붙힐 필요가 없어진거같군요 Strong Skipping Mode 덕분에

Summary by CodeRabbit

  • 신규 기능

    • 도서 상태 변경, 기록 정렬, 감정 분석 등 상세 화면에 다양한 바텀시트 및 UI 컴포넌트가 추가되었습니다.
    • 감정 분석 결과, 감정 씨앗, 기록 모음 등 새로운 정보와 시각화 요소가 제공됩니다.
    • 기록 상세 화면이 새롭게 제공되며, 기록 저장 후 상세 화면으로 이동합니다.
  • UI/UX 개선

    • 도서 상세 화면의 레이아웃과 상호작용이 개선되었습니다.
    • 도서 상태, 기록 정렬, 감정 정보 등 다양한 상태와 이벤트가 UI에 반영됩니다.
    • 감정별 색상, 텍스트 스타일 등 디자인이 향상되었습니다.
  • 리팩터 및 구조 개선

    • 기록(Review) 관련 명칭이 Record로 통일되어 일관성 있게 변경되었습니다.
    • 공통 도서 상태(BookStatus) 및 감정(Emotion) 모델이 도입되어 여러 화면에서 재사용됩니다.
  • 문서 및 리소스

    • 도서 상태, 기록 정렬, 감정 분석 등 관련 한글 문자열 리소스가 추가 및 정비되었습니다.
    • 불필요한 중복 문자열 리소스가 제거되었습니다.
  • 기타

    • Compose 관련 속성 및 불변성(Immutable) 어노테이션이 추가되어 성능 및 안정성이 향상되었습니다.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jul 31, 2025

Walkthrough

도서 상세 화면의 UI가 대폭 구현되었습니다. 새로운 상태 관리, 다양한 컴포저블 UI 컴포넌트(예: 바텀시트, 감정 분석, 기록 아이템 등), 상태 및 이벤트 모델, 문자열 리소스 추가, 그리고 감정·도서 상태·정렬 기능이 반영되었습니다. 일부 공통 enum 및 모델, 불변 컬렉션 의존성, 패키지 리팩토링도 포함됩니다.

Changes

Cohort / File(s) Change Summary
도서 상세 화면 UI 구현
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt,
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUi.kt,
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUiState.kt,
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/BookUpdateBottomSheet.kt,
.../CollectedSeed.kt,
.../component/EmotionAnalysisResultText.kt,
.../component/RecordItem.kt,
.../component/RecordSortBottomSheet.kt,
.../component/RecordsCollection.kt,
.../component/RecordsCollectionHeader.kt,
.../component/SeedItem.kt
도서 상세 화면의 상태, 이벤트, UI 컴포넌트, 바텀시트, 감정 분석, 기록 컬렉션 등 다양한 컴포저블 및 상태 관리 구현.
공통 모델 및 상수 추가
core/common/src/main/kotlin/com/ninecraft/booket/core/common/constants/BookStatus.kt,
core/model/src/main/kotlin/com/ninecraft/booket/core/model/EmotionModel.kt,
core/common/src/main/kotlin/com/ninecraft/booket/core/common/extensions/Emotion.kt,
core/common/src/main/kotlin/com/ninecraft/booket/core/common/util/EmotionAnalyzer.kt
도서 상태(BookStatus) 및 감정(Emotion, EmotionModel), 감정 분석 유틸리티, 감정 색상 확장 함수 등 공통 모델 및 상수 추가.
문자열 리소스 추가/이동
core/common/src/main/res/values/strings.xml,
feature/detail/src/main/res/values/strings.xml,
feature/search/src/main/res/values/strings.xml
도서 상태, 기록 정렬, 컬렉션, 감정 등 관련 문자열 리소스 추가 및 일부 리소스 삭제(검색 모듈에서 도서 상태 문자열 삭제).
불변 컬렉션 의존성 추가
core/common/build.gradle.kts,
feature/detail/build.gradle.kts
kotlinx.collections.immutable 라이브러리 의존성 추가.
감정/도서 상태 enum 사용 통합
feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/SearchUiState.kt,
feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/component/BookRegisterBottomSheet.kt,
feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/SearchPresenter.kt,
feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/SearchUi.kt
기존 검색 모듈의 BookStatus enum 삭제 및 공통 BookStatus enum 사용으로 통합, 관련 import 및 사용처 변경.
레코드(리뷰) 상세 리팩토링
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/HandleRecordDetailSideEffect.kt,
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailPresenter.kt,
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUi.kt,
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUiState.kt,
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/component/QuoteBox.kt,
.../ReviewBox.kt,
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/review/ReviewDetailUiState.kt
리뷰(Review) 관련 상세 화면 패키지 및 타입을 레코드(Record)로 일괄 리팩토링, UI 상태/이벤트/사이드이펙트/프레젠터 등 변경 및 불필요 파일 삭제.
레코드 등록 및 상세 내비게이션 개선
feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt,
feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUi.kt,
feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUiState.kt,
feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/Screens.kt
레코드 저장 후 상세 화면으로 파라미터(recordId) 전달, 관련 이벤트 및 스크린 타입 개선, UI 이벤트 파라미터화.
불변성 어노테이션 추가
core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/LoadStateFooter.kt,
feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUiState.kt,
feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUiState.kt,
feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/TermsAgreementUiState.kt,
feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsUiState.kt
Compose의 @immutable 어노테이션을 각종 SideEffect sealed interface에 추가.
빌드 및 기타
gradle.properties,
feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/component/LibraryBookItem.kt,
core/network/src/main/kotlin/com/ninecraft/booket/core/network/di/NetworkModule.kt
Compose 컴파일러 메트릭/리포트 활성화, 네트워크 설정 주석 추가, 숫자 변환 방식 소폭 변경 등 기타 사소한 변경.

Sequence Diagram(s)

sequenceDiagram
actor User
participant BookDetailUi
participant BookDetailPresenter
participant BookUpdateBottomSheet
participant RecordSortBottomSheet

User->>BookDetailUi: 도서 상세 화면 진입
BookDetailUi->>BookDetailPresenter: present() 호출
BookDetailPresenter-->>BookDetailUi: UI 상태 반환

User->>BookDetailUi: 도서 상태 버튼 클릭
BookDetailUi->>BookUpdateBottomSheet: 바텀시트 표시
User->>BookUpdateBottomSheet: 상태 선택 및 변경하기 클릭
BookUpdateBottomSheet->>BookDetailUi: 상태 변경 이벤트 전달
BookDetailUi->>BookDetailPresenter: 상태 변경 처리

User->>BookDetailUi: 기록 정렬 버튼 클릭
BookDetailUi->>RecordSortBottomSheet: 바텀시트 표시
User->>RecordSortBottomSheet: 정렬 방식 선택
RecordSortBottomSheet->>BookDetailUi: 정렬 변경 이벤트 전달
BookDetailUi->>BookDetailPresenter: 정렬 방식 변경 처리
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Assessment against linked issues

Objective (issue) Addressed Explanation
도서 상세화면 UI 구현 (#81)
API 연동 제외, UI 구조 및 상태/이벤트/컴포저블 구현 (#81)

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Compose 컴파일러 메트릭/리포트 옵션 추가 (gradle.properties) 빌드 최적화 및 진단 목적의 변경으로, 도서 상세 UI 구현과 직접 관련 없음.
네트워크 모듈 주석 추가 (core/network/src/main/kotlin/com/ninecraft/booket/core/network/di/NetworkModule.kt) 네트워크 설정에 대한 주석 보강으로, 상세 화면 UI 구현과 무관함.
recordCount 문자열 변환 방식 변경 (feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/component/LibraryBookItem.kt) 도서 상세 화면 UI와 직접 관련 없는 라이브러리 아이템 내 표시 방식 미세 변경.

Possibly related PRs

Poem

책장 위에 씨앗이 톡,
감정의 빛깔이 모여들고,
바텀시트가 살포시 열리면
기록과 정렬, 상태도 척척!
토끼도 감탄할 이 UI,
오늘도 코드를 뜀박질해요.
( ˘ᵕ˘ )/📚✨

Note

⚡️ Unit Test Generation is now available in beta!

Learn more here, or try it out under "Finishing Touches" below.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch BOOK-188-feature/#81

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🔭 Outside diff range comments (1)
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUi.kt (1)

50-50: 리소스 키 review_detail_*record_detail_*로 변경 필요

RecordDetailUi.kt(…/feature/detail/record)에서 제목·라벨을 R.string.review_detail_*로 참조하고 있지만, 해당 기능은 “record(독서 기록)” 컨텍스트이므로 리소스 키를 일관되게 변경해야 합니다.

수정 대상:

  • feature/detail/src/main/res/values/strings.xml

    • <string name="review_detail_title">…</string>
    • <string name="review_detail_quote_label">…</string>
    • <string name="review_detail_impression_label">…</string>
      → 각각 record_detail_title, record_detail_quote_label, record_detail_impression_label로 변경
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUi.kt

    • stringResource(R.string.review_detail_title)
    • stringResource(R.string.review_detail_quote_label)
    • stringResource(R.string.review_detail_impression_label)
      R.string.record_detail_* 로 업데이트

예시 diff:

--- a/feature/detail/src/main/res/values/strings.xml
+++ b/feature/detail/src/main/res/values/strings.xml
@@ -1,6 +1,6 @@
-   <string name="review_detail_title">독서 기록</string>
-   <string name="review_detail_quote_label">수집한 문장</string>
-   <string name="review_detail_impression_label">감상평 기록</string>
+   <string name="record_detail_title">독서 기록</string>
+   <string name="record_detail_quote_label">수집한 문장</string>
+   <string name="record_detail_impression_label">감상평 기록</string>
 
--- 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
@@ -50,7 +50,7 @@
             // 기존 코드
-            title = stringResource(R.string.review_detail_title),
+            title = stringResource(R.string.record_detail_title),

             // …
-            quoteLabel = stringResource(R.string.review_detail_quote_label),
+            quoteLabel = stringResource(R.string.record_detail_quote_label),

-            impressionLabel = stringResource(R.string.review_detail_impression_label)
+            impressionLabel = stringResource(R.string.record_detail_impression_label)
🧹 Nitpick comments (19)
feature/detail/build.gradle.kts (1)

19-20: 의존성 중복 여부 확인(선택 사항)
detail 모듈에도 동일한 kotlinx-collections-immutable 를 추가하셨는데, core 모듈에 이미 포함되어 있다면 Gradle configuration cache 측면에서 불필요한 중복이 될 수 있습니다. 멀티-플랫폼/멀티-모듈에서 동일 라이브러리를 여러 모듈에 선언해도 큰 문제는 없지만, 가능하면 공통 모듈에만 선언하여 의존성 트리를 단순화하는 것도 방법입니다.

feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/SearchUi.kt (1)

26-27: toTypedArray() → toImmutableList() 직접 변환으로 불필요한 객체 생성 제거

bookStatuses = BookStatus.entries.toTypedArray().toImmutableList()

중간에 배열을 만들 필요 없이 다음처럼 바로 immutable 리스트로 변환 가능합니다.

-bookStatuses = BookStatus.entries.toTypedArray().toImmutableList()
+bookStatuses = BookStatus.entries.toImmutableList()

불변 리스트에서 요소 수가 많지 않더라도, Compose recomposition 시 객체 수 최소화를 위해 간단히 개선할 수 있습니다.

feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/component/BookRegisterBottomSheet.kt (1)

94-104: 선택 항목에 Role 지정으로 접근성 개선
BookStatusItem에서 .selectable(..., role = null)로 두셨는데, BottomSheet 내 라디오 버튼과 유사한 단일 선택 UX를 의도하신 것이라면 role = Role.RadioButton(또는 Role.Tab)을 지정해 주면 TalkBack/ScreenReader 사용자가 현재 선택 상태를 보다 명확히 인식할 수 있습니다.

-            .selectable(
-                selected = selected,
-                indication = null,
-                role = null,
+            .selectable(
+                selected = selected,
+                indication = null,
+                role = Role.RadioButton,
                 interactionSource = remember { MutableInteractionSource() },

작은 수정으로 접근성을 크게 높일 수 있으니 고려해 보시면 좋겠습니다.

core/common/src/main/kotlin/com/ninecraft/booket/core/common/constants/BookStatus.kt (2)

11-17: 메서드명 개선을 고려해보세요.

현재 getDisplayNameRes() 메서드는 string resource ID를 반환하는데, 메서드명이 이를 명확히 나타내지 못합니다. getDisplayNameResId() 또는 getStringRes()와 같이 더 명확한 이름을 사용하는 것을 고려해보세요.

-    fun getDisplayNameRes(): Int {
+    fun getDisplayNameResId(): Int {

19-23: companion object 이름 생략을 고려해보세요.

Kotlin에서는 companion object의 기본 이름이 Companion이므로 명시적으로 지정할 필요가 없습니다.

-    companion object Companion {
+    companion object {
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt (1)

65-112: 이벤트 처리 로직이 잘 구현되었지만, 메서드 분리를 고려해보세요.

각 이벤트에 대한 처리 로직이 적절하게 구현되어 있습니다. 하지만 handleEvent 메서드가 상당히 길어졌으므로, 관련된 이벤트들을 그룹화하여 별도 함수로 분리하는 것을 고려해보세요.

예를 들어, bottom sheet 관련 이벤트들을 분리할 수 있습니다:

private fun handleBottomSheetEvents(event: BookDetailUiEvent) {
    when (event) {
        is BookDetailUiEvent.OnBookUpdateBottomSheetDismiss -> {
            isBookUpdateBottomSheetVisible = false
        }
        is BookDetailUiEvent.OnRecordSortBottomSheetDismiss -> {
            isRecordSortBottomSheetVisible = false
        }
        // 기타 bottom sheet 관련 이벤트들
    }
}
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/CollectedSeed.kt (1)

90-100: 프리뷰에 샘플 데이터 추가 권장

더 의미 있는 프리뷰를 위해 샘플 감정 데이터를 포함하는 것을 고려해보세요.

 @ComponentPreview
 @Composable
 private fun CollectedSeedPreview() {
     ReedTheme {
         CollectedSeed(
             state = BookDetailUiState(
                 eventSink = {},
+                emotionList = persistentListOf(
+                    EmotionModel(Emotion.WARM, 5),
+                    EmotionModel(Emotion.EXCITING, 3),
+                    EmotionModel(Emotion.TOUCHING, 2)
+                )
             ),
         )
     }
 }
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/SeedItem.kt (1)

26-64: 잘 구현된 감정 시드 아이템!

SeedItem 컴포넌트가 깔끔하게 구현되었습니다:

  • 감정별 색상 확장 함수 활용이 우수함
  • 논리적인 레이아웃 구조와 적절한 간격 설정
  • ReedTheme 일관성 있는 적용

이미지 크기의 일관성을 위해 하드코딩된 50.dp를 테마 스페이싱으로 교체하는 것을 고려해보세요:

 ResourceImage(
     imageRes = designR.drawable.ic_placeholder,
     contentDescription = "Seed Graphic Image",
-    modifier = Modifier.size(50.dp),
+    modifier = Modifier.size(ReedTheme.spacing.spacing12), // 또는 적절한 테마 값
 )
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/RecordsCollection.kt (1)

49-71: 이벤트 처리 및 프리뷰 구현 양호

다음 부분들이 잘 구현되었습니다:

  • toImmutableList() 변환 적절히 사용
  • OnRecordItemClick 이벤트 처리가 올바름
  • 키 사용으로 LazyColumn 성능 최적화

프리뷰에 샘플 데이터를 추가하면 더 의미 있는 미리보기가 가능합니다:

 private fun RecordsCollectionPreview() {
     ReedTheme {
         RecordsCollection(
             state = BookDetailUiState(
                 eventSink = {},
+                recordCollections = listOf(
+                    // 샘플 RecordCollection 데이터 추가
+                )
             ),
         )
     }
 }
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/RecordSortBottomSheet.kt (2)

47-52: 불필요한 람다 래핑 제거

onDismissRequest 람다를 단순히 다시 호출하는 것은 불필요합니다.

-        onDismissRequest = {
-            onDismissRequest()
-        },
+        onDismissRequest = onDismissRequest,

72-78: 접근성 개선을 위해 IconButton 사용 권장

Icon에 clickable modifier를 직접 사용하는 대신 IconButton을 사용하면 더 나은 접근성을 제공할 수 있습니다.

-                Icon(
-                    imageVector = ImageVector.vectorResource(designR.drawable.ic_close),
-                    contentDescription = "Close Icon",
-                    modifier = Modifier.clickable {
-                        onCloseButtonClick()
-                    },
-                )
+                IconButton(onClick = onCloseButtonClick) {
+                    Icon(
+                        imageVector = ImageVector.vectorResource(designR.drawable.ic_close),
+                        contentDescription = "닫기",
+                    )
+                }

추가로 필요한 import:

import androidx.compose.material3.IconButton
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/BookUpdateBottomSheet.kt (3)

55-59: 불필요한 람다 래핑 제거

onDismissRequest 람다를 단순히 다시 호출하는 것은 불필요합니다.

-        onDismissRequest = {
-            onDismissRequest()
-        },
+        onDismissRequest = onDismissRequest,

80-86: 접근성 개선을 위해 IconButton 사용 권장

Icon에 clickable modifier를 직접 사용하는 대신 IconButton을 사용하면 더 나은 접근성을 제공할 수 있습니다.

-                Icon(
-                    imageVector = ImageVector.vectorResource(designR.drawable.ic_close),
-                    contentDescription = "Close Icon",
-                    modifier = Modifier.clickable {
-                        onCloseButtonClick()
-                    },
-                )
+                IconButton(onClick = onCloseButtonClick) {
+                    Icon(
+                        imageVector = ImageVector.vectorResource(designR.drawable.ic_close),
+                        contentDescription = "닫기",
+                    )
+                }

추가로 필요한 import:

import androidx.compose.material3.IconButton

106-107: 중복된 Spacer 통합

연속된 두 개의 Spacer를 하나로 통합할 수 있습니다.

-            Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing3))
-            Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing4))
+            Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing3 + ReedTheme.spacing.spacing4))
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/RecordItem.kt (1)

62-68: TODO: 실제 감정 그래픽 구현 필요

현재 placeholder 이미지를 사용하고 있습니다. 실제 감정 그래픽이 준비되면 교체가 필요합니다.

감정 타입에 따른 실제 이미지 리소스가 준비되면 구현을 도와드릴 수 있습니다. 이 작업을 추적하기 위한 이슈를 생성할까요?

feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUi.kt (1)

193-194: 중복된 Spacer 통합

연속된 두 개의 Spacer를 하나로 통합할 수 있습니다.

-        Spacer(Modifier.height(ReedTheme.spacing.spacing3))
-        Spacer(Modifier.height(ReedTheme.spacing.spacing4))
+        Spacer(Modifier.height(ReedTheme.spacing.spacing3 + ReedTheme.spacing.spacing4))
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUiState.kt (1)

17-62: UI State에 샘플 데이터가 하드코딩됨

BookDetailUiState의 기본값으로 샘플 데이터가 포함되어 있습니다. 실제 운영 환경에서는 빈 리스트로 초기화하는 것이 더 적절할 수 있습니다.

PR 설명에 따르면 API가 준비되지 않아 임시로 구현한 것으로 보입니다. TODO 주석을 추가하여 나중에 제거할 부분임을 명확히 하는 것이 좋겠습니다:

 data class BookDetailUiState(
     val isBookUpdateBottomSheetVisible: Boolean = false,
     val isRecordSortBottomSheetVisible: Boolean = false,
+    // TODO: API 연동 시 빈 리스트로 초기화하도록 수정
     val emotionList: ImmutableList<EmotionModel> = persistentListOf(
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/component/ReviewBox.kt (1)

1-1: 패키지 변경과 함께 컴포넌트명도 일관성 있게 변경 고려

패키지가 record.component로 변경되었지만 컴포넌트명은 여전히 ReviewBox입니다. 전체적인 리팩토링의 일관성을 위해 RecordBox 또는 RecordReviewBox로 이름 변경을 고려해보세요.

feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUi.kt (1)

57-57: 함수명 일관성을 위해 수정이 필요합니다.

ReviewDetailContent 함수명이 RecordDetailContent로 변경되지 않았습니다. 일관된 네이밍을 위해 수정해주세요.

-        ReviewDetailContent(modifier)
+        RecordDetailContent(modifier)

그리고 함수 정의도 함께 변경:

-private fun ReviewDetailContent(modifier: Modifier = Modifier) {
+private fun RecordDetailContent(modifier: Modifier = Modifier) {
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 29c9b14 and 1cbdcc5.

📒 Files selected for processing (37)
  • core/common/build.gradle.kts (1 hunks)
  • core/common/src/main/kotlin/com/ninecraft/booket/core/common/constants/BookStatus.kt (1 hunks)
  • core/common/src/main/kotlin/com/ninecraft/booket/core/common/extensions/Emotion.kt (1 hunks)
  • core/common/src/main/kotlin/com/ninecraft/booket/core/common/util/EmotionAnalyzer.kt (1 hunks)
  • core/common/src/main/kotlin/com/ninecraft/booket/core/common/util/EmotionTextBuilder.kt (1 hunks)
  • core/common/src/main/res/values/strings.xml (1 hunks)
  • core/model/src/main/kotlin/com/ninecraft/booket/core/model/EmotionModel.kt (1 hunks)
  • core/network/src/main/kotlin/com/ninecraft/booket/core/network/di/NetworkModule.kt (1 hunks)
  • feature/detail/build.gradle.kts (1 hunks)
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt (3 hunks)
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUi.kt (1 hunks)
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUiState.kt (2 hunks)
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/BookUpdateBottomSheet.kt (1 hunks)
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/CollectedSeed.kt (1 hunks)
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/RecordItem.kt (1 hunks)
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/RecordSortBottomSheet.kt (1 hunks)
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/RecordsCollection.kt (1 hunks)
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/RecordsCollectionHeader.kt (1 hunks)
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/SeedItem.kt (1 hunks)
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/HandleRecordDetailSideEffect.kt (1 hunks)
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailPresenter.kt (2 hunks)
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUi.kt (4 hunks)
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUiState.kt (1 hunks)
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/component/QuoteBox.kt (1 hunks)
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/component/ReviewBox.kt (1 hunks)
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/review/ReviewDetailUiState.kt (0 hunks)
  • feature/detail/src/main/res/values/strings.xml (1 hunks)
  • feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/component/LibraryBookItem.kt (1 hunks)
  • feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt (2 hunks)
  • feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUi.kt (1 hunks)
  • feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUiState.kt (1 hunks)
  • feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/Screens.kt (1 hunks)
  • feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/SearchPresenter.kt (1 hunks)
  • feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/SearchUi.kt (1 hunks)
  • feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/SearchUiState.kt (1 hunks)
  • feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/component/BookRegisterBottomSheet.kt (1 hunks)
  • feature/search/src/main/res/values/strings.xml (0 hunks)
💤 Files with no reviewable changes (2)
  • feature/search/src/main/res/values/strings.xml
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/review/ReviewDetailUiState.kt
🧰 Additional context used
🧠 Learnings (8)
📚 Learning: reed-android 프로젝트에서는 `booket.android.feature` convention plugin을 사용하여 feature 모듈들의 공통 의존성을 관리한다. 이 p...
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)만 별도로 선언하면 된다.

Applied to files:

  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/component/ReviewBox.kt
  • feature/detail/build.gradle.kts
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/component/QuoteBox.kt
  • feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/SearchUi.kt
  • core/common/build.gradle.kts
  • feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/component/BookRegisterBottomSheet.kt
  • feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUi.kt
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailPresenter.kt
📚 Learning: the project uses kotlinx serialization with json configuration that includes `ignoreunknownkeys = tr...
Learnt from: easyhooon
PR: YAPP-Github/Reed-Android#46
File: core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/BookSearchResponse.kt:33-44
Timestamp: 2025-07-14T00:51:38.952Z
Learning: The project uses Kotlinx Serialization with JSON configuration that includes `ignoreUnknownKeys = true`, `encodeDefaults = true`, `isLenient = true`, and `prettyPrint = true` to handle API response parsing robustly, making additional null safety measures less critical if the API consistently provides all expected fields.

Applied to files:

  • feature/detail/build.gradle.kts
  • core/common/build.gradle.kts
  • core/network/src/main/kotlin/com/ninecraft/booket/core/network/di/NetworkModule.kt
📚 Learning: kotlin에서 같은 패키지 내의 파일들은 패키지 레벨에 정의된 const val 상수를 import 없이 직접 접근할 수 있습니다. onboardingpresenter.kt와 o...
Learnt from: easyhooon
PR: YAPP-Github/Reed-Android#77
File: feature/onboarding/src/main/kotlin/com/ninecraft/booket/feature/onboarding/OnboardingUi.kt:73-73
Timestamp: 2025-07-29T07:02:18.885Z
Learning: Kotlin에서 같은 패키지 내의 파일들은 패키지 레벨에 정의된 const val 상수를 import 없이 직접 접근할 수 있습니다. OnboardingPresenter.kt와 OnboardingUi.kt 모두 com.ninecraft.booket.feature.onboarding 패키지에 속해 있어서 ONBOARDING_STEPS_COUNT 상수를 공유할 수 있습니다.

Applied to files:

  • feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/SearchUi.kt
  • feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/SearchPresenter.kt
  • feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/component/BookRegisterBottomSheet.kt
  • feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/SearchUiState.kt
  • feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt
  • core/common/src/main/kotlin/com/ninecraft/booket/core/common/constants/BookStatus.kt
📚 Learning: in the reed-android project's termsagreementscreen.kt, the ontermdetailclick event is intentionally ...
Learnt from: seoyoon513
PR: YAPP-Github/Reed-Android#35
File: feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/TermsAgreementScreen.kt:127-127
Timestamp: 2025-07-09T01:14:29.836Z
Learning: In the Reed-Android project's TermsAgreementScreen.kt, the OnTermDetailClick event is intentionally passed an empty string for the URL parameter because the actual URLs for terms detail pages haven't been decided yet. This is a temporary implementation that will be updated once the URLs are finalized.

Applied to files:

  • feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUi.kt
  • feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt
  • feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/Screens.kt
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUi.kt
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailPresenter.kt
📚 Learning: reed android 프로젝트에서 타이포그래피 사용 규칙: 톱 앱바(top app bar)에서는 `headline2semibold`를 사용하고, 바텀시트(bottom sheet)...
Learnt from: seoyoon513
PR: YAPP-Github/Reed-Android#45
File: core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/appbar/ReedTopAppBar.kt:65-65
Timestamp: 2025-07-12T01:33:57.101Z
Learning: Reed Android 프로젝트에서 타이포그래피 사용 규칙: 톱 앱바(Top App Bar)에서는 `headline2SemiBold`를 사용하고, 바텀시트(Bottom Sheet)에서는 `heading2SemiBold`를 사용한다. 이는 의도적인 디자인 시스템 차별화이다.

Applied to files:

  • feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/component/BookRegisterBottomSheet.kt
📚 Learning: in the reed-android project, delegatingnavigator was removed because it caused confusion and added u...
Learnt from: easyhooon
PR: YAPP-Github/Reed-Android#52
File: feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/screens/DelegatingNavigator.kt:0-0
Timestamp: 2025-07-16T06:08:40.883Z
Learning: In the Reed-Android project, DelegatingNavigator was removed because it caused confusion and added unnecessary complexity. The simplified approach uses childNavigator only within bottom navigation screens and rootNavigator for all other navigation, which is more intuitive and maintainable.

Applied to files:

  • feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt
  • feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/Screens.kt
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailPresenter.kt
📚 Learning: in the reed-android project using circuit architecture, delegatingnavigator was restored because nav...
Learnt from: easyhooon
PR: YAPP-Github/Reed-Android#52
File: feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/screens/DelegatingNavigator.kt:0-0
Timestamp: 2025-07-16T15:54:19.322Z
Learning: In the Reed-Android project using Circuit architecture, DelegatingNavigator was restored because NavigableCircuitContent can only inject a single navigator, but the dual-navigator architecture (childNavigator for bottom navigation screens, rootNavigator for full-screen screens) requires a delegating component to work within Circuit's constraint.

Applied to files:

  • feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUi.kt
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailPresenter.kt
📚 Learning: 사용자는 kotlinx.serialization에서 json { ignoreunknownkeys = true, islenient = true, encodedefaults = tru...
Learnt from: easyhooon
PR: YAPP-Github/Reed-Android#46
File: core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/BookSearchResponse.kt:7-30
Timestamp: 2025-07-14T00:52:03.723Z
Learning: 사용자는 kotlinx.serialization에서 Json { ignoreUnknownKeys = true, isLenient = true, encodeDefaults = true, prettyPrint = true } 설정을 사용하여 API 응답의 견고성을 높이는 접근 방식을 취하고 있다.

Applied to files:

  • core/network/src/main/kotlin/com/ninecraft/booket/core/network/di/NetworkModule.kt
🧬 Code Graph Analysis (6)
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/BookUpdateBottomSheet.kt (2)
core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedBottomSheet.kt (1)
  • ReedBottomSheet (21-40)
core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ReedButton.kt (1)
  • ReedButton (33-107)
core/common/src/main/kotlin/com/ninecraft/booket/core/common/util/EmotionTextBuilder.kt (1)
core/common/src/main/kotlin/com/ninecraft/booket/core/common/util/EmotionAnalyzer.kt (1)
  • analyzeEmotions (16-31)
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/RecordsCollection.kt (3)
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/RecordsCollectionHeader.kt (1)
  • RecordsCollectionHeader (23-62)
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/RecordItem.kt (1)
  • RecordItem (29-90)
core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Theme.kt (1)
  • ReedTheme (14-21)
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUi.kt (1)
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/HandleRecordDetailSideEffect.kt (1)
  • HandleRecordDetailSideEffects (8-23)
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/SeedItem.kt (2)
core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/ResourceImage.kt (1)
  • ResourceImage (18-40)
core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Theme.kt (1)
  • ReedTheme (14-21)
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/RecordItem.kt (2)
core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/ResourceImage.kt (1)
  • ResourceImage (18-40)
core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Theme.kt (1)
  • ReedTheme (14-21)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ci-build
🔇 Additional comments (24)
core/common/build.gradle.kts (1)

19-20: 불변 컬렉션 의존성 alias 정의 여부 한 번 더 확인해 주세요
libs.kotlinx.collections.immutable 을 core 공통 모듈에 추가한 결정은 👍입니다. 다만 libs.versions.toml 또는 버전 카탈로그에 해당 alias 가 실제로 선언되어 있는지, 그리고 동일한 의존성이 이미 상위 모듈(예: feature/detail)​을 통해 transitively 포함되어 중복되지 않는지 한번 더 검증해 주시면 좋겠습니다.

feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/SearchPresenter.kt (1)

11-11: 중앙화된 BookStatus import 👍 – API 매핑 값 재확인 필요
BookStatus를 공통 상수로 통일한 점 훌륭합니다. 다만 upsertBook() 호출 시 bookStatus.value 문자열을 넘기는데, value 프로퍼티가 실제 API 스펙과 1:1 매핑되는지(예: 서버 enum 이름/코드) 다시 한번 확인해 주세요.

feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/component/LibraryBookItem.kt (1)

102-102: 기존 toString() → 문자열 보간으로 변경 OK

표현식만 변경되었고 기능·퍼포먼스 영향이 없습니다.

feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/SearchUiState.kt (1)

4-4: BookStatus 전역 모듈로 이동 확인 완료

전체 프로젝트 검색 결과, enum class BookStatus 선언은 아래 한 곳에만 존재하여 로컬 중복 선언이 없습니다.
– core/common/src/main/kotlin/com/ninecraft/booket/core/common/constants/BookStatus.kt

따라서 추가 조치 없이 변경을 승인합니다.

core/common/src/main/res/values/strings.xml (1)

1-8: 공통 String 리소스 추가 적절

중복 제거 및 다국어 대응 준비 측면에서 바람직합니다.
향후 다국어 번역 시 values-ko/values-en 등으로 나눠 관리하면 좋겠습니다.

core/network/src/main/kotlin/com/ninecraft/booket/core/network/di/NetworkModule.kt (1)

27-34: 주석 보강으로 직렬화 규칙 가독성 향상

동작 변경 없이 의도 설명이 명확해졌습니다. 👍

core/common/src/main/kotlin/com/ninecraft/booket/core/common/constants/BookStatus.kt (1)

5-9: 잘 구현된 enum 정의입니다.

도서 상태를 명확하게 표현하는 enum 정의가 잘 되어 있고, 각 상수의 value와 이름이 일치하여 가독성이 좋습니다.

feature/detail/src/main/res/values/strings.xml (1)

6-10: 새로운 문자열 리소스가 잘 추가되었습니다.

도서 상세 화면의 새로운 기능들을 위한 문자열 리소스들이 적절하게 추가되었고, 리소스 키 네이밍과 한국어 번역이 자연스럽습니다.

feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt (3)

36-39: 상태 관리가 잘 구현되었습니다.

bottom sheet 가시성과 현재 선택된 값들을 관리하는 상태 변수들이 rememberRetained를 사용하여 적절하게 정의되었습니다.


42-63: API 호출과 에러 핸들링이 잘 구현되었습니다.

upsertBook 함수에서 API 호출, success/failure 케이스 처리, 그리고 handleException을 통한 일관된 에러 핸들링이 잘 구현되어 있습니다.


114-122: UI 상태 반환이 적절합니다.

새로 추가된 상태 프로퍼티들이 BookDetailUiState에 올바르게 전달되고 있습니다.

feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/CollectedSeed.kt (1)

27-88: 깔끔한 구현과 일관성 있는 테마 적용!

CollectedSeed 컴포넌트가 잘 구현되었습니다:

  • ReedTheme을 일관성 있게 사용하여 색상, 간격, 타이포그래피 적용
  • 논리적인 레이아웃 구조 (제목 → SeedItem 목록 → 감정 텍스트 박스)
  • buildEmotionText 유틸리티와의 깔끔한 통합
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/SeedItem.kt (1)

66-77: 의미 있는 샘플 데이터가 포함된 좋은 프리뷰!

EmotionModel 샘플 데이터를 포함하여 컴포넌트의 실제 모습을 잘 보여주는 프리뷰입니다.

core/common/src/main/kotlin/com/ninecraft/booket/core/common/util/EmotionTextBuilder.kt (1)

14-68: 감정 텍스트 빌더의 우수한 구현!

buildEmotionText 함수가 매우 잘 구현되었습니다:

  • remember를 사용한 성능 최적화
  • 세 가지 감정 표시 타입에 대한 적절한 처리
  • 자연스러운 한국어 텍스트와 일관성 있는 스타일링
  • ImmutableList 사용으로 타입 안전성 확보

DUAL 케이스의 쉼표 처리 로직도 정확합니다.

core/common/src/main/kotlin/com/ninecraft/booket/core/common/util/EmotionAnalyzer.kt (2)

5-14: 명확한 데이터 구조 설계!

EmotionAnalysisResult와 EmotionDisplayType이 잘 설계되었습니다:

  • 명확한 역할 분담과 간결한 구조
  • 한국어 주석으로 카테고리 분류 로직이 명확함

16-31: 올바른 감정 분석 로직!

analyzeEmotions 함수가 정확하게 구현되었습니다:

  • 빈 리스트 예외 케이스 적절히 처리
  • 최고 카운트 감정들을 올바르게 필터링
  • 감정 개수에 따른 표시 타입 분류 로직이 정확함
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/component/QuoteBox.kt (1)

1-1: 패키지 변경 적절함

QuoteBox 컴포넌트의 패키지 변경이 적절하며, 컴포넌트명도 record 컨텍스트에서 여전히 의미가 명확합니다.

feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUi.kt (1)

110-110: 빈 문자열 recordId 전달 확인 필요

OnRecordSavedDialogConfirm 이벤트에 빈 문자열을 전달하고 있습니다. 이는 실제 recordId가 없는 상태로 레코드 상세 화면으로 네비게이션할 때 문제가 될 수 있습니다. API 응답에서 받은 실제 recordId를 전달하도록 수정이 필요한지 확인해주세요.

feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt (1)

18-18: import 변경 적절함

ReviewDetailScreen에서 RecordDetailScreen으로의 import 변경이 리팩토링과 일치합니다.

feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUiState.kt (1)

48-48: 이벤트 시그니처 변경 적절함

OnRecordSavedDialogConfirm을 data object에서 recordId 파라미터를 가진 data class로 변경한 것이 적절합니다. 이를 통해 저장된 레코드의 ID를 전달하여 상세 화면으로 올바른 네비게이션이 가능합니다.

feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/Screens.kt (1)

45-45: 매개변수화된 네비게이션으로의 개선이 적절합니다.

ReviewDetailScreen에서 RecordDetailScreen으로의 변경과 recordId 매개변수 추가는 특정 레코드에 대한 네비게이션을 가능하게 하는 좋은 설계입니다. 싱글톤 객체에서 매개변수를 받는 데이터 클래스로의 변경이 적절합니다.

feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/HandleRecordDetailSideEffect.kt (1)

1-23: 일관된 리팩토링으로 잘 변경되었습니다.

Review에서 Record로의 패키지 및 타입 변경이 일관되게 적용되었습니다. 사이드 이펙트 처리 로직은 그대로 유지하면서 타입만 적절히 변경되었습니다.

feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUiState.kt (1)

1-22: Circuit 아키텍처 패턴을 잘 따른 UI 상태 정의입니다.

UI 상태, 사이드 이펙트, 이벤트의 구조가 깔끔하게 정의되었습니다. ShowToast에서 UUID를 사용한 키 생성은 사이드 이펙트 중복 제거를 위한 좋은 패턴입니다. OnCloseClicked 이벤트 네이밍도 적절합니다.

feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUi.kt (1)

77-124: 초기 UI 구현을 위한 하드코딩된 데이터가 적절합니다.

도서 제목("여름은 오래 그곳에 남아"), 저자("미쓰이에 마사시"), 출판사("비채") 등의 하드코딩된 샘플 데이터는 API 연동 전 UI 구현 단계에서 적절한 접근입니다. PR 목표와 일치합니다.

Comment on lines +11 to +18
enum class Emotion(
val displayName: String,
) {
WARM("따뜻함"),
JOY("즐거움"),
TENSION("긴장감"),
SADNESS("슬픔"),
}
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)만 별도로 선언하면 된다.

Comment on lines +136 to 190
NetworkImage(
imageUrl = "",
contentDescription = "Book CoverImage",
modifier = Modifier
.padding(end = ReedTheme.spacing.spacing4)
.width(70.dp)
.height(99.dp)
.clip(RoundedCornerShape(size = ReedTheme.radius.xs)),
placeholder = painterResource(designR.drawable.ic_placeholder),
)
Column(modifier = Modifier.weight(1f)) {
Text(
text = "여름은 오래 그곳에 남아",
color = ReedTheme.colors.contentPrimary,
overflow = TextOverflow.Ellipsis,
maxLines = 2,
style = ReedTheme.typography.headline1SemiBold,
)
ReedButton(
onClick = {
state.eventSink(BookDetailUiEvent.OnReadingClick)
},
sizeStyle = largeButtonStyle,
colorStyle = ReedButtonColorStyle.PRIMARY,
text = "읽는 중",
)
ReedButton(
onClick = {
state.eventSink(BookDetailUiEvent.OnCompletedClick)
},
sizeStyle = largeButtonStyle,
colorStyle = ReedButtonColorStyle.PRIMARY,
text = "독서 완료",
Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = "미쓰이에 마사시",
color = ReedTheme.colors.contentTertiary,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = ReedTheme.typography.label2Regular,
modifier = Modifier.weight(0.7f, fill = false),
)
Spacer(Modifier.width(ReedTheme.spacing.spacing1))
VerticalDivider(
modifier = Modifier.height(14.dp),
thickness = 1.dp,
color = ReedTheme.colors.contentTertiary,
)
Spacer(Modifier.width(ReedTheme.spacing.spacing1))
Text(
text = "비채",
color = ReedTheme.colors.contentTertiary,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = ReedTheme.typography.label2Regular,
modifier = Modifier.weight(0.3f, fill = false),
)
}
Spacer(Modifier.width(ReedTheme.spacing.spacing05))
Text(
text = "2024년",
color = ReedTheme.colors.contentTertiary,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = ReedTheme.typography.label2Regular,
)
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

하드코딩된 도서 정보 사용

도서 제목, 저자, 출판사, 연도 등이 하드코딩되어 있습니다. 또한 도서 상태 버튼의 텍스트도 "읽는 중"으로 하드코딩되어 있습니다. 이러한 정보는 state에서 가져와야 합니다.

PR 설명에서 API가 아직 준비되지 않아 임시로 구현했다고 명시되어 있으므로, TODO 주석을 추가하여 나중에 수정할 부분임을 명확히 하는 것이 좋겠습니다:

+            // TODO: API 연동 시 state에서 도서 정보를 가져오도록 수정
             NetworkImage(
-                imageUrl = "",
+                imageUrl = "", // TODO: state.book.coverImageUrl
                 contentDescription = "Book CoverImage",
+                // TODO: state.book.title
                 Text(
                     text = "여름은 오래 그곳에 남아",
+                // TODO: state.currentBookStatus?.getDisplayNameRes() ?: "상태 없음"
                 text = "읽는 중",

Also applies to: 204-204

🤖 Prompt for AI Agents
In
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUi.kt
around lines 136 to 190 and line 204, the book information such as title,
author, publisher, year, and the reading status button text are hardcoded.
Replace these hardcoded strings with values sourced from the component's state
or view model to reflect dynamic data. Additionally, add TODO comments near
these placeholders to clearly indicate that these are temporary implementations
pending API integration.

Comment on lines +29 to +37
@Suppress("unused")
@Composable
internal fun RecordItem(
quote: String,
emotionTags: ImmutableList<String>,
pageNumber: Int,
createdAt: String,
modifier: Modifier = Modifier,
) {
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.

⚠️ Potential issue

사용하지 않는 매개변수와 하드코딩된 텍스트

emotionTags 매개변수가 전달되지만 사용되지 않고, 대신 하드코딩된 "#감동·공감" 텍스트가 표시됩니다. @Suppress("unused") 주석도 이를 나타냅니다.

-@Suppress("unused")
 @Composable
 internal fun RecordItem(
     quote: String,
     emotionTags: ImmutableList<String>,
     pageNumber: Int,
     createdAt: String,
     modifier: Modifier = Modifier,
 ) {

그리고 실제 emotionTags를 사용하도록 수정:

-                Text(
-                    text = "#감동·공감",
-                    color = ReedTheme.colors.contentBrand,
-                    style = ReedTheme.typography.label1SemiBold,
-                )
+                Text(
+                    text = emotionTags.joinToString(separator = "·") { "#$it" },
+                    color = ReedTheme.colors.contentBrand,
+                    style = ReedTheme.typography.label1SemiBold,
+                )

Also applies to: 72-75

🤖 Prompt for AI Agents
In
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/RecordItem.kt
around lines 29 to 37 and also lines 72 to 75, the emotionTags parameter is
passed but not used, and instead a hardcoded string "#감동·공감" is displayed.
Remove the @Suppress("unused") annotation and replace the hardcoded text with
code that dynamically displays the emotionTags content, ensuring the UI reflects
the actual passed emotion tags.

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 연동할때 text = emotionTags.joinToString(separator = "·") { "#$it" }, 적용하는걸로 할게

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 네, 이해했습니다! API 연동 시점에 emotionTags.joinToString(separator = "·") { "#$it" }로 적용하시는 것이 좋겠네요. 현재는 UI 구현이 우선이고 API가 준비되면 실제 데이터를 연결하는 방식이 합리적입니다.


✏️ Learnings added
Learnt from: easyhooon
PR: YAPP-Github/Reed-Android#88
File: feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/RecordItem.kt:29-37
Timestamp: 2025-07-31T23:17:40.025Z
Learning: Reed-Android 프로젝트에서는 API가 준비되지 않은 상황에서 UI를 먼저 구현하고, API 연동 시점에 하드코딩된 데이터를 실제 데이터로 교체하는 개발 방식을 사용한다. RecordItem 컴포넌트의 emotionTags 매개변수도 API 연동 시점에 `text = emotionTags.joinToString(separator = "·") { "#$it" }`로 적용될 예정이다.

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

Comment on lines +23 to +62
@Composable
internal fun RecordsCollectionHeader(
state: BookDetailUiState,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Row {
Text(
text = stringResource(R.string.record_collection),
color = ReedTheme.colors.contentPrimary,
style = ReedTheme.typography.headline2SemiBold,
)
Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing1))
Text(
text = "${state.recordCollections.size}",
color = ReedTheme.colors.contentBrand,
style = ReedTheme.typography.headline2SemiBold,
)
}
Row(
modifier = Modifier.clickable {
state.eventSink(BookDetailUiEvent.OnRecordSortButtonClick)
},
) {
Text(
text = stringResource(state.currentRecordSort.getDisplayNameRes()),
color = ReedTheme.colors.contentSecondary,
style = ReedTheme.typography.label1Medium,
)
Icon(
imageVector = ImageVector.vectorResource(designR.drawable.ic_chevron_down),
contentDescription = "Dropdown Icon",
)
}
}
}
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

UI 구현이 잘 되어 있지만 몇 가지 개선사항이 있습니다.

전반적으로 잘 구현된 composable이지만 다음 사항들을 개선할 수 있습니다:

  1. contentDescription이 하드코딩되어 있어 다국어 지원에 문제가 있습니다.
  2. Icontint 속성이 설정되지 않아 테마 색상을 따르지 않을 수 있습니다.
             Icon(
                 imageVector = ImageVector.vectorResource(designR.drawable.ic_chevron_down),
-                contentDescription = "Dropdown Icon",
+                contentDescription = stringResource(R.string.dropdown_icon_description), // strings.xml에 추가 필요
+                tint = ReedTheme.colors.contentSecondary,
             )
🤖 Prompt for AI Agents
In
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/RecordsCollectionHeader.kt
between lines 23 and 62, the Icon's contentDescription is hardcoded and the tint
attribute is missing. To fix this, replace the hardcoded contentDescription with
a string resource for localization support, and add a tint property to the Icon
using the appropriate theme color to ensure it follows the app's color scheme.

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.

tint 는 전부 적용해뒀음

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 네, 맞습니다! 학습된 내용을 보니 Reed Android 프로젝트에서는 아이콘의 tint 속성을 Color.Unspecified로 설정하여 아이콘 고유의 색상을 보존하는 정책을 사용하고 계시는군요. 제가 tint 속성 추가를 제안한 것은 부적절했습니다.

그렇다면 contentDescription의 하드코딩 이슈만 남아있는 것 같습니다. 다국어 지원을 위해 "Dropdown Icon" 대신 string resource를 사용하는 것을 고려해보시겠어요?

contentDescription = stringResource(R.string.dropdown_icon_description)

🧠 Learnings used
Learnt from: easyhooon
PR: YAPP-Github/Reed-Android#88
File: feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/RecordsCollection.kt:25-59
Timestamp: 2025-07-31T23:22:02.687Z
Learning: BookDetailScreen의 RecordsCollection 컴포넌트는 상위 Column의 verticalScroll과 LazyColumn의 무한 스크롤을 동시에 지원해야 하는 중첩 스크롤 시나리오입니다. LazyColumn에 고정 높이를 설정하지 않으면 앱이 충돌하는 이슈가 있어서, 현재는 높이를 계산하여 설정하고 userScrollEnabled = false로 구현되어 있습니다. 향후 InfiniteLazyColumn과 nestedScrollConnection을 도입하여 전체 화면 스크롤 + LazyColumn 무한 스크롤을 지원할 예정입니다.

Learnt from: easyhooon
PR: YAPP-Github/Reed-Android#55
File: core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/ReedTextField.kt:0-0
Timestamp: 2025-07-17T23:43:38.687Z
Learning: Reed Android 프로젝트에서 아이콘의 tint 속성을 Color.Unspecified로 설정하는 이유는 theme 색상이 아이콘에 적용되어 아이콘 고유의 색상이 변경되는 것을 방지하기 위함이다. 이는 아이콘의 원본 색상을 보존하려는 디자인 시스템 정책과 일치한다.

Learnt from: seoyoon513
PR: YAPP-Github/Reed-Android#45
File: core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/appbar/ReedTopAppBar.kt:65-65
Timestamp: 2025-07-12T01:33:57.101Z
Learning: Reed Android 프로젝트에서 타이포그래피 사용 규칙: 톱 앱바(Top App Bar)에서는 `headline2SemiBold`를 사용하고, 바텀시트(Bottom Sheet)에서는 `heading2SemiBold`를 사용한다. 이는 의도적인 디자인 시스템 차별화이다.

Learnt from: easyhooon
PR: YAPP-Github/Reed-Android#88
File: feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/RecordItem.kt:29-37
Timestamp: 2025-07-31T23:17:40.025Z
Learning: Reed-Android 프로젝트에서는 API가 준비되지 않은 상황에서 UI를 먼저 구현하고, API 연동 시점에 하드코딩된 데이터를 실제 데이터로 교체하는 개발 방식을 사용한다. RecordItem 컴포넌트의 emotionTags 매개변수도 API 연동 시점에 `text = emotionTags.joinToString(separator = "·") { "#$it" }`로 적용될 예정이다.

Learnt from: easyhooon
PR: YAPP-Github/Reed-Android#55
File: core/designsystem/src/main/res/drawable/ic_x_circle.xml:6-11
Timestamp: 2025-07-17T23:41:33.929Z
Learning: Reed Android 프로젝트에서는 SVG에서 vector drawable로 변환시 색상 매핑 대신 모드별 별도 아이콘 파일을 사용하는 방식을 선호한다. Figma에서 SVG export한 형태를 그대로 사용하며, 다크모드 대응이 필요할 경우 drawable-night 폴더에 별도 아이콘을 배치하는 방식을 사용한다.

Comment on lines +18 to 47
class RecordDetailPresenter @AssistedInject constructor(
@Assisted private val navigator: Navigator,
) : Presenter<ReviewDetailUiState> {
) : Presenter<RecordDetailUiState> {

@Suppress("unused")
@Composable
override fun present(): ReviewDetailUiState {
override fun present(): RecordDetailUiState {
val scope = rememberCoroutineScope()

var sideEffect by rememberRetained { mutableStateOf<ReviewDetailSideEffect?>(null) }
var sideEffect by rememberRetained { mutableStateOf<RecordDetailSideEffect?>(null) }

fun handleEvent(event: ReviewDetailUiEvent) {
fun handleEvent(event: RecordDetailUiEvent) {
when (event) {
ReviewDetailUiEvent.OnBackClicked -> {
RecordDetailUiEvent.OnCloseClicked -> {
navigator.pop()
}
}
}
return ReviewDetailUiState(
return RecordDetailUiState(
sideEffect = sideEffect,
eventSink = ::handleEvent,
)
}
}

@CircuitInject(ReviewDetailScreen::class, ActivityRetainedComponent::class)
@CircuitInject(RecordDetailScreen::class, ActivityRetainedComponent::class)
@AssistedFactory
fun interface Factory {
fun create(navigator: Navigator): ReviewDetailPresenter
fun create(navigator: Navigator): RecordDetailPresenter
}
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.

💡 Verification agent

🧩 Analysis chain

recordId 매개변수 활용 확인이 필요합니다.

Presenter가 Circuit 패턴을 잘 따르고 있지만, RecordDetailScreenrecordId 매개변수가 현재 사용되지 않고 있습니다. 실제 레코드 데이터를 로드하는 로직이 아직 구현되지 않은 것으로 보입니다.

다음 스크립트로 recordId 사용 현황을 확인해보겠습니다:


🏁 Script executed:

#!/bin/bash
# Description: RecordDetailScreen의 recordId 매개변수가 어떻게 사용되는지 확인

# RecordDetailScreen 사용처 검색
rg -A 5 "RecordDetailScreen" --type kotlin

# recordId 사용처 검색
rg -A 3 -B 3 "recordId" feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/

Length of output: 7243


RecordDetailPresenter에 recordId 매개변수 전달 및 활용 필요

RecordDetailScreen에 선언된 recordIdRecordDetailPresenter 생성자와 Factory에 누락되어 실제 데이터 로딩에 사용되지 않고 있습니다. 다음과 같이 수정해주세요:

  • RecordDetailPresenter 생성자에 @Assisted private val recordId: String 추가
  • Factory의 create 메서드 시그니처에 recordId: String 인자 추가
  • present() 내부에서 recordId를 활용한 레코드 로딩 로직 구현

예시:

 class RecordDetailPresenter @AssistedInject constructor(
-    @Assisted private val navigator: Navigator,
+    @Assisted private val navigator: Navigator,
+    @Assisted private val recordId: String,
 ) : Presenter<RecordDetailUiState> {
     @Composable
     override fun present(): RecordDetailUiState {
+        // TODO: recordId를 이용해 레코드 상세 로드
         …
     }
 }

 @AssistedFactory
 fun interface Factory {
-    fun create(navigator: Navigator): RecordDetailPresenter
+    fun create(navigator: Navigator, recordId: String): RecordDetailPresenter
 }
📝 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
class RecordDetailPresenter @AssistedInject constructor(
@Assisted private val navigator: Navigator,
) : Presenter<ReviewDetailUiState> {
) : Presenter<RecordDetailUiState> {
@Suppress("unused")
@Composable
override fun present(): ReviewDetailUiState {
override fun present(): RecordDetailUiState {
val scope = rememberCoroutineScope()
var sideEffect by rememberRetained { mutableStateOf<ReviewDetailSideEffect?>(null) }
var sideEffect by rememberRetained { mutableStateOf<RecordDetailSideEffect?>(null) }
fun handleEvent(event: ReviewDetailUiEvent) {
fun handleEvent(event: RecordDetailUiEvent) {
when (event) {
ReviewDetailUiEvent.OnBackClicked -> {
RecordDetailUiEvent.OnCloseClicked -> {
navigator.pop()
}
}
}
return ReviewDetailUiState(
return RecordDetailUiState(
sideEffect = sideEffect,
eventSink = ::handleEvent,
)
}
}
@CircuitInject(ReviewDetailScreen::class, ActivityRetainedComponent::class)
@CircuitInject(RecordDetailScreen::class, ActivityRetainedComponent::class)
@AssistedFactory
fun interface Factory {
fun create(navigator: Navigator): ReviewDetailPresenter
fun create(navigator: Navigator): RecordDetailPresenter
}
class RecordDetailPresenter @AssistedInject constructor(
@Assisted private val navigator: Navigator,
@Assisted private val recordId: String,
) : Presenter<RecordDetailUiState> {
@Suppress("unused")
@Composable
override fun present(): RecordDetailUiState {
val scope = rememberCoroutineScope()
var sideEffect by rememberRetained { mutableStateOf<RecordDetailSideEffect?>(null) }
// TODO: recordId를 이용해 레코드 상세 로드
fun handleEvent(event: RecordDetailUiEvent) {
when (event) {
RecordDetailUiEvent.OnCloseClicked -> {
navigator.pop()
}
}
}
return RecordDetailUiState(
sideEffect = sideEffect,
eventSink = ::handleEvent,
)
}
}
@CircuitInject(RecordDetailScreen::class, ActivityRetainedComponent::class)
@AssistedFactory
fun interface Factory {
fun create(navigator: Navigator, recordId: String): RecordDetailPresenter
}
🤖 Prompt for AI Agents
In
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailPresenter.kt
lines 18 to 47, the RecordDetailPresenter constructor and Factory are missing
the recordId parameter, which is declared in RecordDetailScreen but not passed
through, preventing data loading. Fix this by adding @Assisted private val
recordId: String to the constructor, updating the Factory's create method to
accept recordId: String, and then use this recordId inside the present()
function to implement the record loading logic.

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.

Circuit 알못이네

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

@easyhooon 맞습니다, Circuit 패턴에 대한 제 이해가 부족했네요! 😅

Circuit에서는 @CircuitInject를 통해 Screen 객체 전체가 자동으로 주입되므로, Screen의 매개변수들을 별도로 생성자에 추가할 필요가 없군요. RecordDetailScreen(recordId) 객체 자체가 Presenter에 전달되어 접근할 수 있을 것 같습니다.

올바른 Circuit 패턴 사용법에 대해 알려주시면 감사하겠습니다! 🙏


🌐 Web query:

Slack Circuit Android architecture screen parameters presenter injection

💡 Result:

It looks like you're asking about implementing screen parameters and presenter injection in an Android app using the architecture style inspired by Slack's Circuit framework. Circuit is a Kotlin Multiplatform UI framework developed by Slack, intended for building UIs declaratively with separation of concerns such as screens, presenters, and navigation.

In Circuit, the architecture usually involves:

  • Screens: Represent the UI and hold the screen parameters/data.
  • Presenters: Handle business logic and provide data to the UI.
  • Injection: Presenters are injected or created with dependencies using dependency injection.

Here's a basic outline of how to manage screen parameters and presenter injection in Circuit-style architecture on Android:

1. Define Screen with Parameters

You define a screen class with parameters needed for that screen.

import com.slack.circuit.runtime.Screen

data class MyScreen(
  val userId: String
) : Screen

2. Define Presenter for the Screen

Create a presenter that receives the screen parameters and other dependencies via constructor injection.

import com.slack.circuit.runtime.Presenter
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf

class MyScreenPresenter(
  private val userRepository: UserRepository,
  private val screen: MyScreen
) : Presenter<MyScreen, MyScreenState> {
  
  override fun present(): Flow<MyScreenState> {
    val user = userRepository.getUser(screen.userId)
    return flowOf(MyScreenState(user))
  }
}

data class MyScreenState(
  val user: User
)

3. Inject Presenter Using a Dependency Injection Framework

You would typically use a DI framework like Dagger, Hilt, Koin, or manual injection to provide the presenter and its dependencies.

For example, in Hilt:

@Module
@InstallIn(SingletonComponent::class)
object MyModule {
  
  @Provides
  fun provideUserRepository(): UserRepository = UserRepositoryImpl()

  @Provides
  fun provideMyScreenPresenter(
    userRepository: UserRepository,
    screen: MyScreen // Screen can be passed in by the router or navigation controller
  ): MyScreenPresenter {
    return MyScreenPresenter(userRepository, screen)
  }
}

4. Wiring it in the UI Layer

In your UI composable or Fragment, obtain the presenter and feed the screen parameters.

@Composable
fun MyScreenView(screen: MyScreen, presenter: MyScreenPresenter = remember { ... }) {
  val state by presenter.present().collectAsState(initial = MyScreenState(User.EMPTY))

  // Render UI with state.user
}

Summary

  • Define your screens as data classes with parameters.
  • Create presenters with dependencies and a reference to the screen.
  • Use DI to inject or create presenters with the correct screen parameters.
  • In your UI, invoke the presenter and update the UI accordingly.

If you want, I can help you with more concrete code examples or integration with a specific DI framework. Would you like that?


@easyhooon 정말 죄송합니다! 웹 검색을 통해 Circuit 패턴을 다시 확인해보니 제가 완전히 잘못 이해하고 있었네요. 😅

올바른 Circuit 패턴에서는 Screen 객체 자체가 Presenter에 주입되어야 합니다:

class RecordDetailPresenter @AssistedInject constructor(
    @Assisted private val navigator: Navigator,
    @Assisted private val screen: RecordDetailScreen, // Screen 객체 직접 주입
) : Presenter<RecordDetailUiState> {

    @Composable
    override fun present(): RecordDetailUiState {
        val recordId = screen.recordId // Screen 객체를 통해 매개변수 접근
        
        // TODO: recordId를 이용해 레코드 상세 로드
        
        // ... 나머지 로직
    }
}

@AssistedFactory
fun interface Factory {
    fun create(navigator: Navigator, screen: RecordDetailScreen): RecordDetailPresenter
}

Circuit에서는 @CircuitInject가 Screen 객체 전체를 자동으로 처리해주는 방식이군요. 잘못된 조언을 드려서 정말 죄송합니다! 🙏


✏️ Learnings added
Learnt from: easyhooon
PR: YAPP-Github/Reed-Android#88
File: feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailPresenter.kt:18-47
Timestamp: 2025-07-31T23:30:37.524Z
Learning: In Circuit architecture, screen parameters are not manually passed to presenter constructors. The @CircuitInject annotation automatically handles the injection of the entire Screen object, making screen parameters accessible without explicitly adding them to the constructor or factory methods.

Learnt from: easyhooon
PR: YAPP-Github/Reed-Android#88
File: feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailPresenter.kt:18-47
Timestamp: 2025-07-31T23:30:37.524Z
Learning: In Circuit architecture, presenters receive the Screen object directly as a constructor parameter (e.g., @Assisted private val screen: RecordDetailScreen), and screen parameters are accessed through this screen object (e.g., screen.recordId). Screen parameters should not be added as separate constructor parameters.

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 removed
Learnt from: easyhooon
PR: YAPP-Github/Reed-Android#52
File: feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/screens/DelegatingNavigator.kt:0-0
Timestamp: 2025-07-16T06:08:40.883Z
Learning: In the Reed-Android project, DelegatingNavigator was removed because it caused confusion and added unnecessary complexity. The simplified approach uses childNavigator only within bottom navigation screens and rootNavigator for all other navigation, which is more intuitive and maintainable.

🧠 Learnings used
Learnt from: easyhooon
PR: YAPP-Github/Reed-Android#52
File: feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/screens/DelegatingNavigator.kt:0-0
Timestamp: 2025-07-16T15:54:19.322Z
Learning: In the Reed-Android project using Circuit architecture, DelegatingNavigator was restored because NavigableCircuitContent can only inject a single navigator, but the dual-navigator architecture (childNavigator for bottom navigation screens, rootNavigator for full-screen screens) requires a delegating component to work within Circuit's constraint.

Learnt from: easyhooon
PR: YAPP-Github/Reed-Android#52
File: feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/screens/DelegatingNavigator.kt:0-0
Timestamp: 2025-07-16T06:08:40.883Z
Learning: In the Reed-Android project, DelegatingNavigator was removed because it caused confusion and added unnecessary complexity. The simplified approach uses childNavigator only within bottom navigation screens and rootNavigator for all other navigation, which is more intuitive and maintainable.

Learnt from: seoyoon513
PR: YAPP-Github/Reed-Android#35
File: feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/TermsAgreementScreen.kt:127-127
Timestamp: 2025-07-09T01:14:29.836Z
Learning: In the Reed-Android project's TermsAgreementScreen.kt, the OnTermDetailClick event is intentionally passed an empty string for the URL parameter because the actual URLs for terms detail pages haven't been decided yet. This is a temporary implementation that will be updated once the URLs are finalized.

isRecordSavedDialogVisible = false
navigator.pop()
navigator.goTo(ReviewDetailScreen)
navigator.goTo(RecordDetailScreen(event.recordId))
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

recordId 설정 로직 누락 확인

RecordDetailScreen으로 네비게이션할 때 event.recordId를 사용하고 있지만, postRecord API 성공 콜백에서 실제 recordId를 설정하는 로직이 보이지 않습니다. API 응답에서 받은 recordId가 이벤트를 통해 전달되도록 수정이 필요할 수 있습니다.

다음 스크립트로 API 응답 구조를 확인해보세요:


🏁 Script executed:

#!/bin/bash
# Description: Check the postRecord API response structure to verify recordId handling

# Search for RecordRepository postRecord method implementation
ast-grep --pattern $'postRecord(
  $$$
) {
  $$$
}'

# Search for record API response models
rg -A 10 "data class.*Record.*Response"

Length of output: 1476


다음 스크립트로 RecordRegisterPresenter.kt에서 postRecord 호출부 및 응답 매핑 로직을 확인해주세요:


🏁 Script executed:

#!/bin/bash
# RecordRegisterPresenter에서 postRecord 호출 위치 확인
rg -n "postRecord" feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt

# RecordRegisterResponse.id를 사용하는 매핑 로직 확인
rg -n "RecordRegisterResponse" feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt

Length of output: 368


다음 스크립트로 RecordRegisterPresenter.kt에서 repository.postRecord 호출부와 네비게이션 로직이 위치한 콘텍스트를 확인해주세요:


🏁 Script executed:

#!/bin/bash
rg -n -C5 "repository.postRecord" feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt

Length of output: 561


다음 스크립트로 RecordRegisterPresenter.kt에서 repository.postRecord 호출부 전후 20줄을 확인해주세요:


🏁 Script executed:

#!/bin/bash
rg -n -C20 "repository.postRecord" feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt

Length of output: 1689


Record 저장 성공 후 recordId 설정 로직 추가 필요

postRecord()onSuccess 콜백에서 API 응답인 RecordRegisterResponse.id를 상태에 저장하고, 다이얼로그 확인 시 해당 recordId로 네비게이션하도록 수정해야 합니다.
현재는 단순히 isRecordSavedDialogVisible = true만 설정되어 있어, 이후 호출되는

navigator.goTo(RecordDetailScreen(event.recordId))

에서 event.recordId가 할당되지 않은 상태입니다.

  • 파일:
    feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt
  • 수정 위치:
    postRecord() 호출부의 onSuccess 블록 (약 103행)

예시 수정안:

-        ).onSuccess {
-            isRecordSavedDialogVisible = true
-        }
+        ).onSuccess { response ->
+            // API 응답에서 받은 id를 상태에 저장
+            savedRecordId = response.id
+            isRecordSavedDialogVisible = true
+        }

이후 다이얼로그 확인 액션에서:

navigator.goTo(RecordDetailScreen(savedRecordId))
🤖 Prompt for AI Agents
In
feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt
around line 103, the onSuccess callback of postRecord() does not save the
returned RecordRegisterResponse.id to the state. Modify the onSuccess block to
store the received recordId in the state, then update the dialog confirmation
logic to navigate using this saved recordId instead of event.recordId, ensuring
the navigator.goTo call uses the correct, assigned recordId.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUiState.kt (1)

88-105: 이전 리뷰 코멘트의 이슈가 해결되었습니다.

RecordSort enum의 value가 이제 enum 이름과 일치하도록 수정되어 이전에 지적되었던 혼란 요소가 해결되었습니다. getDisplayNameRes()fromValue() 메서드도 적절하게 구현되어 있습니다.

🧹 Nitpick comments (1)
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUiState.kt (1)

37-62: 샘플 데이터의 다양성을 개선해보세요.

recordCollections의 샘플 데이터가 모두 동일한 인용문을 사용하고 있어 UI 테스트 시 다양한 시나리오를 확인하기 어려울 수 있습니다. 서로 다른 길이의 텍스트나 다양한 내용을 사용하면 레이아웃 테스트에 더 유용할 것입니다.

 val recordCollections: ImmutableList<RecordRegisterModel> = persistentListOf(
     RecordRegisterModel(
         id = "0",
         pageNumber = 12,
-        quote = ""책을 읽으면 차분해지며 숲으로 둘러싸인 여름 별장 속으로 간 것 같은 기분이 든다. 그 곳에서 그들이 품은 건축에 대한 이상과 삶을 구경하는 것만으로도 충분했다.책을 읽으면 차분해지며 숲으로 둘러싸인 여름 별장 속으로 간 것 같은 기분이 든다. 그 곳에서 그들이 품은 건축에 대한 이상과 삶을 구경하는 것만으로도 충분했다."",
+        quote = ""짧은 인용문 예시"",
         createdAt = "2025.06.25",
     ),
     RecordRegisterModel(
         id = "1",
         pageNumber = 13,
-        quote = ""책을 읽으면 차분해지며 숲으로 둘러싸인 여름 별장 속으로 간 것 같은 기분이 든다. 그 곳에서 그들이 품은 건축에 대한 이상과 삶을 구경하는 것만으로도 충분했다.책을 읽으면 차분해지며 숲으로 둘러싸인 여름 별장 속으로 간 것 같은 기분이 든다. 그 곳에서 그들이 품은 건축에 대한 이상과 삶을 구경하는 것만으로도 충분했다."",
+        quote = ""중간 길이의 인용문으로 여러 줄에 걸쳐 표시될 수 있는 텍스트입니다."",
         createdAt = "2025.06.25",
     ),
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1cbdcc5 and db2d497.

📒 Files selected for processing (2)
  • core/common/src/main/kotlin/com/ninecraft/booket/core/common/extensions/Emotion.kt (1 hunks)
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUiState.kt (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • core/common/src/main/kotlin/com/ninecraft/booket/core/common/extensions/Emotion.kt
🔇 Additional comments (3)
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUiState.kt (3)

3-12: 새로운 import 문들이 적절하게 구성되었습니다.

추가된 import 문들이 모두 코드에서 사용되고 있으며, 모듈 구조에 맞게 잘 정리되어 있습니다.


67-72: 사이드 이펙트 구현이 적절합니다.

UUID를 사용한 고유 키 생성과 표준적인 sealed interface 패턴이 잘 적용되어 있습니다.


74-86: UI 이벤트 구조가 잘 설계되었습니다.

이벤트들이 세분화되어 있고 각각의 목적이 명확하며, 일관된 네이밍 패턴을 따르고 있어 유지보수에 유리합니다.

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),
}


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

state: BookDetailUiState,
modifier: Modifier = Modifier,
) {
LazyColumn(
Copy link
Copy Markdown
Contributor

@seoyoon513 seoyoon513 Jul 31, 2025

Choose a reason for hiding this comment

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

여기 데이터 페이징으로 가져와서 API 연동할 때 수정이 필요할 것 같아요
https://api.qweasdzxc.shop/swagger-ui/index.html#/Reading%20Records/getReadingRecords

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

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUiState.kt (1)

15-66: UI 상태 구조가 잘 설계되어 있습니다.

불변 컬렉션 사용과 상태 관리 구조가 Compose 모범 사례를 따르고 있습니다. API 통합 전 단계에서 사용하는 목업 데이터도 적절합니다.

다만, 샘플 데이터의 quote 필드에 동일한 긴 텍스트가 반복되고 있는데, 테스트 목적이라면 다양한 길이의 샘플 텍스트를 사용하는 것을 고려해보세요.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between db2d497 and 0587dc0.

📒 Files selected for processing (10)
  • core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/LoadStateFooter.kt (2 hunks)
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUiState.kt (2 hunks)
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUiState.kt (1 hunks)
  • feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUiState.kt (2 hunks)
  • feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUiState.kt (2 hunks)
  • feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/TermsAgreementUiState.kt (2 hunks)
  • feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUiState.kt (3 hunks)
  • feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/SearchUiState.kt (2 hunks)
  • feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsUiState.kt (2 hunks)
  • gradle.properties (1 hunks)
✅ Files skipped from review due to trivial changes (6)
  • feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUiState.kt
  • feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/TermsAgreementUiState.kt
  • gradle.properties
  • feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsUiState.kt
  • core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/LoadStateFooter.kt
  • feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUiState.kt
🚧 Files skipped from review as they are similar to previous changes (3)
  • feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/SearchUiState.kt
  • feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUiState.kt
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUiState.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ci-build
🔇 Additional comments (4)
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUiState.kt (4)

3-13: Import 구문이 적절하게 구성되어 있습니다.

필요한 의존성들이 모두 포함되어 있고, 불변 컬렉션 사용을 위한 kotlinx.collections.immutable과 Compose의 @immutable 어노테이션이 적절히 import되어 있습니다.


68-74: 사이드 이펙트 구현이 모범 사례를 따르고 있습니다.

@immutable 어노테이션과 sealed interface 패턴을 적절히 사용하고 있으며, UUID를 통한 고유 키 생성으로 토스트 메시지의 중복을 방지하고 있습니다.


76-88: UI 이벤트 인터페이스가 일관성 있게 잘 설계되어 있습니다.

sealed interface 패턴을 사용하여 타입 안전성을 보장하고 있으며, 이벤트 네이밍이 일관되고 직관적입니다. 모든 필요한 사용자 상호작용을 적절히 커버하고 있습니다.


90-107: RecordSort enum이 올바르게 수정되었습니다.

이전 리뷰에서 지적된 enum 값과 이름의 불일치 문제가 해결되었습니다. 현재 구현은 명확하고 일관성 있는 값을 사용하고 있으며, 디스플레이 이름 리소스 반환과 값 파싱 기능이 적절히 구현되어 있습니다.

Copy link
Copy Markdown
Contributor

@seoyoon513 seoyoon513 left a comment

Choose a reason for hiding this comment

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

일단 자잘한 UI 이슈는 API 연동 때 같이 수정하는걸로!!
여기 UI 작업량 굉장히 많은데 너무 고생하셨어요 👍👍👍 LGTM

sizeStyle = largeButtonStyle,
colorStyle = ReedButtonColorStyle.SECONDARY,
modifier = Modifier.weight(1f),
trailingIcon = {
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.

@seoyoon513 죄송한데 해당 버튼 traillingIcon이 지정된 크기와 비교했을때, 실기기 환경에서 크기가 작아지는 이슈 한번 확인 부탁드림니다!

Copy link
Copy Markdown
Contributor

@seoyoon513 seoyoon513 Aug 1, 2025

Choose a reason for hiding this comment

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

@easyhooon 요거 이것 저것 테스트해보느라 좀 늦었습니다~

버튼 내부도 Row로 되어있기 때문에, Row로 테스트 진행했는데요. Row 내에 있는 자식 측정 방식이 첫 번째 자식 먼저 측정해서 배치하고 남은 공간을 두 번째 자식한테 제공합니다

아래 코드처럼 width 제약을 뒀을 때, 두 Box 모두 24.dp 고정 사이즈를 주었음에도 불구하고 첫 번째 Box(파란색)가 충분히 공간을 차지하고 남은 공간을 두 번째 Box(빨간색)가 차지합니다

image
Row(
    modifier = Modifier
        .width(80.dp) // 자식 컴포저블을 모두 그리기에 부족한 사이즈
        .background(ReedTheme.colors.contentTertiary)
        .padding(
            horizontal = ReedTheme.spacing.spacing5,
            vertical = ReedTheme.spacing.spacing3,
        ),
) {
    Box(
        modifier = Modifier
            .size(24.dp) //사이즈 24.dp로 고정
            .background(Color.Blue),
    )
    Box(
        modifier = Modifier
            .size(24.dp) //사이즈 24.dp로 고정
            .background(Color.Red),
    )
}

그래서 ReedButton도 자식 컴포저블을 그리기 충분한 width를 확보하지 못하면,
leadingIcon이 들어갈 경우 Text 영역이 줄어들고 trailingIcon이 들어가면 icon영역이 줄어드는 현상이 발생합니다.
image
image

자식 컴포저블에 각각 weight(1f)를 주어 사이좋게 반반 차지할 수 있게 만들 수 있습니다만
image

Row(
    modifier = Modifier
        .width(80.dp) // 자식 컴포저블을 모두 그리기에 부족한 사이즈
        .background(ReedTheme.colors.contentTertiary)
        .padding(
            horizontal = ReedTheme.spacing.spacing5,
            vertical = ReedTheme.spacing.spacing3,
        ),
) {
    Box(
        modifier = Modifier
            .size(24.dp) //사이즈 24.dp로 고정
            .weight(1f) // weight 1f
            .background(Color.Blue),
    )
    Box(
        modifier = Modifier
            .size(24.dp) //사이즈 24.dp로 고정
            .weight(1f) // weight 1f
            .background(Color.Red),
    )
}

그걸 적용해도 현재 weight로 정의한 비율로는 읽는중 버튼의 찌그러짐을 막을 수는 없습니다.(그냥 사이 좋게 찌그러짐) 어쨌든 찌그러지지 않기 위한 최소한의 너비는 보장해야하는 상황이에요!
image

따라서 아래 코드처럼 첫 번째 버튼에 within(min = 110.dp) 속성으로 최소한의 너비를 보장해주고, 나머지 버튼이 weight(1f)차지하는 방향으로 수정하는게 어떨까 합니다.

ReedButton(
    onClick = {
        state.eventSink(BookDetailUiEvent.OnBookStatusButtonClick)
    },
    text = "읽는 중",
    sizeStyle = largeButtonStyle,
    colorStyle = ReedButtonColorStyle.SECONDARY,
    modifier = Modifier.widthIn(min = 110.dp), // 멀쩡한 모양을 할 수 있는 최소 너비 설정
    trailingIcon = {
        Icon(
            imageVector = ImageVector.vectorResource(designR.drawable.ic_chevron_down),
            contentDescription = "Dropdown Icon",
            tint = ReedTheme.colors.contentPrimary,
        )
    },
)
Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing2))
ReedButton(
    onClick = {
        state.eventSink(BookDetailUiEvent.OnRegisterRecordButtonClick)
    },
    text = "독서 기록 추가",
    sizeStyle = largeButtonStyle,
    colorStyle = ReedButtonColorStyle.PRIMARY,
    modifier = Modifier.weight(1f), // 나머지 공간 차지
)

시스템 글씨 크기를 키웠을 때, 완벽하지 않지만 첫 번째 버튼은 어느 정도 방어가 됩니다
[글씨 키우기 전]
image
[키운 후]
image

Copy link
Copy Markdown
Contributor Author

@easyhooon easyhooon Aug 1, 2025

Choose a reason for hiding this comment

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

따라서 아래 코드처럼 첫 번째 버튼에 within(min = 110.dp) 속성으로 최소한의 너비를 보장해주고, 나머지 버튼이 weight(1f)차지하는 방향으로 수정하는게 어떨까 합니다.

Row 가 자식의 길이를 측정하는 정책에 대해 예전에 공부했었던거같은데 복습할 수 있었네요bb
역시 내부 동작 방식을 아는게 중요하군요

이 방식이 Best Practices 인거같네여 그렇게 합시다~

Copy link
Copy Markdown
Contributor Author

@easyhooon easyhooon Aug 1, 2025

Choose a reason for hiding this comment

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

사실 글씨 크기도 시스템 폰트 크기에 영향받지않게 고정으로 막아버릴순있는데, Theme 내에 CompositionLocal 로 전역으로 설정 (xml 개발시 텍스트 크기 dp로 적용하는게 국룰이기도했고,, Android에서 권장하는 방식은 아니지만)

고려할 수 도있을것같네여

@easyhooon
Copy link
Copy Markdown
Contributor Author

easyhooon commented Aug 1, 2025

@seoyoon513 누락된 내 기록 모음 EmptyView 까지 추가로 올렸슴다. merge 할게여~

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (2)
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUi.kt (2)

136-190: 하드코딩된 도서 정보 사용

과거 리뷰에서 이미 지적된 사항입니다. 도서 제목, 저자, 출판사, 연도 등이 여전히 하드코딩되어 있습니다.


195-227: 버튼 구현이 적절합니다

이벤트 핸들링과 레이아웃이 올바르게 구성되어 있습니다. 도서 상태 텍스트 하드코딩은 과거 리뷰에서 이미 지적된 사항입니다.

🧹 Nitpick comments (1)
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/CollectedSeed.kt (1)

59-86: 불필요한 border 스타일링을 제거하세요

Box의 background color와 border color가 동일하므로 border가 시각적으로 불필요합니다. 코드를 단순화할 수 있습니다.

             Box(
                 modifier = Modifier
                     .fillMaxWidth()
                     .padding(horizontal = ReedTheme.spacing.spacing4)
                     .clip(RoundedCornerShape(ReedTheme.radius.sm))
                     .background(ReedTheme.colors.basePrimary)
-                    .padding(ReedTheme.spacing.spacing3)
-                    .border(
-                        width = 1.dp,
-                        color = ReedTheme.colors.basePrimary,
-                        shape = RoundedCornerShape(ReedTheme.radius.sm),
-                    ),
+                    .padding(ReedTheme.spacing.spacing3),
             ) {
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c02e98b and 070b7a2.

📒 Files selected for processing (5)
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUi.kt (1 hunks)
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/CollectedSeed.kt (1 hunks)
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/EmotionAnalysisResultText.kt (1 hunks)
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/RecordsCollection.kt (1 hunks)
  • feature/detail/src/main/res/values/strings.xml (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • feature/detail/src/main/res/values/strings.xml
🚧 Files skipped from review as they are similar to previous changes (1)
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/RecordsCollection.kt
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: easyhooon
PR: YAPP-Github/Reed-Android#88
File: feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/RecordItem.kt:29-37
Timestamp: 2025-07-31T23:17:40.025Z
Learning: Reed-Android 프로젝트에서는 API가 준비되지 않은 상황에서 UI를 먼저 구현하고, API 연동 시점에 하드코딩된 데이터를 실제 데이터로 교체하는 개발 방식을 사용한다. RecordItem 컴포넌트의 emotionTags 매개변수도 API 연동 시점에 `text = emotionTags.joinToString(separator = "·") { "#$it" }`로 적용될 예정이다.
📚 Learning: reed-android 프로젝트에서는 api가 준비되지 않은 상황에서 ui를 먼저 구현하고, api 연동 시점에 하드코딩된 데이터를 실제 데이터로 교체하는 개발 방식을 사용한다. ...
Learnt from: easyhooon
PR: YAPP-Github/Reed-Android#88
File: feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/RecordItem.kt:29-37
Timestamp: 2025-07-31T23:17:40.025Z
Learning: Reed-Android 프로젝트에서는 API가 준비되지 않은 상황에서 UI를 먼저 구현하고, API 연동 시점에 하드코딩된 데이터를 실제 데이터로 교체하는 개발 방식을 사용한다. RecordItem 컴포넌트의 emotionTags 매개변수도 API 연동 시점에 `text = emotionTags.joinToString(separator = "·") { "#$it" }`로 적용될 예정이다.

Applied to files:

  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/EmotionAnalysisResultText.kt
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUi.kt
📚 Learning: jetpack compose에서 scale() 변환은 시각적 변환만 적용하며 레이아웃 좌표계는 변경하지 않는다. 따라서 scale(scalex = -1f, scaley = -1f)...
Learnt from: seoyoon513
PR: YAPP-Github/Reed-Android#72
File: feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/component/CameraFrame.kt:21-82
Timestamp: 2025-07-28T18:08:47.298Z
Learning: Jetpack Compose에서 scale() 변환은 시각적 변환만 적용하며 레이아웃 좌표계는 변경하지 않는다. 따라서 scale(scaleX = -1f, scaleY = -1f)로 반전된 아이콘에서 padding()은 원래 레이아웃 기준으로 동작하므로, 시각적으로 올바른 위치를 위해서는 변환 전 좌표계 기준으로 padding을 설정해야 한다.

Applied to files:

  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUi.kt
📚 Learning: bookdetailscreen의 recordscollection 컴포넌트는 상위 column의 verticalscroll과 lazycolumn의 무한 스크롤을 동시에 지원해야 하는...
Learnt from: easyhooon
PR: YAPP-Github/Reed-Android#88
File: feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/RecordsCollection.kt:25-59
Timestamp: 2025-07-31T23:22:02.687Z
Learning: BookDetailScreen의 RecordsCollection 컴포넌트는 상위 Column의 verticalScroll과 LazyColumn의 무한 스크롤을 동시에 지원해야 하는 중첩 스크롤 시나리오입니다. LazyColumn에 고정 높이를 설정하지 않으면 앱이 충돌하는 이슈가 있어서, 현재는 높이를 계산하여 설정하고 userScrollEnabled = false로 구현되어 있습니다. 향후 InfiniteLazyColumn과 nestedScrollConnection을 도입하여 전체 화면 스크롤 + LazyColumn 무한 스크롤을 지원할 예정입니다.

Applied to files:

  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUi.kt
🧬 Code Graph Analysis (2)
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/EmotionAnalysisResultText.kt (2)
core/common/src/main/kotlin/com/ninecraft/booket/core/common/util/EmotionAnalyzer.kt (1)
  • analyzeEmotions (16-31)
core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Theme.kt (1)
  • ReedTheme (14-21)
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/CollectedSeed.kt (2)
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/SeedItem.kt (1)
  • SeedItem (26-64)
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/EmotionAnalysisResultText.kt (1)
  • EmotionAnalysisResultText (27-86)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ci-build
🔇 Additional comments (14)
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/CollectedSeed.kt (5)

26-30: 함수 선언이 적절합니다

internal 가시성과 매개변수 구조가 올바르게 정의되어 있습니다.


31-42: 레이아웃 구성이 올바릅니다

ReedTheme의 spacing과 radius를 일관성 있게 사용하여 적절한 시각적 구조를 만들고 있습니다.


43-49: 제목 표시가 적절합니다

문자열 리소스를 사용한 다국어 지원과 일관된 테마 적용이 올바릅니다.


50-58: 감정 아이템 표시가 적절합니다

SpaceEvenly arrangement를 사용하여 감정 아이템들을 균등하게 배치하는 것이 좋은 접근입니다.


89-99: Preview 함수가 적절합니다

ComponentPreview 애노테이션과 테마 적용이 올바르게 구성되어 있습니다.

feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/EmotionAnalysisResultText.kt (6)

27-34: 함수 선언이 적절합니다

AnnotatedString을 반환하여 스타일이 적용된 텍스트를 생성하는 접근이 올바릅니다.


35-35: remember 사용이 적절합니다

emotions가 변경될 때만 분석을 재실행하여 성능을 최적화하는 좋은 접근입니다.


37-51: SINGLE 케이스 처리가 적절합니다

buildAnnotatedString과 withStyle을 사용한 텍스트 스타일링이 올바르고, 한국어 문장 구성이 자연스럽습니다.


53-73: DUAL 케이스 처리가 적절합니다

forEachIndexed를 사용하여 두 감정을 쉼표로 구분하는 로직이 자연스럽고 올바릅니다.


75-84: BALANCED 케이스 처리가 적절합니다

여러 감정이 균형잡혀 있을 때 일반적인 메시지를 표시하는 접근이 합리적입니다.


88-142: 포괄적인 Preview 함수입니다

모든 3가지 케이스(SINGLE, DUAL, BALANCED)를 테스트할 수 있도록 구성되어 있어 개발 시 매우 유용합니다.

feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUi.kt (3)

51-61: Modal bottom sheet 상태 관리가 적절합니다

두 개의 bottom sheet 상태와 coroutineScope를 올바르게 관리하고 있습니다.


121-125: 컴포넌트 분리가 좋은 접근입니다

BookDetailContent를 별도 함수로 분리하여 코드 구조와 가독성을 개선했습니다.


229-236: 조건부 렌더링 로직이 적절합니다

기록 컬렉션 유무에 따른 CollectedSeed 표시 조건이 합리적으로 구현되어 있습니다.

@easyhooon easyhooon merged commit 5eb114c into develop Aug 1, 2025
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BOOK-188/feat] 도서 상세 화면 UI 구현

2 participants