Skip to content

[API] 피드 API 구현#100

Merged
rbqks529 merged 18 commits intoTHIP-TextHip:developfrom
rbqks529:feat/#95_API_Feeds
Aug 18, 2025
Merged

[API] 피드 API 구현#100
rbqks529 merged 18 commits intoTHIP-TextHip:developfrom
rbqks529:feat/#95_API_Feeds

Conversation

@rbqks529
Copy link
Collaborator

@rbqks529 rbqks529 commented Aug 18, 2025

➕ 이슈 링크


🔎 작업 내용

  • 피드 상단 API 연결
  • 피드 수정 구현
  • 해당 책으로 작성된 피드 조회

📸 스크린샷


😢 해결하지 못한 과제

  • [] 방 검색은 아직 구현하지 못했습니다


📢 리뷰어들에게

  • 참고해야 할 사항들을 적어주세요

Summary by CodeRabbit

  • New Features

    • 피드 수정 기능 도입: 기존 이미지 삭제, 부분 수정(내용/공개/태그) 및 수정 전용 화면 타이틀.
    • 도서 상세에서 관련 피드 목록(정렬, 무한 스크롤) 및 필터 드롭다운 제공.
    • 내 피드 헤더에 프로필·팔로워·총 게시물 정보 반영.
    • 피드 카드의 책 아이콘 클릭으로 도서 상세 이동 및 댓글 화면에서 편집 이동 지원.
  • Improvements

    • 댓글 하단 시트 동작 개선(작성자: 수정·삭제, 비작성자: 신고 토스트).
    • 상단 앱바 아이콘 터치 영역 개선(버튼화).
    • 검색 라이브 딜레이 300ms로 단축.

@coderabbitai
Copy link

coderabbitai bot commented Aug 18, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

피드 수정 요청/응답 모델과 서비스/레포지토리 API를 추가하고, 내 피드 정보·특정 도서 관련 피드 조회를 연동했습니다. 피드 작성 화면에 편집 모드와 기존 이미지 제거를 지원했고, 피드 관련 화면·카드 컴포저블에 내비게이션·콜백을 확장했습니다. 일부 상단 앱바 아이콘 클릭 처리를 IconButton으로 래핑했습니다.

Changes

Cohort / File(s) Summary
Feed API models: request/response
**/data/model/feed/request/UpdateFeedRequest.kt, **/data/model/feed/response/FeedDetailResponse.kt, **/data/model/feed/response/FeedMineInfoResponse.kt, **/data/model/feed/response/RelatedBooksResponse.kt, **/data/model/users/response/UsersMyFollowingsRecentFeedsResponse.kt
신규/확장 데이터 모델 추가: UpdateFeedRequest, FeedMineInfoResponse, RelatedBooksResponse/RelatedFeedItem 추가; FeedDetailResponse에 bookImageUrl, isPublic 필드 추가; recentWritersmyFollowingUsers 필드명 변경.
Feed service & repository
**/data/service/FeedService.kt, **/data/repository/FeedRepository.kt
API 엔드포인트 추가: GET /feeds/mine/info, GET /feeds/related-books/{isbn}, PATCH /feeds/{feedId}; 레포지토리에 대응 메서드(getMyFeedInfo, getRelatedBookFeeds, updateFeed) 추가.
Feed write: UI/state/viewmodel
**/ui/feed/viewmodel/FeedWriteUiState.kt, **/ui/feed/viewmodel/FeedWriteViewModel.kt, **/ui/feed/screen/FeedWriteScreen.kt
편집 모드 지원 추가: 기존 이미지 목록·제거, isEditMode·editingFeedId, currentImageCount 계산, loadFeedForEdit / setEditData / removeExistingImage / createOrUpdateFeed / updateFeed 흐름 구현, UI에서 기존 이미지 삭제 콜백 추가.
Feed screens & components
**/ui/feed/screen/FeedScreen.kt, **/ui/feed/screen/FeedCommentScreen.kt, **/ui/feed/component/MyFeedCard.kt, **/ui/mypage/component/SavedFeedCard.kt, **/ui/feed/viewmodel/FeedViewModel.kt
FeedScreen 헤더를 myFeedInfo로 전환, 카드 컴포저블에 onBookClick 추가 및 책 상세 내비게이션 연결, 댓글 화면에 편집 내비게이션·신고/삭제 분기 추가, FeedViewModel에 myFeedInfo 로드 로직 추가.
Navigation / Routes
**/ui/navigator/navigations/FeedNavigation.kt, **/ui/navigator/routes/FeedRoutes.kt, **/ui/navigator/navigations/SearchNavigation.kt
FeedScreen 호출에서 불필요 파라미터 제거 및 onNavigateToBookDetail/onNavigateToFeedEdit 연결, FeedRoutes.WritefeedId·편집용 필드 추가, 검색에서 피드 클릭 시 댓글 화면으로 이동 연결.
Search UI & viewmodel
**/ui/search/component/SearchFilterButton.kt, **/ui/search/screen/SearchBookDetailScreen.kt, **/ui/search/viewmodel/BookDetailViewModel.kt, **/ui/search/viewmodel/SearchBookViewModel.kt
검색 도서 상세에 관련 피드 로드·정렬·페이징 로직 추가, 필터 버튼/드롭다운 컴포넌트 추가, BookDetailViewModel에 related feed 로딩/페이징/정렬/매핑 구현, 라이브 검색 지연 1000→300ms로 단축.
Top app bars
**/ui/common/topappbar/DefaultTopAppBar.kt, **/ui/common/topappbar/GradationTopAppBar.kt
Icon의 clickable modifier 대신 IconButton으로 래핑하여 클릭 처리 변경(임포트 정리 포함).
Resources
app/src/main/res/values/strings.xml
문자열 추가: edit_feed_titleapi_sort_like, api_sort_latest.

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant FeedScreen
  participant FeedViewModel
  participant FeedRepository
  participant FeedService

  User->>FeedScreen: "내 피드" 탭 선택
  FeedScreen->>FeedViewModel: loadMyFeeds()
  FeedViewModel->>FeedRepository: getMyFeedInfo()
  FeedRepository->>FeedService: GET /feeds/mine/info
  FeedService-->>FeedRepository: FeedMineInfoResponse
  FeedRepository-->>FeedViewModel: Result<FeedMineInfoResponse?>
  FeedViewModel-->>FeedScreen: uiState.myFeedInfo 업데이트
Loading
sequenceDiagram
  actor User
  participant SearchBookDetailScreen
  participant BookDetailViewModel
  participant FeedRepository
  participant FeedService

  User->>SearchBookDetailScreen: 정렬 변경 (좋아요/최신)
  SearchBookDetailScreen->>BookDetailViewModel: changeSortOrder(isbn, sort)
  BookDetailViewModel->>FeedRepository: getRelatedBookFeeds(isbn, sort)
  FeedRepository->>FeedService: GET /feeds/related-books/{isbn}?sort=...
  FeedService-->>FeedRepository: RelatedBooksResponse
  FeedRepository-->>BookDetailViewModel: Result<RelatedBooksResponse?>
  BookDetailViewModel-->>SearchBookDetailScreen: uiState.relatedFeeds 업데이트
Loading
sequenceDiagram
  actor User
  participant FeedCommentScreen
  participant Nav
  participant FeedWriteViewModel
  participant FeedRepository
  participant FeedService

  User->>FeedCommentScreen: 더보기 > 수정
  FeedCommentScreen->>Nav: navigate to Write(feedId)
  Nav->>FeedWriteViewModel: loadFeedForEdit(feedId)
  FeedWriteViewModel->>FeedRepository: getFeedDetail(feedId)
  Note over FeedWriteViewModel: 상태에 편집 데이터 채움
  User->>FeedWriteViewModel: 저장 클릭
  FeedWriteViewModel->>FeedRepository: updateFeed(feedId, UpdateFeedRequest)
  FeedRepository->>FeedService: PATCH /feeds/{feedId}
  FeedService-->>FeedRepository: CreateFeedResponse
  FeedRepository-->>FeedWriteViewModel: Result<CreateFeedResponse?>
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Assessment against linked issues

Objective Addressed Explanation
피드 수정 (#95)
내 피드 상단화면 (#95)
특정 책으로 작성된 피드 목록 조회 (#95)

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Icon clickable → IconButton 래핑 (app/src/main/java/com/texthip/thip/ui/common/topappbar/DefaultTopAppBar.kt) UI interaction refactor unrelated to API 연동 요구사항.
Icon clickable → IconButton 래핑 (app/src/main/java/com/texthip/thip/ui/common/topappbar/GradationTopAppBar.kt) UI interaction refactor unrelated to API 연동 요구사항.
라이브 검색 지연 1000ms→300ms (app/src/main/java/com/texthip/thip/ui/search/viewmodel/SearchBookViewModel.kt) 검색 UX 튜닝으로, 링크된 API 연동 이슈의 범위와 직접 관련없음.

Possibly related PRs

Suggested reviewers

  • Nico1eKim
  • JJUYAAA

Poem

토끼가 말하네, 깡총깡총 새 피드 도착! 🥕
책 표지 따라 발자국 쭉쭉, 수정도 척척,
이미지 한 움큼 정리하고 또 한숨엔 당근,
내 피드가 반짝이면 귀도 춤춘다,
클릭 한 번에 길 따라 떠나는 버그 없는 밤.

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.
    • 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.
  • 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 the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

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

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

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

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • 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

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR implements the Feed API functionality including feed edit capabilities, feed information display, and related book feeds. The implementation adds comprehensive feed management features to the Android application.

  • Added feed editing functionality with support for updating content, tags, visibility, and image management
  • Implemented related book feeds display with infinite scrolling and sorting options
  • Enhanced feed screens with proper API integration for user info and feed management

Reviewed Changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
strings.xml Added string resources for feed editing and API sort constants
SearchBookViewModel.kt Reduced search delay from 1000ms to 300ms for better responsiveness
BookDetailViewModel.kt Major refactor to support related feeds display with pagination and sorting
SearchBookDetailScreen.kt Complete UI overhaul to display related feeds with infinite scroll
SearchFilterButton.kt New component for filter dropdown functionality
FeedRoutes.kt Extended routing to support feed editing with additional parameters
SearchNavigation.kt Added navigation to feed comments from search results
FeedNavigation.kt Enhanced navigation with feed editing and book detail routing
SavedFeedCard.kt Added book click navigation support
FeedWriteViewModel.kt Added comprehensive feed editing functionality
FeedWriteUiState.kt Extended state to support edit mode and existing images
FeedViewModel.kt Added my feed info fetching capability
FeedWriteScreen.kt Enhanced UI to support feed editing mode
FeedScreen.kt Integrated with API for user info display
FeedCommentScreen.kt Added edit navigation and improved menu handling
MyFeedCard.kt Added book navigation support
GradationTopAppBar.kt Replaced clickable with IconButton for better accessibility
DefaultTopAppBar.kt Replaced clickable with IconButton for better accessibility
FeedService.kt Added new API endpoints for feed management
FeedRepository.kt Implemented feed update and related feeds functionality
Various response models Added new data models for API responses

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
You can also share your feedback on Copilot code review for a chance to win a $100 gift card. Take the survey.

var isBookmarked by remember { mutableStateOf(bookDetail?.isSaved ?: false) }
var selectedFilterOption by remember { mutableIntStateOf(0) }

var isFilterDropdownVisible by remember { mutableStateOf(false) }
Copy link

Copilot AI Aug 18, 2025

Choose a reason for hiding this comment

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

[nitpick] The filter dropdown state is managed at the composable level but could be better managed in the ViewModel to maintain state across configuration changes and improve testability.

Copilot uses AI. Check for mistakes.
private fun getCurrentMyFeeds(): List<com.texthip.thip.data.model.feed.response.MyFeedItem> {
// TODO: FeedViewModel에서 현재 내 피드 목록을 가져오는 방법 구현 필요
// 임시로 빈 리스트 반환
return emptyList()
Copy link

Copilot AI Aug 18, 2025

Choose a reason for hiding this comment

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

This function is declared but not implemented and always returns an empty list. Consider removing it if not needed or implementing the actual functionality.

Suggested change
return emptyList()
// 실제 내 피드 목록을 FeedRepository에서 가져오는 구현
suspend fun getCurrentMyFeeds(): List<com.texthip.thip.data.model.feed.response.MyFeedItem> {
return feedRepository.getMyFeeds().getOrNull() ?: emptyList()

Copilot uses AI. Check for mistakes.
onClick = {
isBottomSheetVisible = false
// TODO: 피드 신고 API 호출
showToast = true
Copy link

Copilot AI Aug 18, 2025

Choose a reason for hiding this comment

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

The report functionality shows a success toast but doesn't actually call any API. This could mislead users into thinking their report was submitted.

Suggested change
showToast = true
// Call the report API via ViewModel
// Show toast only on success, handle error
val context = LocalContext.current
val feedIdToReport = feedDetail.feedId
// Use LaunchedEffect to launch coroutine
LaunchedEffect(feedIdToReport) {
val result = feedDetailViewModel.reportFeed(feedIdToReport)
if (result) {
showToast = true
} else {
ToastWithDate(context, "신고에 실패했습니다. 다시 시도해주세요.")
}
}

Copilot uses AI. Check for mistakes.
onConfirm = {
showDialog = false
isBottomSheetVisible = false
// TODO: 피드 삭제 API 호출
Copy link

Copilot AI Aug 18, 2025

Choose a reason for hiding this comment

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

The delete functionality shows a confirmation dialog but doesn't actually call any API to delete the feed. Users will expect the feed to be deleted after confirmation.

Suggested change
// TODO: 피드 삭제 API 호출
viewModel.deleteFeed(feedId)
onNavigateBack()

Copilot uses AI. Check for mistakes.
Copy link

@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: 6

♻️ Duplicate comments (1)
app/src/main/java/com/texthip/thip/ui/feed/screen/FeedCommentScreen.kt (1)

481-514: 액션 바텀시트: 수정/삭제/신고 동작 연결 필요

  • 내 피드: 삭제는 확인 다이얼로그만 열고 실제 삭제 호출은 비어 있습니다.
  • 타인 피드: 신고는 TODO 상태입니다.

사용자 기대와 PR 목적(피드 API 연동)에 맞게 ViewModel을 통해 API를 실제 호출하세요.

예시(개념 스케치):

                 MenuBottomSheetItem(
                     text = stringResource(R.string.report),
                     color = colors.Red,
                     onClick = {
                         isBottomSheetVisible = false
-                        // TODO: 피드 신고 API 호출
-                        showToast = true
+                        // TODO: ViewModel API 호출 후 성공 시에만 토스트
+                        viewModel.reportFeed(feedDetail.feedId) { success ->
+                            showToast = success
+                            if (!success) {
+                                // 필요 시 에러 안내
+                            }
+                        }
                     }
                 )

삭제 확인 콜백도 실제 삭제 호출과 네비게이션을 연결하세요:

                     onConfirm = {
                         showDialog = false
                         isBottomSheetVisible = false
-                        // TODO: 피드 삭제 API 호출
+                        viewModel.deleteFeed(feedDetail.feedId) { success ->
+                            if (success) {
+                                onNavigateBack()
+                            } else {
+                                // 필요 시 에러 안내
+                            }
+                        }
                     },
🧹 Nitpick comments (28)
app/src/main/java/com/texthip/thip/ui/common/topappbar/DefaultTopAppBar.kt (1)

41-44: contentDescription 하드코딩을 stringResource로 전환 권장 (i18n/접근성)

현재 "Back Button", "More Options"가 하드코딩되어 있어 지역화가 불가합니다. string 리소스로 분리해 다국어/접근성을 개선하는 것을 권장합니다.

적용 예시:

-                contentDescription = "Back Button",
+                contentDescription = stringResource(R.string.cd_back_button),
-                    contentDescription = "More Options",
+                    contentDescription = stringResource(R.string.cd_more_options),

리소스 추가 예시(프로젝트의 res/values/strings.xml):

<resources>
    <string name="cd_back_button">뒤로가기</string>
    <string name="cd_more_options">더보기</string>
</resources>

필요하시면 strings.xml 추가와 참조 변경까지 포함한 패치 제안 드리겠습니다.

Also applies to: 62-65

app/src/main/java/com/texthip/thip/ui/common/topappbar/GradationTopAppBar.kt (1)

51-54: contentDescription도 string 리소스로 분리 권장

하드코딩된 영어 문자열은 지역화에 취약합니다. 아래처럼 stringResource를 사용해 주세요.

-                contentDescription = "Back Button",
+                contentDescription = stringResource(R.string.cd_back_button),
-                contentDescription = "More Options",
+                contentDescription = stringResource(R.string.cd_more_options),

strings.xml 예시(없다면 추가):

<resources>
    <string name="cd_back_button">뒤로가기</string>
    <string name="cd_more_options">더보기</string>
</resources>

리소스 키 네이밍/번역 추가도 필요하면 제안 드릴게요.

Also applies to: 70-73

app/src/main/res/values/strings.xml (1)

419-422: API 프로토콜 상수는 코드 상수/타입으로 관리를 검토해주세요

문자열 리소스에 translatable="false"로 둔 것은 적절하지만, API 파라미터 값(프로토콜 상수)은 리소스보다는 Kotlin 상수(sealed class/enum 또는 const val)로 관리하는 편이 리팩터링·오타 방지·IDE 리팩터링 지원 측면에서 더 안전합니다. 또한 화면용 sort_like/sort_latest와 이름이 비슷해 혼동의 여지도 있습니다.

  • 파일 & 위치: app/src/main/res/values/strings.xml (420–421)
  • 중복 정의 여부: 스크립트 실행 결과 중복 없음

선택적 리팩터 예시:

sealed interface FeedSort {
    fun toApiValue(): String

    object Like : FeedSort {
        override fun toApiValue() = "like"
    }
    object Latest : FeedSort {
        override fun toApiValue() = "latest"
    }
}
app/src/main/java/com/texthip/thip/data/model/feed/request/UpdateFeedRequest.kt (2)

6-12: 부분 업데이트 시 null 직렬화 동작 확인 필요 (explicitNulls/encodeDefaults)

kotlinx.serialization 설정에 따라 null 필드가 JSON에 포함될 수 있습니다. 부분 업데이트에서 “값을 보내지 않음(omit)”과 “null로 설정”의 의미가 다르면, Json 설정을 명확히 해야 합니다.

검증/개선 가이드:

  • 서버가 “미전송=변경 없음”, “null=필드 초기화”로 구분한다면 Json { explicitNulls = true }가 필요합니다.
  • 서버가 “null 미허용, 미전송만 허용”이라면 Json { explicitNulls = false; encodeDefaults = false }로 null/기본값을 생략하세요.

예시 설정(프로젝트 전역 Serializer 설정에 적용):

val json = Json {
    ignoreUnknownKeys = true
    encodeDefaults = false   // 기본값은 내보내지 않음
    explicitNulls = false    // null은 내보내지 않음(omit)
}

또한 remainImageUrls의 의미(“서버에 남길 이미지 목록” vs “삭제할 목록”)가 서버 계약과 일치하는지 한번 더 확인 부탁드립니다. 불일치 시 업데이트 결과가 반대로 적용될 수 있습니다.


6-12: 간단한 KDoc으로 필드 의미를 명시하면 유지보수성이 좋아집니다

특히 remainImageUrls의 의미가 혼동되기 쉬워 KDoc 보강을 권합니다.

아래와 같이 주석을 추가하는 것을 제안드립니다:

 @Serializable
+/**
+ * 피드 부분 업데이트 요청 모델.
+ * - null인 필드는 서버에 전송하지 않아 기존 값을 유지합니다(Serializer 설정에 따름).
+ * - remainImageUrls: 편집 후 '유지할' 기존 이미지의 URL 목록(서버 계약에 따라 반대 의미일 수 있어 확인 필요).
+ */
 data class UpdateFeedRequest(
     @SerialName("contentBody") val contentBody: String? = null,
     @SerialName("isPublic") val isPublic: Boolean? = null,
     @SerialName("tagList") val tagList: List<String>? = null,
     @SerialName("remainImageUrls") val remainImageUrls: List<String>? = null
 )
app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteUiState.kt (1)

33-36: 편집 모드에서 ‘기존+신규’ 이미지 합산 로직 권장 (최대치/추가 가능 여부 계산)

현재는 편집 모드에서 기존 이미지 개수만 세고(canAddMoreImages도 불가) 있어, 기존 이미지를 일부 삭제한 뒤 신규 이미지를 추가하는 일반적인 UX가 막힐 수 있습니다. 합산 방식으로 바꾸면 작성/편집 공통 규칙(총 3장 제한)을 일관되게 유지할 수 있습니다.

아래처럼 합산 기반으로 수정 제안드립니다.

-    // 현재 모드에 따른 이미지 개수
-    val currentImageCount: Int
-        get() = if (isEditMode) existingImageUrls.size else imageUris.size
+    // 전체 이미지 개수(기존 + 신규)
+    val currentImageCount: Int
+        get() = existingImageUrls.size + imageUris.size

     val isImageCountValid: Boolean
-        get() = currentImageCount <= 3
+        get() = currentImageCount <= 3

     // 이미지 추가 가능 여부
     val canAddMoreImages: Boolean
-        get() = !isEditMode && imageUris.size < 3
+        get() = currentImageCount < 3

만약 이번 스프린트 범위에서 “편집 시 신규 추가 금지”가 의도라면 현 구현 유지가 맞습니다. 그 경우 UI에서 사전에 명확히 안내하는 문구(예: “편집에서는 기존 이미지만 삭제할 수 있어요”)를 고려해 주세요.

Also applies to: 37-39, 50-53

app/src/main/java/com/texthip/thip/data/model/feed/response/RelatedBooksResponse.kt (2)

8-8: 네트워크 응답 결손 대비: List 필드에 기본값을 지정해 역직렬화 안전성 확보를 권장합니다

API에서 feeds 혹은 contentUrls 키가 누락되면 kotlinx.serialization은 예외를 던집니다. 컬렉션 필드는 기본값을 부여해두면 결손 응답에도 안전합니다.

다음처럼 기본값을 부여하는 것을 제안합니다:

-    @SerialName("feeds") val feeds: List<RelatedFeedItem>,
+    @SerialName("feeds") val feeds: List<RelatedFeedItem> = emptyList(),
...
-    @SerialName("contentUrls") val contentUrls: List<String>,
+    @SerialName("contentUrls") val contentUrls: List<String> = emptyList(),

Also applies to: 27-27


15-16: ID 스케일 확인 필요 (Int → Long 여부 검토)

백엔드에서 ID가 Long 범위를 사용할 가능성이 있으면 현재 Int 선언은 오버플로 리스크가 있습니다. 실제 스키마(예: Swagger/Schema) 기준으로 타입을 확인 부탁드립니다. 필요 시 아래처럼 변경할 수 있습니다.

-    @SerialName("feedId") val feedId: Int,
-    @SerialName("creatorId") val creatorId: Int,
+    @SerialName("feedId") val feedId: Long,
+    @SerialName("creatorId") val creatorId: Long,
app/src/main/java/com/texthip/thip/ui/search/component/SearchFilterButton.kt (2)

65-71: 아이콘 접근성: 장식용 아이콘은 contentDescription을 null로 설정하세요

해당 아이콘은 텍스트와 버튼이 이미 의미를 전달하고 있어 보조기기에 중복 노출될 수 있습니다. 장식용으로 처리하는 것이 접근성 측면에서 더 적절합니다.

-                contentDescription = "Dropdown",
+                contentDescription = null,

선택적으로, 버튼 자체에 의미 있는 설명을 부여하려면 Rowsemantics를 추가하거나 SearchFilterButtoncontentDescription 파라미터를 노출하는 방법도 있습니다.


51-52: 터치 타깃 권고 크기(48dp) 충족 검토

머티리얼 가이드라인의 최소 터치 영역 48dp를 고려하면 36dp는 다소 작습니다. 여유가 있다면 48dp로 상향을 권장합니다.

-                .height(36.dp)
+                .height(48.dp)
app/src/main/java/com/texthip/thip/ui/search/viewmodel/SearchBookViewModel.kt (1)

55-57: 라이브 검색 디바운스 300ms로 단축 — 서버 레이트리밋/트래픽 영향 확인 권장

UX 측면에서는 반응성이 좋아지지만, 300ms는 트래픽이 증가할 수 있습니다. 서버 레이트리밋/쿼터, 약한 네트워크 환경에서의 요청 중복/취소 처리 영향을 한 번 점검해 주세요.

리뷰 제안:

  • 디바운스 값을 상수로 추출해 A/B 조정이 쉽게 만드세요.
  • 최소 입력 길이(n≥2 등) 조건을 두면 불필요한 요청을 줄일 수 있습니다.

예시(파일 내 추가/수정 필요):

// 클래스 내부 어딘가에 상수 정의
companion object {
    private const val LIVE_SEARCH_DEBOUNCE_MS = 300L
}
-                delay(300)
+                delay(LIVE_SEARCH_DEBOUNCE_MS)
app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt (3)

76-79: 탭 전환 시 내 피드 정보 동시 로드 좋습니다. refresh 경로에서도 동기화 고려 바랍니다

탭 인덱스 1 선택 시 fetchMyFeedInfo() 호출은 적절합니다. 추가로 당 탭에서 당겨서 새로고침(refreshMyFeeds) 시에도 최신화가 필요하다면 동일 호출을 포함하는 것을 권장합니다.

예시(참고용, 기존 함수 외부 수정 제안):

// refreshMyFeeds() 마지막 onSuccess 블록 직후 등 적절한 위치에서 호출
fetchMyFeedInfo()

281-287: 에러 상태 처리는 문제 없으나, 성공 시 이전 에러 클리어 고려

onFailure에서 error를 채우고 있으므로, 직전 에러가 남지 않도록 성공 시 error = null로 명시 클리어를 권장합니다.

다음과 같이 수정하면 일관성이 좋아집니다:

-                    updateState {
-                        it.copy(
-                            isLoading = false,
-                            error = exception.message
-                        )
-                    }
+                    updateState {
+                        it.copy(
+                            isLoading = false,
+                            error = exception.message
+                        )
+                    }

(주: 본 제안은 fetchRecentWriters 실패 분기에서의 패턴 유지에 대한 코멘트입니다. 성공 분기에서 error를 null로 명시하는 패턴도 일관성 측면에서 고려해 주세요.)


291-305: fetchMyFeedInfo 성공 시 이전 에러를 지워주면 UX가 더 안정적입니다

현재 성공 시 error를 건드리지 않아 과거 오류 메시지가 남을 수 있습니다. 성공 시 error = null 세팅을 권장합니다.

                 .onSuccess { data ->
                     updateState {
-                        it.copy(myFeedInfo = data)
+                        it.copy(
+                            myFeedInfo = data,
+                            error = null
+                        )
                     }
                 }
app/src/main/java/com/texthip/thip/ui/navigator/routes/FeedRoutes.kt (1)

21-24: nullable 리스트 기본값을 emptyList로 설정해 NPE·분기 감소

editTagList, editContentUrls는 기본값을 emptyList()로 두면 사용처의 null 분기를 줄일 수 있습니다(타입은 그대로 nullable 유지 가능).

-        val editTagList: List<String>? = null,
-        val editContentUrls: List<String>? = null
+        val editTagList: List<String>? = emptyList(),
+        val editContentUrls: List<String>? = emptyList()
app/src/main/java/com/texthip/thip/data/service/FeedService.kt (1)

56-63: 정렬 파라미터는 enum으로 타입 세이프티를 확보하는 것을 권장

sort를 String? 대신 제한된 값의 enum으로 노출하면 오입력 방지와 유지보수성이 좋아집니다.

-    suspend fun getRelatedBookFeeds(
-        @Path("isbn") isbn: String,
-        @Query("sort") sort: String? = null,
-        @Query("cursor") cursor: String? = null
-    ): BaseResponse<RelatedBooksResponse>
+    suspend fun getRelatedBookFeeds(
+        @Path("isbn") isbn: String,
+        @Query("sort") sort: RelatedBookSort? = null,
+        @Query("cursor") cursor: String? = null
+    ): BaseResponse<RelatedBooksResponse>

참고: enum 정의 예시(파일 외부 추가)

enum class RelatedBookSort {
    RECENT, POPULAR;
    override fun toString(): String = name.lowercase() // 서버 기대값에 맞춰 변환
}
app/src/main/java/com/texthip/thip/ui/feed/screen/FeedWriteScreen.kt (2)

188-203: 수정 모드에서 이미지 추가 버튼 클릭 비활성화 조건 보강

주석은 “수정 모드에서는 비활성화”라고 되어 있지만, 실제 클릭 가능 여부는 uiState.canAddMoreImages만으로 제어되고 있습니다. ViewModel에서 addImages가 수정 모드에선 early return 하므로, 버튼은 눌리지만 아무 동작이 없어 UX 혼란이 생길 수 있습니다. isEditMode까지 함께 체크하도록 보강하세요.

적용 제안:

-                                .let {
-                                    if (uiState.canAddMoreImages) it.clickable {
-                                        imagePickerLauncher.launch("image/*")
-                                    } else it // 클릭 비활성화
-                                },
+                                .let {
+                                    if (!uiState.isEditMode && uiState.canAddMoreImages) {
+                                        it.clickable { imagePickerLauncher.launch("image/*") }
+                                    } else it // 수정 모드 또는 제한 수량 도달 시 비활성화
+                                },

213-235: 기존 이미지 LazyRow에 안정 키(Key) 부여

현재 size 기반 인덱스로 items를 구성하고 있어 삭제 시 컴포저블 재활용으로 인한 깜빡임/매칭 오류 가능성이 있습니다. URL을 key로 부여하는 itemsIndexed 형태로 교체하세요.

-                    items(uiState.existingImageUrls.size) { index ->
-                        Box(modifier = Modifier.size(80.dp)) {
-                            AsyncImage(
-                                model = uiState.existingImageUrls[index],
+                    itemsIndexed(
+                        items = uiState.existingImageUrls,
+                        key = { _, url -> url }
+                    ) { index, url ->
+                        Box(modifier = Modifier.size(80.dp)) {
+                            AsyncImage(
+                                model = url,
                                 contentDescription = null,
                                 modifier = Modifier.fillMaxSize(),
                                 contentScale = ContentScale.Crop
                             )
                             IconButton(
                                 onClick = { onRemoveExistingImage(index) },
                                 modifier = Modifier
                                     .align(Alignment.TopEnd)
                                     .size(24.dp)
                             ) {
                                 Icon(
                                     painter = painterResource(id = R.drawable.ic_x),
                                     contentDescription = null,
                                     tint = colors.White
                                 )
                             }
                         }
                     }
app/src/main/java/com/texthip/thip/ui/feed/screen/FeedCommentScreen.kt (2)

40-40: 사용되지 않는 LocalContext 제거

LocalContext를 import하고 context를 선언했으나 사용되지 않습니다. 정리해 빌드 경고를 줄이세요.

-import androidx.compose.ui.platform.LocalContext
...
-    val context = LocalContext.current

Also applies to: 84-84


468-477: 신고 토스트 표시 로직(LGTM) + 후속 보완 제안

토스트 오버레이와 3초 자동 숨김 처리는 직관적입니다. 다만 실제 신고 API 호출과 성공/실패 분기 처리가 아직 TODO이니, 성공 시에만 토스트를 띄우고 실패 시 에러 안내를 권장합니다.

원하시면 FeedDetailViewModel에 reportFeed(feedId: Int): Boolean 추가 및 호출부까지 구현 PR 보조하겠습니다.

app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt (1)

178-197: 부분 업데이트 요청 모델로의 수정 반영(LGTM) + 로깅 개선 제안

UpdateFeedRequest로 부분 업데이트를 지원한 점 좋습니다. 한편, uriToMultipartBodyPart의 예외 처리에서 printStackTrace 대신 일관된 로깅 유틸을 쓰는 것을 권장합니다(운영 환경에서의 가시성 향상).

추가로 createFeed에서 withContext(Dispatchers.IO)로 파일 변환을 감싼 점과 finally에서 임시 파일 정리까지 포함한 리소스 관리가 깔끔합니다.

app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt (2)

64-77: 카테고리 인덱스 기본값 -1 유지 권장

카테고리 로딩 전에도 기본값 0을 주면, 빈 카테고리에서 선택된 것으로 간주되어 이후 파생 계산(availableTags 등)에서 인덱스 오류/의도치 않은 UI 표시가 발생할 수 있습니다. 찾지 못한 경우 -1을 유지하세요.

-        }.let { if (it == -1) 0 else it }
+        }.let { if (it == -1) -1 else it }

119-155: 수동 편집 데이터 주입(setEditData)에서도 동일한 카테고리 인덱스 보정 필요

loadFeedForEdit와 동일하게 -1 기본값을 유지하도록 보정하는 것이 안전합니다.

-        }.let { if (it == -1) 0 else it }
+        }.let { if (it == -1) -1 else it }
app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookDetailScreen.kt (1)

525-535: 정렬 드롭다운 선택 시 자동 닫힘 처리

옵션 선택 후 isFilterDropdownVisible을 false로 바꿔 드롭다운을 닫는 것이 자연스러운 UX입니다.

-                    onOptionSelected = { option ->
-                        onSortChange(option)
-                    },
+                    onOptionSelected = { option ->
+                        onSortChange(option)
+                        isFilterDropdownVisible = false
+                    },
app/src/main/java/com/texthip/thip/ui/search/viewmodel/BookDetailViewModel.kt (3)

20-35: UI 상태 모델 확장 방향 좋음. 다만 feedItems 계산은 매 접근마다 O(n) 재계산됨

  • feedItems를 게터에서 매번 map하는 구조는 recomposition 시 반복 비용이 발생할 수 있습니다.
  • 또한 UI에서 사용하는 FeedItem 타입이 ui.mypage.mock.FeedItem에 의존하는 것은 모듈 간 결합을 키웁니다.

두 가지 개선 제안:

  • 선택 1: UI에 필요한 모델만 노출하도록 BookDetailUiState에서 relatedFeeds 대신 feedItems만 보유하고, 로드/페이지네이션 성공 시에만 map하여 저장.
  • 선택 2: FeedItem을 공용(ui-common) 패키지로 이동해 mock 의존 제거.

79-104: 관련 피드 로딩 시작/성공 시 feedError 초기화 필요

에러 이후 재시도에서 이전 에러 메시지가 남아 있을 수 있습니다. 시작 시와 성공 시에 feedError를 null로 정리하는 편이 안전합니다.

아래처럼 최소한 시작/성공 시 초기화를 권장합니다.

-            updateState { it.copy(isLoadingFeeds = true) }
+            updateState { it.copy(isLoadingFeeds = true, feedError = null) }
@@
-                        it.copy(
+                        it.copy(
                             relatedFeeds = response?.feeds ?: emptyList(),
                             nextCursor = response?.nextCursor,
                             isLast = response?.isLast ?: true,
                             isLoadingFeeds = false,
-                            currentSort = sort
+                            currentSort = sort,
+                            feedError = null
                         )

79-104: 정렬 변경 시 응답 역전(race) 가능성 안내

연속된 정렬 변경이 빠르게 발생하면 이전 요청 응답이 나중에 도착하여 최신 정렬 결과를 덮어쓸 수 있습니다. 간단히 마지막 요청의 sort/cursor 토큰을 상태로 보관해 응답 적용 시 검증하거나, Job을 보관하여 이전 요청을 취소하는 방식(마지막 요청만 유효) 도입을 권장합니다.

원하시면 관련 코드를 추가한 버전을 제안드리겠습니다. 또한 저장/정렬 변경 시 동시 요청 취소가 필요한지 제품 요구사항을 확인해 주세요.

Also applies to: 140-144

app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt (1)

87-99: 무한 스크롤 트리거의 키에 아이템 개수 포함 권장

현재 remember 키가 canLoadMoreCurrentTab, isLoadingMore만 포함합니다. 리스트 아이템 수 변화만 있을 때 트리거 갱신이 지연될 수 있어 size를 키에 포함하는 것이 안전합니다.

-    val shouldLoadMore by remember(feedUiState.canLoadMoreCurrentTab, feedUiState.isLoadingMore) {
+    val shouldLoadMore by remember(
+        feedUiState.canLoadMoreCurrentTab,
+        feedUiState.isLoadingMore,
+        feedUiState.currentTabFeeds.size
+    ) {
         derivedStateOf {
             val layoutInfo = listState.layoutInfo
             val totalItems = layoutInfo.totalItemsCount
             val lastVisibleIndex = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0

             feedUiState.canLoadMoreCurrentTab &&
                     !feedUiState.isLoadingMore &&
                     feedUiState.currentTabFeeds.isNotEmpty() &&
                     totalItems > 0 &&
                     lastVisibleIndex >= totalItems - 3
         }
     }
📜 Review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e3438dc and 3718996.

📒 Files selected for processing (25)
  • app/src/main/java/com/texthip/thip/data/model/feed/request/UpdateFeedRequest.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/model/feed/response/FeedDetailResponse.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/model/feed/response/FeedMineInfoResponse.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/model/feed/response/RelatedBooksResponse.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/model/users/response/UsersMyFollowingsRecentFeedsResponse.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt (2 hunks)
  • app/src/main/java/com/texthip/thip/data/service/FeedService.kt (3 hunks)
  • app/src/main/java/com/texthip/thip/ui/common/topappbar/DefaultTopAppBar.kt (3 hunks)
  • app/src/main/java/com/texthip/thip/ui/common/topappbar/GradationTopAppBar.kt (3 hunks)
  • app/src/main/java/com/texthip/thip/ui/feed/component/MyFeedCard.kt (2 hunks)
  • app/src/main/java/com/texthip/thip/ui/feed/screen/FeedCommentScreen.kt (12 hunks)
  • app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt (5 hunks)
  • app/src/main/java/com/texthip/thip/ui/feed/screen/FeedWriteScreen.kt (8 hunks)
  • app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt (5 hunks)
  • app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteUiState.kt (3 hunks)
  • app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt (5 hunks)
  • app/src/main/java/com/texthip/thip/ui/mypage/component/SavedFeedCard.kt (2 hunks)
  • app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt (4 hunks)
  • app/src/main/java/com/texthip/thip/ui/navigator/navigations/SearchNavigation.kt (2 hunks)
  • app/src/main/java/com/texthip/thip/ui/navigator/routes/FeedRoutes.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/ui/search/component/SearchFilterButton.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookDetailScreen.kt (12 hunks)
  • app/src/main/java/com/texthip/thip/ui/search/viewmodel/BookDetailViewModel.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/ui/search/viewmodel/SearchBookViewModel.kt (1 hunks)
  • app/src/main/res/values/strings.xml (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (5)
app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt (2)
app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt (1)
  • updateState (34-36)
app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedDetailViewModel.kt (1)
  • updateState (28-30)
app/src/main/java/com/texthip/thip/ui/feed/screen/FeedCommentScreen.kt (2)
app/src/main/java/com/texthip/thip/ui/common/modal/ToastWithDate.kt (1)
  • ToastWithDate (24-60)
app/src/main/java/com/texthip/thip/ui/common/bottomsheet/MenuBottomSheet.kt (1)
  • MenuBottomSheet (22-59)
app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt (3)
app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt (1)
  • updateState (59-61)
app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedDetailViewModel.kt (1)
  • updateState (28-30)
app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt (1)
  • updateFeed (179-196)
app/src/main/java/com/texthip/thip/ui/search/viewmodel/BookDetailViewModel.kt (2)
app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt (1)
  • updateState (41-43)
app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt (1)
  • updateState (28-30)
app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt (4)
app/src/main/java/com/texthip/thip/ui/common/header/AuthorHeader.kt (1)
  • AuthorHeader (34-121)
app/src/main/java/com/texthip/thip/utils/color/HexToColor.kt (1)
  • hexToColor (6-13)
app/src/main/java/com/texthip/thip/ui/feed/component/FeedSubscribelistBar.kt (1)
  • FeedSubscribeBarlist (35-101)
app/src/main/java/com/texthip/thip/ui/theme/Theme.kt (1)
  • ThipTheme (41-69)
🔇 Additional comments (43)
app/src/main/java/com/texthip/thip/ui/common/topappbar/DefaultTopAppBar.kt (2)

8-8: IconButton 도입 import 추가 적절

IconButton 사용을 위한 import 추가가 적절합니다. 기존 clickable 대비 접근성(역할/포커스/리플)과 터치 타깃 일관성이 좋아집니다.


36-45: Icon을 IconButton으로 감싼 전환은 접근성·터치 타깃 면에서 바람직

  • 클릭 처리를 Icon에서 분리해 IconButton에 위임한 점이 좋습니다.
  • 56dp 바 높이에서 IconButton 기본 최소 터치 영역(48dp)을 확보할 수 있고, TalkBack/스크린리더 역할 정보도 자동 부여됩니다.

Also applies to: 57-66

app/src/main/java/com/texthip/thip/ui/common/topappbar/GradationTopAppBar.kt (2)

10-10: IconButton 도입 import 추가 적절

IconButton 사용으로 상단바 버튼의 접근성과 상호작용 일관성이 향상됩니다.


46-55: IconButton 래핑 전환 좋습니다

  • 좌/우 아이콘 모두 IconButton으로 감싸 정렬(.align)만 상위에 두고 클릭을 버튼에 위임한 점이 깔끔합니다.
  • 기존 동작을 유지하면서 머티리얼 상호작용(리플/롤)과 포커스 처리가 표준화됩니다.

Also applies to: 65-74

app/src/main/res/values/strings.xml (1)

342-342: edit_feed_title 추가 적절 — UI 맥락에 잘 맞습니다

편집 모드 타이틀 리소스 추가는 명확하고 기존 edit_feed와 역할이 분리되어 있어 읽기 좋습니다. 사용처에서도 혼선이 줄어들 것으로 보입니다.

app/src/main/java/com/texthip/thip/data/model/feed/response/FeedDetailResponse.kt (1)

18-19: bookImageUrl / isPublic 필드 추가 적절 — 역호환도 안전합니다

옵셔널로 추가되어 구버전 응답에서도 파싱 실패가 없고, 신규 서버 필드를 자연스럽게 수용합니다. 도메인·UI 확장에도 필요한 정보라 👍

Also applies to: 26-26

app/src/main/java/com/texthip/thip/data/model/feed/response/FeedMineInfoResponse.kt (1)

6-17: 신규 Mine Info 모델 정의 깔끔합니다

필수/옵셔널 구분이 적절하고, 컬렉션 타입도 널이 아닌 기본값으로 받아 안전합니다. 서비스/레포지토리 연동에도 문제 없어 보입니다.

app/src/main/java/com/texthip/thip/data/model/feed/response/RelatedBooksResponse.kt (1)

6-11: 모델 구조 전반 LGTM

필드와 @SerialName 매핑이 명확하고, UI/레포지토리에서 쓰기 적절한 형태로 잘 정의되어 있습니다.

Also applies to: 13-32

app/src/main/java/com/texthip/thip/ui/search/component/SearchFilterButton.kt (1)

88-99: 오버레이/드롭다운 레이어링 방식 LGTM

바깥 터치로 닫기 처리와 AnimatedVisibility 전환이 명확하게 분리되어 있고, 드롭다운이 오버레이 위에 렌더링되도록 순서가 잘 잡혀 있습니다.

Also applies to: 100-141

app/src/main/java/com/texthip/thip/ui/navigator/navigations/SearchNavigation.kt (2)

49-51: 피드 댓글 화면으로의 내비게이션 연결 LGTM

onFeedClick을 통해 navigateToFeedComment(feedId)로 자연스럽게 연결되었습니다. 뒤로가기 흐름도 기존 패턴과 일관됩니다.


9-9: feedId 파라미터(Int) 타입 일치 확인

  • 확장 함수: fun NavHostController.navigateToFeedComment(feedId: Int)
  • 경로 정의: @Serializable data class Comment(val feedId: Int)
  • 사용처(SearchNavigation.kt, FeedNavigation.kt) 모두 feedId: Int를 전달하므로 타입이 일치합니다.
app/src/main/java/com/texthip/thip/data/model/users/response/UsersMyFollowingsRecentFeedsResponse.kt (1)

7-8: 역직렬화 키 호환성 확인 필요
DTO 필드명이 recentWritersmyFollowingUsers로 변경되었으므로, 실제 백엔드 JSON 키가 일치하는지 꼭 확인하세요. 불일치 시(recentWriters 키가 그대로인 경우) @SerialName("recentWriters")를 사용해 매핑을 유지해야 역직렬화 오류를 방지할 수 있습니다.

영향받는 파일:

  • app/src/main/java/com/texthip/thip/data/model/users/response/UsersMyFollowingsRecentFeedsResponse.kt

예시(diff):

 import kotlinx.serialization.Serializable
+import kotlinx.serialization.SerialName

 @Serializable
 data class UsersMyFollowingsRecentFeedsResponse(
-    val myFollowingUsers: List<RecentWriterList>
+    @SerialName("recentWriters")
+    val myFollowingUsers: List<RecentWriterList>
 )

만약 서버도 키를 myFollowingUsers로 변경했다면, 현재 코드로 동작하지만 장기적인 안정성을 위해 명시적인 @SerialName("myFollowingUsers") 추가를 권장합니다.

app/src/main/java/com/texthip/thip/ui/mypage/component/SavedFeedCard.kt (2)

66-67: ActionBookButton에 onBookClick 정상 연결 확인

의도대로 onBookClick이 연결되었습니다. 기본값이 빈 람다라서 기존 호출부도 깨지지 않습니다.


39-41: SavedFeedCard 호출부에서 trailing-lambda 사용 미발견

스크립트 검사를 통해 SavedFeedCard( 호출부에서 마지막 인자를 람다로 넘기는 패턴을 찾지 못했습니다.
그러나 기존 소비 코드에 trailing-lambda 사용 가능성을 완전히 배제할 수 없으므로, 호환성 보장을 위해 아래 중 하나를 권장드립니다:

  • onCommentClick을 마지막 파라미터로 위치를 변경
  • 구버전 시그니처(기존 순서) 오버로드 추가

필요하다면 호출부를 수동으로 한번 더 검토해 주십시오.

app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt (3)

6-6: FeedMineInfoResponse import 추가 적절

새 UI 상태 필드 의존성을 정상적으로 도입했습니다.


22-22: UI 상태에 myFeedInfo 추가 OK

헤더/소개 영역 등의 표시를 위한 상태 확장으로 적절합니다. 기본값 null로 두어 초기 로딩 전 접근 안정성도 확보되었습니다.


272-279: recent writers 소스 필드 변경 확인 필요

data?.recentWriters → data?.myFollowingUsers로 변경되었습니다. 레포/모델이 동일하게 갱신되었는지, 그리고 UI에서 기대 타입(RecentWriterList)의 스키마가 그대로 유지되는지 확인해 주세요.

app/src/main/java/com/texthip/thip/ui/feed/component/MyFeedCard.kt (2)

48-49: ActionBookButton.onClick 연결 확인

의도대로 onBookClick이 정상 연결되었습니다.


34-36: trailing-lambda 호출 미발견: 파라미터 순서 변경은 선택사항입니다

현재 코드베이스 전반에서 MyFeedCard를 trailing-lambda 형태로 호출한 곳이 없어, onContentClickonBookClick의 순서를 바꾸지 않아도 문제가 발생하지 않습니다.
필요시 아래와 같이 순서를 바꾸해도 되지만, 필수 변경은 아닙니다.

• 파라미터 선언 (선택적 리팩터):

-    onContentClick: () -> Unit = {},
-    onBookClick: () -> Unit = {}
+    onBookClick: () -> Unit = {},
+    onContentClick: () -> Unit = {}
app/src/main/java/com/texthip/thip/ui/navigator/routes/FeedRoutes.kt (1)

14-24: Write 라우트 페이로드 확장 적절

수정 모드(feedId 및 편집 관련 필드)까지 포괄하도록 확장된 시그니처는 요구사항과 일치합니다. 모든 필드를 기본값으로 두어 직렬화/역직렬화 호환성 측면에서도 안전합니다.

app/src/main/java/com/texthip/thip/data/service/FeedService.kt (3)

9-20: 신규 모델·어노테이션 import 추가 OK

새 엔드포인트에 필요한 타입/어노테이션이 정확히 추가되었습니다.


52-55: 내 피드 정보 조회 엔드포인트 추가 적절

UI에서 ‘내 피드’ 헤더 정보를 구성하기 위한 데이터 취득 경로로 자연스럽습니다.


80-86: updateFeed 응답 타입이 CreateFeedResponse인 점 확인 필요

PATCH 응답이 생성 응답 스키마(CreateFeedResponse)와 동일한지 백엔드 계약을 확인해 주세요. 별도 UpdateFeedResponse가 존재한다면 해당 타입으로 교체하는 것이 명확합니다.

app/src/main/java/com/texthip/thip/ui/feed/screen/FeedWriteScreen.kt (3)

73-81: 생성/수정 흐름 통합(LGTM)

createOrUpdateFeed로 라우팅한 결정 적절합니다. 성공/실패 콜백도 UI로 올바르게 전달되고 있습니다.


133-140: 편집/신규 타이틀 전환(LGTM)

isEditMode에 따른 타이틀 전환 적용 잘 되었습니다.


261-271: 사진 카운트 계산 변경(LGTM)

currentImageCount 사용으로 기존+신규 이미지를 정확히 반영하는 점 좋습니다.

app/src/main/java/com/texthip/thip/ui/feed/screen/FeedCommentScreen.kt (1)

146-167: 배경 블러 처리(LGTM)

바텀시트/다이얼로그 표시 시 배경 블러 처리로 포커스가 명확합니다.

app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt (2)

153-159: 내 피드 정보 API 추가(LGTM)

핸들러 체인(handleBaseResponse → getOrThrow)과 Result 래핑 일관성 좋습니다.


160-170: 관련 도서 피드 조회 API 추가(LGTM)

sort/cursor 파라미터 옵션화 및 Result 래핑 적절합니다.

app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt (5)

85-98: 편집 모드 상태 세팅(LGTM)

기존 이미지 URL, 공개 여부 반전(isPrivate), 편집 모드 플래그/ID 설정까지 일관성 있게 세팅되어 있습니다.


291-296: 수정 모드에서 이미지 추가 차단(LGTM)

isEditMode에서 addImages 조기 반환으로 서버 API 제약과 UI 정책을 맞춘 점 좋습니다.


313-319: 기존 이미지 제거 동작(LGTM)

인덱스 검증 후 상태 업데이트 방식 깔끔합니다. remainImageUrls는 updateFeed 호출 시 반영되어 서버와 동기화됩니다.


355-363: 생성/수정 라우팅(LGTM)

편집 여부/ID에 따라 update vs create 분기 처리 적절합니다.


418-459: 피드 수정 API 연동(LGTM)

  • 폼 검증 → 로딩 플래그 → repository.updateFeed 호출 → 성공 시 feedId 반환 → 로딩 해제의 플로우가 견고합니다.
  • remainImageUrls로 서버에 유지 이미지를 명시하는 설계도 좋습니다.
app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookDetailScreen.kt (2)

217-236: 헤더 이미지 플레이스홀더/에러 처리(LGTM)

fallback/error 리소스 지정으로 빈/에러 이미지 상황 대비가 되어 있습니다.


445-470: 피드 아이템 키 지정(LGTM)

itemsIndexed에 key 지정으로 리스트 업데이트 안정성이 확보되어 있습니다.

app/src/main/java/com/texthip/thip/ui/search/viewmodel/BookDetailViewModel.kt (1)

40-49: 상태 갱신 헬퍼 도입 굿

updateState 캡슐화로 가독성과 일관성이 좋아졌습니다. 다른 ViewModel들과 컨벤션도 맞습니다.

app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt (1)

41-43: 도서 상세 화면 네비게이션 연동 LGTM

FeedScreen에 onNavigateToBookDetail을 전달하는 연결이 자연스럽고, 확장 메서드 사용도 일관적입니다.

app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt (5)

67-71: FeedScreen 파라미터 변경 LGTM

도서 상세로의 네비게이션 콜백 추가가 자연스럽고 호출부와도 일관됩니다.


215-235: 헤더 바인딩 안전 처리 LGTM

  • myFeedInfo의 닉네임/별칭/색상에 대한 안전 기본값 처리와 hexToColor fallback이 적절합니다.
  • FeedMineInfoResponse 연동 방향도 자연스럽습니다.

352-354: 전체 피드 카드 onBookClick 연동 LGTM

도서 상세 이동 콜백 연결이 명확합니다.


395-398: 프리뷰 시그니처 갱신 LGTM

새 파라미터 반영이 정확합니다.

Also applies to: 406-409


286-295: ISBN은 Non-null String으로 선언되어 있습니다
MyFeedResponse.ktMyFeedItem 데이터 클래스에서 @SerialName("isbn") val isbn: String으로 정의되어 있어, myFeed.isbnnull일 가능성이 없습니다.
따라서 onNavigateToBookDetail(myFeed.isbn) 호출은 안전하며 별도의 null 검사나 빌드/런타임 이슈 대응이 필요하지 않습니다.

Likely an incorrect or invalid review comment.

Comment on lines +111 to +114
.padding(
top = with(density) { (filterButtonPosition.y - 36).toDp() },
end = 20.dp // 기존 FilterButton과 동일하게 화면 오른쪽에서 20dp
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

px/dp 단위 혼용으로 인한 드롭다운 위치 오차 (밀도 의존 버그)

filterButtonPosition.y는 px 단위인데 36을 그대로 빼고 toDp()로 변환하고 있습니다. 이 경우 mdpi 외 단말에서 드롭다운이 어긋납니다. px→dp 변환을 먼저 한 다음 dp끼리 연산해야 합니다.

다음과 같이 수정해 주세요:

-                        top = with(density) { (filterButtonPosition.y - 36).toDp() },
+                        top = with(density) { filterButtonPosition.y.toDp() - 36.dp },

추가로, 화면 상단 근처에서 음수 padding이 되지 않도록 보정하는 것도 권장합니다:

-                        top = with(density) { filterButtonPosition.y.toDp() - 36.dp },
+                        top = (with(density) { filterButtonPosition.y.toDp() - 36.dp }).coerceAtLeast(0.dp),
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/ui/search/component/SearchFilterButton.kt
around lines 111 to 114, the code subtracts a raw px constant from
filterButtonPosition.y (which is in px) then calls toDp(), causing
density-dependent misplacement; convert the pixel value to dp first and perform
dp arithmetic (e.g., filterButtonPosition.y.toDp() - 36.dp) and clamp the result
to a non-negative value so padding cannot become negative (use max(0.dp, ...));
keep the end = 20.dp unchanged.

Comment on lines +102 to +105
onSortChange = { sortType ->
val apiSortType = if (sortType == "인기순") "like" else "latest"
viewModel.changeSortOrder(isbn, apiSortType)
},
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

정렬 타입 매핑에 하드코딩된 문자열 제거

"인기순" 같은 하드코딩은 다국어/문구 변경 시 깨집니다. UI에서 선택한 option 문자열과 filterOptions를 이용하거나 enum/상수로 매핑하세요.

예시:

-        onSortChange = { sortType ->
-            val apiSortType = if (sortType == "인기순") "like" else "latest"
-            viewModel.changeSortOrder(isbn, apiSortType)
-        },
+        onSortChange = { sortType ->
+            val likeLabel = stringResource(R.string.sort_like)
+            val apiSortType = if (sortType == likeLabel) "like" else "latest"
+            viewModel.changeSortOrder(isbn, apiSortType)
+        },
📝 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
onSortChange = { sortType ->
val apiSortType = if (sortType == "인기순") "like" else "latest"
viewModel.changeSortOrder(isbn, apiSortType)
},
onSortChange = { sortType ->
val likeLabel = stringResource(R.string.sort_like)
val apiSortType = if (sortType == likeLabel) "like" else "latest"
viewModel.changeSortOrder(isbn, apiSortType)
},
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookDetailScreen.kt
around lines 102–105, remove the hardcoded comparison to the Korean label "인기순"
and map UI selection to API sort keys via a stable identifier (use the option's
key/value from filterOptions or an enum/constant map) instead: derive
apiSortType from the selected option's key or a lookup map (e.g., enum or
Map<String, String> that maps option keys/ids to "like"/"latest"), then call
viewModel.changeSortOrder(isbn, apiSortType); do not rely on display strings for
logic so localization or text changes won’t break behavior.

Comment on lines 155 to 159
LaunchedEffect(shouldLoadMore) {
if (shouldLoadMore && uiState?.isLoadingMore == false && uiState.isLast == false) {
onLoadMore()
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

널 안정성 버그: uiState.isLast 접근 시 NPE/컴파일 오류

uiState가 nullable인데 isLast에는 안전 호출이 빠져 있습니다. 컴파일 오류입니다. 아래처럼 수정하세요.

-    if (shouldLoadMore && uiState?.isLoadingMore == false && uiState.isLast == false) {
+    if (shouldLoadMore && uiState?.isLoadingMore == false && uiState?.isLast == false) {
         onLoadMore()
     }
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookDetailScreen.kt
around lines 155–159, uiState is nullable but the code accesses uiState.isLast
without a safe call causing a compile/NPE risk; fix by guarding uiState before
accessing isLast (either use a null-check early in the LaunchedEffect or use a
safe-call like uiState?.isLast) so the condition becomes fully null-safe (e.g.,
ensure both isLoadingMore and isLast are accessed via uiState? or bail out when
uiState is null) and keep the onLoadMore() invocation unchanged.

Comment on lines +148 to +172
updateState { it.copy(isSaving = true, error = null) }

bookRepository.saveBook(isbn, type)
.onSuccess { saveResponse ->
saveResponse?.let {
saveResponse?.let { it ->
// 책 상세 정보의 isSaved 상태 업데이트
val updatedBookDetail = _uiState.value.bookDetail?.copy(isSaved = it.isSaved)
_uiState.value = _uiState.value.copy(
bookDetail = updatedBookDetail,
isSaving = false,
error = null
)
val updatedBookDetail =
_uiState.value.bookDetail?.copy(isSaved = it.isSaved)
updateState {
it.copy(
bookDetail = updatedBookDetail,
isSaving = false,
error = null
)
}
}
}
.onFailure { exception ->
_uiState.value = _uiState.value.copy(
isSaving = false,
error = exception.message ?: stringResourceProvider.getString(R.string.error_book_save_failed)
)
updateState {
it.copy(
isSaving = false,
error = exception.message
?: stringResourceProvider.getString(R.string.error_book_save_failed)
)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

saveBook 성공 응답 null 시 isSaving 해제 누락 및 가독성 저하(it 중첩)

  • onSuccess에서 응답이 null이면 isSaving=false가 되지 않아 로딩이 지속될 수 있습니다.
  • let { it -> }updateState { it -> }의 it 중첩은 가독성을 해칩니다.

아래와 같이 null 응답 처리 및 변수명 개선을 제안합니다.

-            bookRepository.saveBook(isbn, type)
-                .onSuccess { saveResponse ->
-                    saveResponse?.let { it ->
-                        // 책 상세 정보의 isSaved 상태 업데이트
-                        val updatedBookDetail =
-                            _uiState.value.bookDetail?.copy(isSaved = it.isSaved)
-                        updateState {
-                            it.copy(
-                                bookDetail = updatedBookDetail,
-                                isSaving = false,
-                                error = null
-                            )
-                        }
-                    }
-                }
+            bookRepository.saveBook(isbn, type)
+                .onSuccess { response ->
+                    if (response == null) {
+                        updateState {
+                            it.copy(
+                                isSaving = false,
+                                error = stringResourceProvider.getString(R.string.error_book_save_failed)
+                            )
+                        }
+                        return@onSuccess
+                    }
+                    val updatedBookDetail = _uiState.value.bookDetail?.copy(isSaved = response.isSaved)
+                    updateState { state ->
+                        state.copy(
+                            bookDetail = updatedBookDetail,
+                            isSaving = false,
+                            error = null
+                        )
+                    }
+                }
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/ui/search/viewmodel/BookDetailViewModel.kt
around lines 148–172, the onSuccess branch fails to clear isSaving when the
saveResponse is null and the nested use of `it` (let { it -> } and updateState {
it -> }) reduces readability; fix by treating null saveResponse explicitly: call
updateState to set isSaving = false and error = null when saveResponse is null,
and for the non-null case compute an updatedBookDetail then call updateState to
set bookDetail and isSaving = false; also rename lambda parameters to avoid
shadowing (e.g., use saveResponse or result and prevState) so there are no
nested `it` usages.

@rbqks529 rbqks529 merged commit 3053422 into THIP-TextHip:develop Aug 18, 2025
1 check was pending
Copy link

@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: 1

♻️ Duplicate comments (2)
app/src/main/java/com/texthip/thip/ui/search/viewmodel/BookDetailViewModel.kt (2)

119-127: 이전 스냅샷(currentState) 사용으로 최신 상태 덮어쓰기 가능성

updateState 클로저 내부에서 currentState.relatedFeeds를 사용하면, 동시 호출/지연으로 최신 state.relatedFeeds를 덮어쓸 수 있습니다. 클로저 인자(state)를 사용하세요.

-                    updateState { state ->
-                        state.copy(
-                            relatedFeeds = currentState.relatedFeeds + (response?.feeds
-                                ?: emptyList()),
-                            nextCursor = response?.nextCursor,
-                            isLast = response?.isLast ?: true,
-                            isLoadingMore = false
-                        )
-                    }
+                    updateState { state ->
+                        state.copy(
+                            relatedFeeds = state.relatedFeeds + (response?.feeds ?: emptyList()),
+                            nextCursor = response?.nextCursor,
+                            isLast = response?.isLast ?: true,
+                            isLoadingMore = false,
+                            feedError = null
+                        )
+                    }

151-163: saveBook 응답 null 시 isSaving 해제 누락(무한 로딩) 및 it 중첩 가독성 저하

응답이 null이면 onSuccess 블록이 아무 것도 하지 않아 isSaving=false가 되지 않습니다. 또한 it 중첩은 가독성을 해칩니다. null 처리 및 변수명을 명확히 해주세요.

-                .onSuccess { saveResponse ->
-                    saveResponse?.let { it ->
-                        // 책 상세 정보의 isSaved 상태 업데이트
-                        val updatedBookDetail =
-                            _uiState.value.bookDetail?.copy(isSaved = it.isSaved)
-                        updateState {
-                            it.copy(
-                                bookDetail = updatedBookDetail,
-                                isSaving = false,
-                                error = null
-                            )
-                        }
-                    }
-                }
+                .onSuccess { response ->
+                    if (response == null) {
+                        updateState { state ->
+                            state.copy(
+                                isSaving = false,
+                                error = stringResourceProvider.getString(R.string.error_book_save_failed)
+                            )
+                        }
+                        return@onSuccess
+                    }
+                    // 책 상세 정보의 isSaved 상태 업데이트
+                    val updatedBookDetail = _uiState.value.bookDetail?.copy(isSaved = response.isSaved)
+                    updateState { state ->
+                        state.copy(
+                            bookDetail = updatedBookDetail,
+                            isSaving = false,
+                            error = null
+                        )
+                    }
+                }
🧹 Nitpick comments (11)
app/src/main/java/com/texthip/thip/ui/search/viewmodel/BookDetailViewModel.kt (9)

30-31: 매직 스트링 제거: 정렬 값 상수화 권장

"like" 문자열 리터럴은 오타/변경에 취약합니다. 파일 상단 또는 파일 스코프에 정렬 상수를 두고 사용하도록 변경해 주세요.

다음과 같이 기본 정렬을 상수로 분리하고 사용을 교체하는 것을 제안합니다.

-    val currentSort: String = "like",
+    val currentSort: String = DEFAULT_SORT,

추가 코드(파일 내 다른 위치에 추가):

// 파일 상단(패키지 선언 아래) 또는 ViewModel 하단에 배치
private const val DEFAULT_SORT = "like"

47-49: StateFlow 원자적 갱신 확보: update 확장 함수 사용

동시 갱신 가능성이 있는 경우를 대비해 MutableStateFlow.update 확장 함수를 사용하는 것이 안전합니다. 현재 구현도 메인 스레드 사용 시 문제 없겠지만 update를 사용하면 더 견고합니다.

-    private fun updateState(update: (BookDetailUiState) -> BookDetailUiState) {
-        _uiState.value = update(_uiState.value)
-    }
+    private fun updateState(update: (BookDetailUiState) -> BookDetailUiState) {
+        _uiState.update(update)
+    }

추가 import:

import kotlinx.coroutines.flow.update

65-66: 기본 정렬값도 상수 사용으로 통일

loadRelatedFeeds 호출 시에도 상수를 사용해 일관성 유지하세요.

-                    loadRelatedFeeds(isbn, "like")
+                    loadRelatedFeeds(isbn, DEFAULT_SORT)

79-80: 함수 기본인자도 상수로 교체

서명 기본값의 매직 스트링 제거를 권장합니다.

-    fun loadRelatedFeeds(isbn: String, sort: String = "like") {
+    fun loadRelatedFeeds(isbn: String, sort: String = DEFAULT_SORT) {

81-82: 관련 피드 로딩 시 에러 상태 초기화 및 실패 기본 메시지 제공

  • 로딩 시작 시 이전 feedError를 null로 초기화해 잔존 에러 표시를 방지하세요.
  • 실패 시 exception.message가 null인 경우를 대비해 사용자 친화적인 기본 에러 문자열을 설정하세요.
-            updateState { it.copy(isLoadingFeeds = true) }
+            updateState { it.copy(isLoadingFeeds = true, feedError = null) }

실패 처리의 예시(리소스 키는 프로젝트에서 사용 중인 공통 에러 문자열로 교체 필요):

// 제안: 실패 시
updateState {
    it.copy(
        isLoadingFeeds = false,
        feedError = exception.message
            ?: stringResourceProvider.getString(R.string.error_common_load_failed)
    )
}

필요 시 strings.xml에 error_common_load_failed(예: "목록을 불러오지 못했습니다.") 추가를 권장합니다.

Also applies to: 95-101


85-93: 성공 시 feedError를 null로 리셋

이전 실패로 설정된 feedError가 성공 후에도 남지 않도록 성공 브랜치에서 명시적으로 feedError를 null로 설정하세요.

                     updateState {
                         it.copy(
                             relatedFeeds = response?.feeds ?: emptyList(),
                             nextCursor = response?.nextCursor,
                             isLast = response?.isLast ?: true,
                             isLoadingFeeds = false,
-                            currentSort = sort
+                            currentSort = sort,
+                            feedError = null
                         )
                     }

106-117: 페이지네이션 중복 호출 레이스 윈도우 축소

isLoadingMore 플래그를 코루틴 런치 이전에 즉시 올리지 않아, 빠른 연속 호출 시 두 번째 호출도 진입할 수 있는 작은 레이스 윈도우가 있습니다. 플래그를 먼저 올리고 필요한 인자(sort/cursor)를 스냅샷으로 캡처한 후 네트워크를 시작하세요.

     fun loadMoreFeeds(isbn: String) {
-        val currentState = _uiState.value
-        if (currentState.isLoadingMore || currentState.isLast || currentState.nextCursor == null) return
-
-        viewModelScope.launch {
-            updateState { it.copy(isLoadingMore = true) }
-
-            feedRepository.getRelatedBookFeeds(
-                isbn,
-                currentState.currentSort,
-                currentState.nextCursor
-            )
+        val snapshot = _uiState.value
+        if (snapshot.isLoadingMore || snapshot.isLast || snapshot.nextCursor == null) return
+
+        // 중복 호출 방지: 플래그를 즉시 올립니다.
+        updateState { it.copy(isLoadingMore = true) }
+
+        val sort = snapshot.currentSort
+        val cursor = snapshot.nextCursor
+        viewModelScope.launch {
+            feedRepository.getRelatedBookFeeds(
+                isbn,
+                sort,
+                cursor
+            )

130-135: 페이지네이션 실패 시 기본 에러 메시지 제공

exception.message가 null일 수 있습니다. 사용자 메시지 일관성을 위해 feedError에 기본 문자열 리소스를 제공하세요.

예시(리소스 키는 프로젝트 기준에 맞게 조정):

updateState { state ->
    state.copy(
        isLoadingMore = false,
        feedError = exception.message
            ?: stringResourceProvider.getString(R.string.error_common_load_failed)
    )
}

178-195: toFeedItem 매핑 필드명 확인 요청

FeedItem의 생성자 파라미터명이 authName인지 authorName인지 확인 부탁드립니다. 오타일 경우 컴파일 에러 또는 UI 불일치가 발생할 수 있습니다. 또한 null 가능 필드가 있다면 기본값 처리(빈 문자열, 0 등)도 고려하세요.

필요하시면 FeedItem 정의에 맞춘 안전한 매핑(널 병합 연산자 활용)으로 패치 제안 드리겠습니다.

app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt (2)

60-87: 조건 분기 가독성 개선 제안(when + 지역 변수 도입)

동일 route 필드 접근을 반복하는 if-else 체인을 when과 지역 변수로 정리하면 가독성이 좋아집니다. 동작 변화 없이 리팩터 가능합니다.

-        LaunchedEffect(route) {
-            if (
-                route.feedId != null &&
-                route.editContentBody != null &&
-                route.isbn != null &&
-                route.bookTitle != null &&
-                route.bookAuthor != null
-            ) {
-                viewModel.setEditData(
-                    feedId = route.feedId,
-                    isbn = route.isbn,
-                    bookTitle = route.bookTitle,
-                    bookAuthor = route.bookAuthor,
-                    bookImageUrl = route.bookImageUrl ?: "",
-                    contentBody = route.editContentBody,
-                    isPublic = route.editIsPublic ?: true,
-                    tagList = route.editTagList ?: emptyList()
-                )
-            } else if (route.feedId != null) {
-                // 수정 모드: 기존 방식 (API 호출)
-                viewModel.loadFeedForEdit(route.feedId)
-            } else if (route.isbn != null &&
-                route.bookTitle != null &&
-                route.bookAuthor != null &&
-                route.bookImageUrl != null &&
-                route.recordContent != null
-            ) {
-                // 새 글 작성 모드: 기록장에서 온 데이터 설정
-                viewModel.setPinnedRecord(
-                    isbn = route.isbn,
-                    bookTitle = route.bookTitle,
-                    bookAuthor = route.bookAuthor,
-                    bookImageUrl = route.bookImageUrl,
-                    recordContent = route.recordContent
-                )
-            }
-        }
+        LaunchedEffect(route) {
+            val feedId = route.feedId
+            val editBody = route.editContentBody
+            val isbn = route.isbn
+            val title = route.bookTitle
+            val author = route.bookAuthor
+            val imageUrl = route.bookImageUrl
+            val record = route.recordContent
+
+            when {
+                feedId != null && editBody != null && isbn != null && title != null && author != null -> {
+                    viewModel.setEditData(
+                        feedId = feedId,
+                        isbn = isbn,
+                        bookTitle = title,
+                        bookAuthor = author,
+                        bookImageUrl = imageUrl ?: "",
+                        contentBody = editBody,
+                        isPublic = route.editIsPublic ?: true,
+                        tagList = route.editTagList ?: emptyList()
+                    )
+                }
+                feedId != null -> {
+                    // 수정 모드: 기존 방식 (API 호출)
+                    viewModel.loadFeedForEdit(feedId)
+                }
+                isbn != null && title != null && author != null && imageUrl != null && record != null -> {
+                    // 새 글 작성 모드: 기록장에서 온 데이터 설정
+                    viewModel.setPinnedRecord(
+                        isbn = isbn,
+                        bookTitle = title,
+                        bookAuthor = author,
+                        bookImageUrl = imageUrl,
+                        recordContent = record
+                    )
+                }
+            }
+        }

118-121: Comment 라우트 파싱은 toRoute로 일관화 추천

상단에서 toRoute<FeedRoutes.Write>()를 사용하듯, 이 부분도 toRoute<FeedRoutes.Comment>()로 통일하면 보일러플레이트가 줄고 타입 안전성이 올라갑니다.

-        val route = backStackEntry.arguments?.let {
-            FeedRoutes.Comment(it.getInt("feedId"))
-        } ?: return@composable
+        val route = backStackEntry.toRoute<FeedRoutes.Comment>()
📜 Review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3718996 and df35eb5.

📒 Files selected for processing (3)
  • app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt (4 hunks)
  • app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookDetailScreen.kt (12 hunks)
  • app/src/main/java/com/texthip/thip/ui/search/viewmodel/BookDetailViewModel.kt (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookDetailScreen.kt
🔇 Additional comments (5)
app/src/main/java/com/texthip/thip/ui/search/viewmodel/BookDetailViewModel.kt (1)

37-45: 전반적 구조와 DI, 상태 관리 패턴은 깔끔합니다

Hilt 주입, UI 상태 단일 소스(MutableStateFlow)와 updateState 헬퍼로의 일관된 갱신 패턴 모두 좋습니다. 이후 제안들은 세부 개선사항입니다.

app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt (4)

18-18: 신규 BookDetail 내비게이션 임포트 — OK

탐색 익스텐션 임포트가 적절히 추가되었습니다.


61-77: 수정 모드 널 안전성 강화(!! 제거) — 이전 리뷰 피드백 반영 확인

isbn/bookTitle/bookAuthor까지 널 체크를 조건에 포함해 !!를 제거한 점 좋습니다. 런타임 크래시 리스크가 사라졌습니다.


101-106: 피드 생성 결과 전달 및 복귀 플로우 — OK

getBackStackEntry(MainTabRoutes.Feed)에 결과를 저장 후 popBackStack으로 자연스럽게 복귀하는 패턴이 일관되고 안전합니다.


127-129: 댓글 화면 → 수정 화면 내비게이션 연결 — OK

FeedRoutes.Write(feedId = feedId)로 진입하면 Write 화면의 분기 로직과 잘 맞습니다.

Comment on lines +41 to 43
onNavigateToBookDetail = { isbn ->
navController.navigateToBookDetail(isbn)
}
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

FeedScreen에 도서 상세 진입 콜백 연결 — OK. 다른 호출부/시그니처 검증 권장

FeedScreen에 onNavigateToBookDetail 콜백을 안전하게 연결했습니다. 나머지 호출부/정의와 시그니처가 일치하는지 빠르게 점검해 주세요.

다음 스크립트로 FeedScreen/FeedCommentScreen 시그니처와 호출부를 확인할 수 있습니다.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== FeedScreen 시그니처에 onNavigateToBookDetail 포함 여부 =="
rg -nP --type=kt -C3 '@Composable\s+fun\s+FeedScreen\s*\(' | rg -n 'onNavigateToBookDetail' || true

echo
echo "== FeedScreen 호출부들이 onNavigateToBookDetail을 전달하는지 =="
rg -nP --type=kt -C2 '\bFeedScreen\s*\(' | rg -n 'onNavigateToBookDetail' || true

echo
echo "== FeedCommentScreen 시그니처에 onNavigateToFeedEdit 포함 여부 =="
rg -nP --type=kt -C3 '@Composable\s+fun\s+FeedCommentScreen\s*\(' | rg -n 'onNavigateToFeedEdit' || true

Length of output: 751


다음과 같이 --type=kt 대신 -g '*.kt' 패턴으로 .kt 파일을 대상 검색하도록 스크립트를 수정했습니다. 실행 후 결과를 공유해 주세요.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== FeedScreen 시그니처에 onNavigateToBookDetail 포함 여부 =="
rg -nP -g '*.kt' -C3 '@Composable\s+fun\s+FeedScreen\s*\(' | rg -n 'onNavigateToBookDetail' || true

echo
echo "== FeedScreen 호출부들이 onNavigateToBookDetail을 전달하는지 =="
rg -nP -g '*.kt' -C2 '\bFeedScreen\s*\(' | rg -n 'onNavigateToBookDetail' || true

echo
echo "== FeedCommentScreen 시그니처에 onNavigateToFeedEdit 포함 여부 =="
rg -nP -g '*.kt' -C3 '@Composable\s+fun\s+FeedCommentScreen\s*\(' | rg -n 'onNavigateToFeedEdit' || true

Length of output: 887


FeedScreen/FeedCommentScreen 시그니처에 콜백 파라미터 누락 — 시그니처 수정 필요

FeedNavigation.kt에서 아래 콜백을 전달하고 있지만, 실제 컴포저블 함수 시그니처에는 포함되어 있지 않아 컴파일 오류가 발생합니다. 해당 시그니처를 수정해 주세요.

  • app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt
    기존

    @Composable
    fun FeedScreen(
        /**/
    ) { /**/ }

    수정

    @Composable
    fun FeedScreen(
        /**/,
        onNavigateToBookDetail: (String) -> Unit,
    ) { /**/ }
  • app/src/main/java/com/texthip/thip/ui/feed/screen/FeedCommentScreen.kt
    기존

    @Composable
    fun FeedCommentScreen(
        /**/
    ) { /**/ }

    수정

    @Composable
    fun FeedCommentScreen(
        /**/,
        onNavigateToFeedEdit: (String) -> Unit,
    ) { /**/ }

수정 후 다시 컴파일 및 네비게이션 흐름이 정상 동작하는지 확인 부탁드립니다.

🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt
around lines 41-43, FeedNavigation passes callbacks that are missing from the
composable signatures, causing compile errors; update the composable
declarations to accept the callbacks: modify FeedScreen in
app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt to add the
parameter onNavigateToBookDetail: (String) -> Unit, and modify FeedCommentScreen
in app/src/main/java/com/texthip/thip/ui/feed/screen/FeedCommentScreen.kt to add
the parameter onNavigateToFeedEdit: (String) -> Unit, then propagate these
parameters through any intermediate calls/usages inside those files and update
any call sites to supply the callbacks, rebuild and verify navigation flows
compile and work correctly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[API] 피드 api 연동 마무리 [(THIP2025-290)]

2 participants