Skip to content

[API] 피드 댓글 구현#98

Merged
Nico1eKim merged 11 commits intoTHIP-TextHip:developfrom
Nico1eKim:api/#97-feed_comments
Aug 18, 2025
Merged

[API] 피드 댓글 구현#98
Nico1eKim merged 11 commits intoTHIP-TextHip:developfrom
Nico1eKim:api/#97-feed_comments

Conversation

@Nico1eKim
Copy link
Member

@Nico1eKim Nico1eKim commented Aug 18, 2025

➕ 이슈 링크


🔎 작업 내용

  • 피드 댓글 조회, 생성, 좋아요, 삭제를 구현했습니다
  • 오늘의 한마디 조회를 구현했습니다

📸 스크린샷

피드 댓글 조회, 생성, 좋아요, 삭제

2025-08-18.4.59.26.mov

오늘의 한마디 조회

KakaoTalk_Video_2025-08-18-20-21-41.mp4

😢 해결하지 못한 과제


📢 리뷰어들에게

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

Summary by CodeRabbit

  • New Features

    • 댓글 액션 모드 추가(팝업/바텀시트) 및 댓글/대댓글에서 삭제·신고 팝업 지원.
    • 댓글 생성 시 작성자·별칭·프로필·작성일·내용·좋아요·대댓글 목록 등 더 풍부한 정보가 즉시 반영.
    • 룸 데일리 인사 조회 페이징(무한스크롤) 및 인사 등록 응답/토스트 추가.
  • Refactor

    • 댓글·피드·그룹챗 화면을 상태 기반으로 재구성: 로딩·오류·페이지네이션·입력 포커스/키보드 처리 개선.
    • 프로필 이미지 URL 기반 비동기 로딩으로 이미지 표시 개선.
  • Style / UI

    • 일부 토스트 메시지 색상 및 문구(일일 작성 한도 안내) 개선.

@coderabbitai
Copy link

coderabbitai bot commented Aug 18, 2025

Walkthrough

댓글/룸 관련 데이터 모델 확장 및 API/리포지토 시그니처 변경, 댓글 UI에 액션 모드(POPUP/BOTTOM_SHEET)·팝업 컴포넌트 추가, 피드 댓글 화면을 ViewModel 분리(MVVM)로 리팩터링하고 댓글 생성/조회 로직을 ViewModel에서 응답 기반으로 즉시 갱신하도록 변경.

Changes

Cohort / File(s) Summary
댓글 생성 응답 모델
app/src/main/java/.../data/model/comments/response/CommentsCreateResponse.kt
CommentsCreateResponse가 단일 commentId → nullable commentId 및 작성자/별칭/본문/플래그/replyList 등 확장 필드로 변경.
액션 모드 및 팝업 UI
app/src/main/java/.../ui/common/CommentActionMode.kt, app/src/main/java/.../ui/feed/component/CommentPopup.kt
CommentActionMode(POPUP, BOTTOM_SHEET) 추가 및 CommentActionPopup 컴포저블 추가.
피드 댓글 화면 — MVVM 리팩터
app/src/main/java/.../ui/feed/screen/FeedCommentScreen.kt
FeedDetailViewModelCommentsViewModel로 분리; 댓글 초기화에 postType = "FEED" 전달; 입력/답글/선택 상태 로컬 관리 및 CommentsEvent로 동작 전파.
그룹/노트 댓글 컴포넌트 변경
app/src/main/java/.../ui/group/note/component/CommentBottomSheet.kt, .../CommentItem.kt, .../CommentSection.kt, .../ReplyItem.kt
CommentSection/CommentItem/ReplyItem 시그니처에 actionMode, isSelected, onDismissPopup, onEvent 등 추가. POPUP/BOTTOM_SHEET 모드 처리 및 팝업 삭제/신고 흐름 추가.
Comments ViewModel / Repository 변경
app/src/main/java/.../ui/group/note/viewmodel/CommentsViewModel.kt, repository interfaces
createComment 성공 시 전체 재요청 대신 응답으로 UI 리스트 직접 갱신(최상위/대댓글 분기). getComments(postId, postType, cursor) 시그니처로 변경 및 페이징 처리 추가.
룸(rooms) API/모델 변경
app/src/main/java/.../data/model/rooms/*, .../data/service/RoomsService.kt, .../data/repository/RoomsRepository.kt
RoomsDailyGreetingRequestRoomsCreateDailyGreetingRequest로 타입명 변경, RoomsCreateDailyGreetingResponse 추가, GET /rooms/{roomId}/daily-greeting 엔드포인트 추가 및 RoomsDailyGreetingResponse를 오늘댓글 리스트+페이징 구조로 변경.
UI 모델/컴포저블 업데이트 및 mock 삭제
app/src/main/java/.../ui/common/cards/CardCommentGroup.kt, .../ui/common/header/ProfileBarWithDate.kt, .../ui/group/note/mock/ReplyData.kt (삭제)
CardCommentGroupTodayCommentList 사용으로 변경. ProfileBarWithDatePainter?String(이미지 URL)으로 변경(AsyncImage 사용). ReplyData mock 파일 삭제.
그룹 채팅 화면 및 ViewModel 상태화
app/src/main/java/.../ui/group/room/screen/GroupRoomChatScreen.kt, .../viewmodel/GroupRoomChatViewModel.kt
GroupRoomChatScreen이 UI state 기반으로 재작성(오늘댓글 TodayCommentList 사용), pagination(LoadMore) 및 UI 상태(GroupRoomChatUiState) 도입.
토스트/문자열 리소스 변경
app/src/main/java/.../ui/common/modal/ToastWithDate.kt, app/src/main/res/values/strings.xml
ToastWithDatecolor 파라미터 추가 및 기본 메시지 설정; 문자열 리소스 group_room_chat_max 추가.

Sequence Diagram(s)

sequenceDiagram
  actor U as 사용자
  participant FS as FeedCommentScreen (UI)
  participant CVM as CommentsViewModel
  participant Repo as CommentsRepository
  participant API as 서버

  U->>FS: 댓글 입력 후 전송
  FS->>CVM: CommentsEvent.CreateComment(content, parentId)
  CVM->>Repo: createComment(postId, postType, content, parentId)
  Repo->>API: POST /comments
  API-->>Repo: CommentsCreateResponse
  Repo-->>CVM: Response
  alt parentId == null
    CVM->>FS: 새 댓글을 comments 리스트 앞에 추가 (prepend)
  else parentId != null
    CVM->>FS: 해당 부모 댓글의 replyList 갱신 (replace parent)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Assessment against linked issues

Objective Addressed Explanation
피드 댓글 조회, 페이징 포함 (#97)
피드 댓글/답글 작성 (#97)
피드 댓글 삭제 (#97) 삭제 UI 및 이벤트는 추가되었으나, 삭제 API 호출의 연결이 이 diff에서 명확히 보이지 않음.
피드 댓글 좋아요 (#97) 좋아요 UI는 존재하나, 좋아요 API 호출/동기화 구현이 이 diff에서 명확하지 않음.

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
ProfileBarWithDate: Painter? → String(AsyncImage) 변경 (app/src/main/java/com/texthip/thip/ui/common/header/ProfileBarWithDate.kt) 피드 댓글 API 연동(#97) 목적과 직접 관련 없음 — 이미지 로드 방식 변경은 별개 UI 개선임.
Rooms API 구조 및 엔드포인트 추가/변경 (app/src/main/java/.../data/service/RoomsService.kt, .../repository/RoomsRepository.kt, .../data/model/rooms/*) #97은 피드 댓글 연동 관련; 룸(daily greeting) 엔드포인트 및 모델 변경은 다른 기능 영역임.
Mock 데이터 파일 삭제 (app/src/main/java/.../ui/group/note/mock/ReplyData.kt) 테스트/데모용 mock 제거로 보이며, API 연동 목표와 직접적인 관련 없음.

Possibly related issues

Possibly related PRs

Suggested reviewers

  • JJUYAAA
  • rbqks529

Poem

토끼가 키보드를 톡톡 두드리네 🐇
댓글은 쑥쑥, 팝업은 뿅뿅
MVVM으로 정리된 숲 속 길
응답 한 조각으로 리스트가 춤춰요
당근 축하, 배포 잘되길!

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

🪧 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

@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

🔭 Outside diff range comments (1)
app/src/main/java/com/texthip/thip/ui/group/note/component/CommentSection.kt (1)

33-35: LazyColumn 아이템 내 fillMaxHeight()로 인해 각 댓글이 화면 전체 높이를 점유할 가능성

LazyColumn의 아이템에서 fillMaxHeight()를 사용하면 각 아이템이 뷰포트 최대 높이로 측정되어 한 화면에 댓글 하나만 보이는 현상이 발생할 수 있습니다. 댓글 목록 UX에 치명적일 수 있으니 fillMaxWidth() 또는 기본 높이(컨텐츠 래핑)로 변경하는 것을 권장합니다.

아래처럼 수정 제안드립니다:

-        Column(
-            modifier = Modifier
-                .fillMaxHeight()
-                .padding(20.dp),
+        Column(
+            modifier = Modifier
+                .fillMaxWidth()
+                .padding(20.dp),
♻️ Duplicate comments (1)
app/src/main/java/com/texthip/thip/ui/group/note/component/ReplyItem.kt (1)

126-133: 신고 액션 TODO 후속 작업 (중복 코멘트)

신고 분기 TODO는 상위(CommentItem)와 동일한 맥락입니다. 이벤트 추가 및 ViewModel 처리 연계를 제안드립니다.

🧹 Nitpick comments (17)
app/src/main/java/com/texthip/thip/ui/common/CommentActionMode.kt (1)

3-6: KDoc 추가 제안 (가독성/유지보수성 향상)

해당 enum은 앱 전반에서 액션 UI 방식 제어의 핵심 스위치가 될 가능성이 큽니다. 간단한 KDoc을 추가해 의도를 명시하면 이후 유지보수 시 도움이 됩니다.

 enum class CommentActionMode {
+    /**
+     * 댓글 액션 표시 방식
+     * - POPUP: 아이템 인접 경량 팝업
+     * - BOTTOM_SHEET: 하단 시트
+     */
     POPUP,
     BOTTOM_SHEET
 }
app/src/main/java/com/texthip/thip/ui/feed/component/CommentPopup.kt (4)

34-39: 정렬 기준 확인 필요: 팝업 정렬/오프셋이 전체 윈도우 기준일 수 있음

Popupalignment = Alignment.BottomEnd는 기본적으로 윈도우 기준입니다. 리스트 아이템 인접에 붙이려는 의도라면, 현재 구현은 화면 우하단에만 나타날 수 있습니다. 필요하다면 PopupPositionProvider를 커스텀하거나, 앵커 기반 컴포넌트(DropdownMenu) 사용을 검토해 주세요.

원하는 동작:

  • 아이템 우상단 아이콘 기준 45.dp 아래로 표시 → 앵커 뷰(아이콘)의 좌표를 기준으로 결정 필요.

대안:

  • 간단: DropdownMenu(expanded, onDismissRequest, offset = DpOffset(...))로 전환
  • 정밀: Popup(positionProvider = object : PopupPositionProvider { ... }) 구현

41-53: 모서리 라운드 시 시각적 클리핑 보장 및 중복 Shape 정리

현재 background(shape)border(shape)만으로는 내용/리플이 정확히 라운드로 클리핑되지 않을 수 있습니다. clip(RoundedCornerShape(...)) 추가와 Shape 재사용을 권장합니다.

-        Box(
-            modifier = Modifier
-                .background(
-                    colors.Black,
-                    RoundedCornerShape(12.dp)
-                )
-                .border(
-                    width = 1.dp,
-                    color = colors.White,
-                    shape = RoundedCornerShape(12.dp)
-                )
-                .clickable { onClick() }
-                .padding(horizontal = 20.dp, vertical = 12.dp)
-        ) {
+        val shape = RoundedCornerShape(12.dp)
+        Box(
+            modifier = Modifier
+                .clip(shape)
+                .background(colors.Black, shape)
+                .border(width = 1.dp, color = colors.White, shape = shape)
+                .clickable { onClick() }
+                .padding(horizontal = 20.dp, vertical = 12.dp)
+        ) {

31-33: 밀도 변환 간소화 (가독성 개선)

toPx().roundToInt() 대신 roundToPx()를 쓰면 간결합니다.

-    val density = LocalDensity.current
-    val yOffsetPx = with(density) { 45.dp.toPx() }.roundToInt()
+    val yOffsetPx = with(LocalDensity.current) { 45.dp.roundToPx() }

63-71: 프리뷰에 테마 래핑 권장

프리뷰에서 ThipTheme.colors/typography에 의존하므로 테마로 감싸 일관된 미리보기를 보장하는 것이 좋습니다.

 @Preview
 @Composable
 private fun CommentActionPopupPreview() {
-    CommentActionPopup(
-        text = "삭제하기",
-        onClick = { },
-        onDismissRequest = { }
-    )
+    com.texthip.thip.ui.theme.ThipTheme {
+        CommentActionPopup(
+            text = "삭제하기",
+            onClick = { },
+            onDismissRequest = { }
+        )
+    }
 }
app/src/main/java/com/texthip/thip/data/model/comments/response/CommentsCreateResponse.kt (1)

15-19: 직렬화 안전성 향상: 기본값 추가 권장

서버 응답에서 일부 필드가 누락되거나 null인 경우를 방어하기 위해 non-null 필드에 기본값을 지정하면 kotlinx.serialization에서 MissingFieldException을 피할 수 있습니다. 특히 replyList는 빈 리스트 기본값을 권장합니다.

-    val likeCount: Int,
-    val isDeleted: Boolean,
-    val isWriter: Boolean,
-    val isLike: Boolean,
-    val replyList: List<ReplyList>,
+    val likeCount: Int = 0,
+    val isDeleted: Boolean = false,
+    val isWriter: Boolean = false,
+    val isLike: Boolean = false,
+    val replyList: List<ReplyList> = emptyList(),

추가 확인 필요:

  • 서버 스키마에서 위 필드가 항상 존재/비null 보장인지 확인 부탁드립니다. 보장되지 않는다면 위 기본값 적용을 권장합니다.
app/src/main/java/com/texthip/thip/ui/group/note/component/CommentBottomSheet.kt (1)

232-239: 하드코딩된 액션 모드 → 매개변수화로 유연성 확보

현재 CommentSection에 항상 CommentActionMode.BOTTOM_SHEET를 전달하고 있습니다. 화면 재사용성과 테스트 용이성을 위해 CommentBottomSheet 자체에 actionMode 파라미터(기본값 BOTTOM_SHEET)를 추가하고 이를 전달하도록 변경을 권장합니다.

적용 예시(선택된 범위 외 변경 포함):

  • 함수 시그니처에 파라미터 추가
@Composable
fun CommentBottomSheet(
    uiState: CommentsUiState,
    onEvent: (CommentsEvent) -> Unit,
    onDismiss: () -> Unit,
    onSendReply: (text: String, parentCommentId: Int?, replyToNickname: String?) -> Unit,
    actionMode: com.texthip.thip.ui.common.CommentActionMode = com.texthip.thip.ui.common.CommentActionMode.BOTTOM_SHEET
) { ... }
  • 내부 CommentLazyList에도 전달하도록 시그니처/호출부 확장
@Composable
private fun CommentLazyList(
    commentList: List<CommentList>,
    isLoadingMore: Boolean,
    isLastPage: Boolean,
    onLoadMore: () -> Unit,
    onReplyClick: (commentId: Int, nickname: String?) -> Unit,
    onEvent: (CommentsEvent) -> Unit,
    onCommentLongPress: (CommentList) -> Unit,
    onReplyLongPress: (ReplyList) -> Unit,
    actionMode: CommentActionMode
) { ... }

// 호출부
CommentLazyList(
    commentList = uiState.comments,
    isLoadingMore = uiState.isLoadingMore,
    isLastPage = uiState.isLast,
    onLoadMore = { onEvent(CommentsEvent.LoadMoreComments) },
    onReplyClick = { commentId, nickname -> ... },
    onEvent = onEvent,
    onCommentLongPress = { comment -> selectedCommentForMenu = comment },
    onReplyLongPress = { reply -> selectedReplyForMenu = reply },
    actionMode = actionMode
)
  • 선택된 라인에서의 변경
             CommentSection(
                 commentItem = comment,
-                actionMode = CommentActionMode.BOTTOM_SHEET,
+                actionMode = actionMode,
                 onReplyClick = onReplyClick,
                 onEvent = onEvent,
                 onCommentLongPress = onCommentLongPress,
                 onReplyLongPress = onReplyLongPress
             )
app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/CommentsViewModel.kt (1)

115-166: 댓글/답글 생성 응답 처리 로직 검증 필요 (부모 교체 vs. 자식 추가)

현재 로직은 parentId == null이면 새 부모 댓글을 prepend, parentId != null이면 응답(res)로부터 부모 댓글 전체를 새로 만들어 기존 부모를 통째로 교체합니다. 이때 서버가 “답글 생성”에 대해

  • A) 갱신된 부모 댓글(전체 replyList 포함)을 반환하는지
  • B) 새로 생성된 “답글 단건”만 반환하는지
    에 따라 안전성이 갈립니다.

만약 B) 케이스라면 현재 구현은 부모를 “답글 단건” 데이터로 오염시키거나 기존 replyList를 잃어버릴 수 있습니다.

권장 방향(양쪽을 모두 방어):

  • res가 부모를 의미하는지 판별(예: res.commentId == parentId 또는 res.replyList.isNotEmpty()) 후
    • 부모면 교체
    • 자식이면 기존 부모.replyList에 추가/머지
  • 또는 서버 스키마를 명확히 하고 A)만 보장된다면 주석/테스트로 가정 고정

원하시면 ReplyList 구조에 맞춰 머지 전략까지 포함한 안전한 구현 패치를 작성해 드리겠습니다.

app/src/main/java/com/texthip/thip/ui/group/note/component/CommentSection.kt (3)

26-28: actionMode 기본값 제공으로 호출부 영향 최소화 제안

외부/기존 호출부와의 호환성 및 미리보기 편의성을 위해 actionMode에 기본값을 제공하는 것을 권장합니다. 현재 PR 범위에서는 모두 전달하고 있으나, 재사용 컴포넌트 특성상 기본값이 안전합니다.

-    actionMode: CommentActionMode,
+    actionMode: CommentActionMode = CommentActionMode.POPUP,

56-58: 선택 판별 로직의 중복을 줄여 가독성 향상

isSelected = selectedCommentId != null && ... 로직이 댓글/답글에 반복되고 있습니다. 상단에 지역 변수로 추출하면 가독성과 유지보수성이 좋아집니다.

예시:

// 함수 시작 직후 등 적절한 위치
val isCommentSelected = selectedCommentId != null && commentItem.commentId == selectedCommentId
...
isSelected = isCommentSelected

답글도 동일 방식으로 val isReplySelected = ... 추출을 고려해 주세요.

Also applies to: 74-76


41-53: TODO 주석 처리 계획 확정

“// todo: 수정 가능” 주석이 남아 있습니다. 로직상 commentId null이면 액션 비활성화가 맞다면, TODO 제거 또는 명확한 코멘트로 교체를 제안드립니다. 향후 변경 계획이 있다면 이슈로 전환해 추적 가능하게 해두는 것이 좋습니다.

필요하시면 관련 이슈 템플릿/본문을 작성해 드릴게요.

app/src/main/java/com/texthip/thip/ui/group/note/component/CommentItem.kt (3)

58-59: 불필요한 단독 식(“data”) 제거

data 단독 식은 아무 효과가 없고 경고를 유발합니다. 제거해 주세요.

-                data
                 ProfileBarFeed(

44-56: 롱프레스 범위 확장: 아이템 전체(Box)로 제스처 영역 이동 권장

현재 롱프레스 제스처가 본문 컬럼에만 적용되어 우측 좋아요 영역(long press)에서는 동작하지 않습니다. 사용자 기대에 맞게 아이템 전체 영역(Box)에 적용하는 것을 권장합니다.

-    Box {
+    Box(
+        modifier = modifier.pointerInput(Unit) {
+            detectTapGestures(onLongPress = { onLongPress() })
+        }
+    ) {
...
-            Column(
-                modifier = modifier.pointerInput(Unit) {
-                    detectTapGestures(onLongPress = { onLongPress() })
-                },
+            Column(
+                modifier = modifier,
                 verticalArrangement = Arrangement.spacedBy(12.dp)
             ) {

114-120: 신고 액션 TODO 후속 작업 권장

신고 분기에서 TODO가 남아 있습니다. ViewModel의 CommentsEvent에 신고 이벤트(예: ReportComment)를 추가하고 서버 연동을 붙이는 방향을 제안드립니다.

원하시면 CommentsEvent/ViewModel 처리/호출부까지 스캐폴딩 코드를 생성해 드릴게요.

app/src/main/java/com/texthip/thip/ui/group/note/component/ReplyItem.kt (1)

47-49: 롱프레스 범위 확장: 전체 Row에 제스처 적용

현재 롱프레스가 본문 컬럼에만 적용되어 좌측 리플 아이콘/우측 좋아요 영역에서는 동작하지 않을 수 있습니다. 전체 Row에 적용하는 것을 권장합니다.

-        Row(
-            horizontalArrangement = Arrangement.spacedBy(8.dp)
-        ) {
+        Row(
+            modifier = modifier.pointerInput(Unit) {
+                detectTapGestures(onLongPress = { onLongPress() })
+            },
+            horizontalArrangement = Arrangement.spacedBy(8.dp)
+        ) {
...
-            Column(
-                modifier = modifier.pointerInput(Unit) {
-                    detectTapGestures(onLongPress = { onLongPress() })
-                },
+            Column(
+                modifier = modifier,
                 verticalArrangement = Arrangement.spacedBy(12.dp)
             ) {

Also applies to: 56-60

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

68-70: Flow 수집에 collectAsStateWithLifecycle 사용 권장

수명 주기 인지 수집으로 더 안전한 상태 관리를 할 수 있습니다.

-    val feedDetailUiState by feedDetailViewModel.uiState.collectAsState()
-    val commentsUiState by commentsViewModel.uiState.collectAsState()
+    val feedDetailUiState by feedDetailViewModel.uiState.collectAsStateWithLifecycle()
+    val commentsUiState by commentsViewModel.uiState.collectAsStateWithLifecycle()

추가 import:

import androidx.lifecycle.compose.collectAsStateWithLifecycle

73-74: 하드코딩 문자열 대신 타입/상수를 사용해 의도 명확화

postType = "FEED"는 상수/enum으로 추출하면 오타 방지 및 호출부 가독성이 좋아집니다. 예: PostType.FEED.name 혹은 const val POST_TYPE_FEED = "FEED".

📜 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 cb26a6f.

📒 Files selected for processing (9)
  • app/src/main/java/com/texthip/thip/data/model/comments/response/CommentsCreateResponse.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/ui/common/CommentActionMode.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/ui/feed/component/CommentPopup.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/ui/feed/screen/FeedCommentScreen.kt (7 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/note/component/CommentBottomSheet.kt (2 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/note/component/CommentItem.kt (6 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/note/component/CommentSection.kt (5 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/note/component/ReplyItem.kt (4 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/CommentsViewModel.kt (3 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
app/src/main/java/com/texthip/thip/ui/group/note/component/CommentItem.kt (4)
app/src/main/java/com/texthip/thip/ui/common/header/ProfileBarFeed.kt (1)
  • ProfileBarFeed (27-80)
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/CommentPopup.kt (1)
  • CommentActionPopup (24-61)
app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/CommentsViewModel.kt (1)
  • onEvent (49-61)
app/src/main/java/com/texthip/thip/ui/group/note/component/ReplyItem.kt (4)
app/src/main/java/com/texthip/thip/ui/common/header/ProfileBarFeed.kt (1)
  • ProfileBarFeed (27-80)
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/CommentPopup.kt (1)
  • CommentActionPopup (24-61)
app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/CommentsViewModel.kt (1)
  • onEvent (49-61)
app/src/main/java/com/texthip/thip/ui/feed/screen/FeedCommentScreen.kt (1)
app/src/main/java/com/texthip/thip/ui/group/note/component/CommentSection.kt (1)
  • CommentSection (19-81)
🔇 Additional comments (8)
app/src/main/java/com/texthip/thip/ui/group/note/component/CommentBottomSheet.kt (1)

32-32: 새 enum 도입 확인

CommentActionMode 도입/임포트 OK입니다.

app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/CommentsViewModel.kt (2)

226-231: LGTM: 답글 좋아요 낙관적 업데이트

toMutableList().set(...)로 필요한 부분만 교체하는 낙관적 업데이트가 깔끔합니다. 실패 시 롤백도 적절합니다.


260-264: getComments 시그니처 변경( postType 추가 )가 전체 호출부에 정상 반영되었습니다

검증 결과, CommentsService, CommentsRepository, CommentsViewModel를 포함한 모든 호출부에서 postType 인자를 함께 전달하도록 업데이트된 것을 확인했습니다. 추가 조치가 필요 없습니다.

app/src/main/java/com/texthip/thip/ui/group/note/component/CommentSection.kt (1)

108-109: 프리뷰에 actionMode 전달 OK

프리뷰에서 actionMode = CommentActionMode.POPUP 전달이 반영되어 신규 API 사용 검증에 도움됩니다.

app/src/main/java/com/texthip/thip/ui/group/note/component/CommentItem.kt (1)

109-123: 팝업 액션 로직 구성 좋습니다

작성자 여부에 따라 삭제/신고를 분기하고, 액션 수행 후 onDismissPopup()으로 닫는 흐름이 명료합니다. PopupProperties(focusable = true)로 외부 탭 dismiss도 이미 대응되어 있어 UX 일관성도 좋습니다.

app/src/main/java/com/texthip/thip/ui/group/note/component/ReplyItem.kt (1)

122-136: 답글 팝업 액션 로직도 일관성 있게 잘 구성됨

작성자 여부에 따른 삭제/신고 분기, 액션 후 dismiss까지 CommentItem과 동일한 패턴으로 구현되어 재사용성과 일관성이 확보되었습니다.

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

270-294: CommentSection 통합 및 이벤트 전달 구조 명확

  • 아이템 key 제공으로 스크롤/재구성 안정성 확보
  • selectedCommentId/onDismissPopup으로 단일 선택 및 해제 흐름 명확
  • Reply/Comment 롱프레스 분기 처리도 직관적

전반적으로 MVVM 재구성과 함께 이벤트 라우팅이 잘 정리됐습니다.


139-144: 외부 탭으로 키보드/선택 해제 UX 적절

바깥 탭 시 키보드 및 선택 해제 처리로 팝업 잔존 상태를 방지하는 UX가 좋습니다. 스크롤 제스처와의 간섭도 적을 것으로 보입니다.

Comment on lines +34 to +39
Popup(
alignment = Alignment.BottomEnd,
offset = IntOffset(x = 0, yOffsetPx),
onDismissRequest = onDismissRequest,
properties = PopupProperties(focusable = true)
) {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

컴파일 오류: IntOffset 인자 네이밍 오류

IntOffset(x = 0, yOffsetPx)는 네이밍된 인자 사용 규칙 위반으로 컴파일되지 않습니다. 두 번째 인자도 이름을 명시해야 합니다.

     Popup(
         alignment = Alignment.BottomEnd,
-        offset = IntOffset(x = 0, yOffsetPx),
+        offset = IntOffset(x = 0, y = yOffsetPx),
         onDismissRequest = onDismissRequest,
         properties = PopupProperties(focusable = true)
     ) {
📝 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
Popup(
alignment = Alignment.BottomEnd,
offset = IntOffset(x = 0, yOffsetPx),
onDismissRequest = onDismissRequest,
properties = PopupProperties(focusable = true)
) {
Popup(
alignment = Alignment.BottomEnd,
offset = IntOffset(x = 0, y = yOffsetPx),
onDismissRequest = onDismissRequest,
properties = PopupProperties(focusable = true)
) {
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/ui/feed/component/CommentPopup.kt around
lines 34 to 39, the IntOffset construction uses a mixed positional/named
argument which causes a compile error; change the call to use both named
parameters (e.g., x = 0, y = yOffsetPx) or use two positional arguments (e.g.,
0, yOffsetPx) so the second parameter is named or positional consistently.

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: 2

🧹 Nitpick comments (12)
app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsCreateDailyGreetingResponse.kt (1)

7-10: 서버 JSON 네이밍 컨벤션 확인 및 필요시 매핑 추가 권장

서버가 snake_case를 반환한다면 현재 camelCase 프로퍼티와 불일치할 수 있습니다. 그런 경우 @SerialName 매핑을 추가해 주세요. 일치한다면 변경 불필요합니다.

적용 예시(서버가 snake_case일 때만 적용):

 package com.texthip.thip.data.model.rooms.response

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

 @Serializable
 data class RoomsCreateDailyGreetingResponse(
-    val attendanceCheckId: Long,
-    val roomId: Long,
-    val isFirstWrite: Boolean,
+    @SerialName("attendance_check_id") val attendanceCheckId: Long,
+    @SerialName("room_id")            val roomId: Long,
+    @SerialName("is_first_write")     val isFirstWrite: Boolean,
 )

추가로, 동일 도메인의 다른 모델(TodayCommentList 등)과 attendanceCheckId 타입(Int/Long) 정합성도 확인해 주세요. 불일치 자체가 문제는 아니지만 혼용 시 캐스팅 비용/혼란이 생길 수 있습니다.

app/src/main/java/com/texthip/thip/ui/common/cards/CardCommentGroup.kt (1)

48-57: 프리뷰용 이미지 URL 제공 권장(시각적 확인성 개선)

빈 문자열은 Coil 로더가 아무 것도 표시하지 않아 프리뷰 확인이 어렵습니다. 샘플 이미지를 넣어 UI를 즉시 검증 가능하게 해주세요.

     CardCommentGroup(
         data = TodayCommentList(
             attendanceCheckId = 1,
             creatorId = 1,
-            creatorProfileImageUrl = "",
+            creatorProfileImageUrl = "https://picsum.photos/48",
             creatorNickname = "user.01",
             todayComment = "이것은 그룹 채팅의 댓글입니다. 이곳에 댓글 내용을 작성할 수 있습니다. 여러 줄로 작성해도 됩니다.",
             postDate = "11시간 전",
             date = "2025-08-18",
             isWriter = false
         ),
         onMenuClick = {}
app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt (1)

256-264: getRoomsDailyGreeting: 반환 타입 명시 및 cursor 기본값 제안

가독성과 일관성을 위해 반환 타입을 명시하고, cursor 기본값을 null로 두는 것을 권장합니다(동일 파일 내 getMyRoomsByType와 패턴 통일).

-    suspend fun getRoomsDailyGreeting(
-        roomId: Int,
-        cursor: String?
-    ) = runCatching {
+    suspend fun getRoomsDailyGreeting(
+        roomId: Int,
+        cursor: String? = null
+    ): Result<com.texthip.thip.data.model.rooms.response.RoomsDailyGreetingResponse?> = runCatching {
         roomsService.getRoomsDailyGreeting(
             roomId = roomId,
             cursor = cursor
         ).handleBaseResponse().getOrThrow()
     }
app/src/main/java/com/texthip/thip/ui/common/header/ProfileBarWithDate.kt (2)

43-49: 빈/잘못된 URL 대비 및 이미지 크롭 제안

빈 문자열이 넘어오면 아무 것도 표시되지 않습니다. 빈 값은 플레이스홀더로 대체하고, 로드 시 크롭을 적용해 일관된 원형 아바타를 보장하세요.

다음 변경을 권장합니다:

-            AsyncImage(
-                model = profileImage,
-                contentDescription = "프로필 이미지",
-                modifier = Modifier
-                    .size(24.dp)
-                    .clip(CircleShape)
-            )
+            if (profileImage.isBlank()) {
+                Spacer(
+                    modifier = Modifier
+                        .size(24.dp)
+                        .clip(CircleShape)
+                        .background(colors.Grey01)
+                )
+            } else {
+                AsyncImage(
+                    model = profileImage,
+                    contentDescription = "프로필 이미지",
+                    contentScale = ContentScale.Crop,
+                    modifier = Modifier
+                        .size(24.dp)
+                        .clip(CircleShape)
+                )
+            }

추가 import:

import androidx.compose.foundation.background
import androidx.compose.ui.layout.ContentScale

82-82: 프리뷰 이미지 URL 개선(가시성 향상)

example.com은 이미지가 아니어서 실패합니다. 샘플 이미지로 교체해 미리보기에서 즉시 확인 가능하게 해주세요.

-            profileImage = "https://example.com",
+            profileImage = "https://picsum.photos/48",
app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsDailyGreetingResponse.kt (1)

12-22: 직렬화 키 네이밍 전략 확인 필요

kotlinx.serialization을 사용 중인데, 백엔드 JSON 키가 snake_case라면 @SerialName이 없으면 매핑 실패합니다. 프로젝트 전역에서 JsonNamingStrategy 또는 커스텀 키 변환을 쓰지 않는다면 각 필드에 @SerialName을 선언해야 합니다.

예시:

@Serializable
data class TodayCommentList(
    @SerialName("attendance_check_id") val attendanceCheckId: Int,
    @SerialName("creator_id") val creatorId: Int,
    @SerialName("creator_nickname") val creatorNickname: String,
    @SerialName("creator_profile_image_url") val creatorProfileImageUrl: String,
    @SerialName("today_comment") val todayComment: String,
    @SerialName("post_date") val postDate: String,
    @SerialName("date") val date: String,
    @SerialName("is_writer") val isWriter: Boolean,
)

Json 키 네이밍 정책을 사용 중인지 알려주시면 거기에 맞춰 일괄 반영 PR 초안을 드릴 수 있습니다.

app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomChatViewModel.kt (3)

34-42: SavedStateHandle 의존 확인

requireNotNull(savedStateHandle["roomId"])는 인자 누락 시 즉시 크래시합니다. 네비게이션 경로에 항상 roomId가 주입됨이 보장되는지 확인 부탁드립니다. 방어적으로 기본값을 두거나 명시적 예외 메시지를 추가하는 것도 방법입니다.

원한다면 다음처럼 타입 안정성과 메시지를 강화할 수 있습니다:

- private val roomId: Int = requireNotNull(savedStateHandle["roomId"])
+ private val roomId: Int = savedStateHandle.get<Int>("roomId")
+    ?: error("GroupRoomChatViewModel: roomId is missing in SavedStateHandle")

43-48: sealed interface에 대한 when 분기에서 else는 불필요

현재 이벤트가 LoadMore 하나뿐이라 else 분기는 죽은 코드입니다. 추후 이벤트 추가 전까진 제거를 권장합니다.

-    when (event) {
-        is GroupRoomChatEvent.LoadMore -> fetchDailyGreetings()
-        else -> Unit
-    }
+    when (event) {
+        GroupRoomChatEvent.LoadMore -> fetchDailyGreetings()
+    }

83-97: 작성 후 즉시 재조회 플로우는 적절

성공 시 전체 새로고침으로 목록 일관성을 보장하는 전략은 간단하고 안전합니다. 향후 낙관적 추가(append)로 전환할 경우, 실패 롤백 처리를 위한 로컬 아이템 키 정책이 필요합니다.

  • 멱등 요청 방지를 위해 빠르게 연속 클릭되는 전송 버튼을 잠깐 비활성화하는 UI 힌트를 추가할 수 있습니다(예: 텍스트 길이>0 && !uiState.isLoading).
app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomChatScreen.kt (3)

53-57: 수명 주기 인지형 수집으로 변경 권장

collectAsState() 대신 collectAsStateWithLifecycle()을 사용하면 화면 비가시 상태에서의 불필요한 수집을 방지할 수 있습니다.

-    val uiState by viewModel.uiState.collectAsState()
+    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

추가로 필요한 import:

import androidx.lifecycle.compose.collectAsStateWithLifecycle

164-171: 키 안정성과 날짜 구분 로직 적절

  • key = attendanceCheckId로 항목 재사용성 확보 OK.
  • reverseLayout에서 다음 아이템과의 날짜 비교로 분리선 노출하는 방식도 일관적입니다.

날짜가 동일한 아이템이 대량 추가될 때 헤더가 자주 깜빡이지 않도록 contentType을 지정하면 성능에 약간 도움이 될 수 있습니다.

itemsIndexed(
    uiState.greetings,
    key = { _, item -> item.attendanceCheckId },
    contentType = { _, item -> item.date }
) { index, message -> ... }

210-241: 자기 글 여부에 따른 메뉴 분기 명확

isWriter 기준으로 수정/삭제 vs 신고를 분기하는 UX가 자연스럽습니다. 추후 실제 액션 연동 시 ViewModel 이벤트로 위임하는 구조를 추천드립니다.

  • 삭제/신고는 네트워크 호출이므로 진행 상태 표시(버튼 비활성화/로딩) 및 실패 토스트/스낵바를 위한 UI 상태 필드 추가를 고려해 보세요.
📜 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 cb26a6f and 1cb7a57.

📒 Files selected for processing (10)
  • app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomsCreateDailyGreetingRequest.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsCreateDailyGreetingResponse.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsDailyGreetingResponse.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt (5 hunks)
  • app/src/main/java/com/texthip/thip/data/service/RoomsService.kt (3 hunks)
  • app/src/main/java/com/texthip/thip/ui/common/cards/CardCommentGroup.kt (3 hunks)
  • app/src/main/java/com/texthip/thip/ui/common/header/ProfileBarWithDate.kt (3 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/note/mock/ReplyData.kt (0 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomChatScreen.kt (6 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomChatViewModel.kt (3 hunks)
💤 Files with no reviewable changes (1)
  • app/src/main/java/com/texthip/thip/ui/group/note/mock/ReplyData.kt
🧰 Additional context used
🧬 Code Graph Analysis (1)
app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomChatScreen.kt (1)
app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomChatViewModel.kt (1)
  • onEvent (43-48)
🔇 Additional comments (18)
app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomsCreateDailyGreetingRequest.kt (2)

6-8: 요청 DTO 리네이밍 및 스키마 일관성 좋습니다

RoomsCreateDailyGreetingRequest(content)로 명확해졌고, 직렬화에도 문제 없어 보입니다.


6-8: 잔여 타입 참조 없음 확인

스크립트 결과 RoomsDailyGreetingRequest에 대한 레거시 참조가 발견되지 않았으며, 새 타입(RoomsCreateDailyGreetingRequest)이 서비스―레포지토리―뷰모델 전반에 걸쳐 올바르게 사용되고 있음을 확인했습니다.

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

5-10: POST 응답 DTO 추가 적절

필드 구성이 간결하고 필요한 데이터만 담고 있어 용도에 부합합니다.

app/src/main/java/com/texthip/thip/ui/common/cards/CardCommentGroup.kt (2)

20-22: TodayCommentList로의 전환 적절

UI 바인딩 객체를 API 응답 형태(TodayCommentList)에 맞춰 단순화한 점이 좋습니다. VM/레포지토리와의 데이터 흐름도 명확해졌습니다.


29-33: 필드 매핑 검토 요청

creatorProfileImageUrl, creatorNickname, postDate 매핑이 올바른지(특히 postDate 포맷: "n시간 전" vs 날짜 문자열) 실제 API 값과 일치하는지 한번만 더 확인해 주세요. UI 포맷팅 책임이 어디에 있는지(서버/VM/뷰)도 합의되면 유지보수에 좋습니다.

app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt (5)

10-10: 요청 DTO 리네이밍에 따른 import 교체 OK

서비스/레포지토리 간 의존성이 정리되었습니다.


56-63: 장르 기본값/매핑 로직 명확화 좋아요

genre ?: getDefaultGenre() 후 API 카테고리 매핑으로 책임이 분리되어 가독성이 좋아졌습니다.


66-69: 시그니처 포맷 정리 OK

cursor 기본값을 명시해 호출부 단순화한 점 일관성 있습니다.


272-275: POST 오늘의 한마디: 새 요청 DTO 적용 OK

요청 생성부가 간결하고 다른 POST들과 패턴도 일치합니다.


256-264: Service-Repository 시그니처 일치 확인
getRoomsDailyGreeting의 파라미터(roomId: Int, cursor: String?) 및 반환 타입(BaseResponse → RoomsDailyGreetingResponse) 모두 서비스(interface)와 레포지토리에서 일치하며, handleBaseResponse().getOrThrow() 호출 방식도 다른 메서드와 동일하게 일관적입니다. 추가 조치 불필요합니다.

app/src/main/java/com/texthip/thip/ui/common/header/ProfileBarWithDate.kt (1)

31-34: 호출부 호환성 확인 완료: ProfileBarWithDate의 profileImage가 모두 String으로 전달되고 있습니다.

  • PreviewProfileBarWithDate (ProfileBarWithDate.kt:81) → "https://example.com"
  • CardCommentGroup.kt:28 → data.creatorProfileImageUrl
  • 그 외 ProfileBarWithDate 호출부에 painterResource 기반 인자 전달 흔적 없음

해당 변경으로 인한 호환성 이슈는 없습니다. [resolve_review_comment】

app/src/main/java/com/texthip/thip/data/service/RoomsService.kt (2)

158-169: 일일 한마디 조회/작성 엔드포인트 시그니처 변경 LGTM

  • GET: cursor 기반 페이지네이션 쿼리 추가, 반환 타입을 새로운 RoomsDailyGreetingResponse로 지정한 점 일관적입니다.
  • POST: 요청/응답을 RoomsCreateDailyGreetingRequest/Response로 분리한 것도 의미가 분명해졌습니다.

레포지토리 계층의 메서드 시그니처와 호출부(ViewModel 포함)가 모두 본 변경사항과 일치하는지 한번 더 확인 부탁드립니다. 특히 GET의 cursor=null일 때 Retrofit이 쿼리를 생략하도록 원하는 동작인지도 체크해 주세요.


7-7: 검증 완료: RoomsDailyGreetingRequest 불필요 참조 제거 확인됨
RoomsDailyGreetingRequest에 대한 참조는 더 이상 존재하지 않으며, 모든 호출이 RoomsCreateDailyGreetingRequest로 대체된 것을 확인했습니다.

승인합니다.

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

7-10: 페이징 메타(isLast/nextCursor) 추가 설계 적절

리스트+커서+종료 플래그 구성으로 ViewModel에서의 페이지 종료 판단이 단순해져 좋습니다. 서버가 nextCursor=null과 isLast=true를 일관되게 내려준다는 가정 하에 동작이 안정적입니다.

서버 응답에서 isLast=false이면서 nextCursor=null인 케이스가 존재하지 않는지 확인 부탁드립니다. 존재한다면 클라이언트 쪽에서 nextCursor null 시 추가 로딩을 중단하도록 보정이 필요합니다.

app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomChatViewModel.kt (2)

15-21: UI 상태 전환(리스트/로딩/오류/페이징) 구조화 좋습니다

UI가 단일 StateFlow로 구독 가능해졌고, 리스트/로딩/오류가 한곳에서 관리되어 화면단 단순화에 도움이 됩니다.


50-53: 중복 요청 방지 가드 적절

isLoading / isLoadingMore / isLastPage 조합으로 중복 호출을 방지하는 접근은 타당합니다.

app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomChatScreen.kt (2)

151-163: 상단 페이지 로딩 인디케이터 배치 적절

reverseLayout 환경에서 상단(이전 페이지) 로딩 시 리스트 상단에 로딩 스피너를 노출하는 구조가 맞습니다. UI/UX 측면에서 충분합니다.


259-274: 프리뷰 데이터 타입 적용 적절

프리뷰가 실제 모델(TodayCommentList)과 동일한 필드로 구성되어 있어 디자인 확인에 유용합니다.

Comment on lines +81 to +91
val isScrolledToTop by remember {
derivedStateOf {
lazyListState.firstVisibleItemIndex == 0 && lazyListState.firstVisibleItemScrollOffset == 0
}
}

LaunchedEffect(isScrolledToTop) {
if (isScrolledToTop && !uiState.isLoadingMore && !uiState.isLastPage) {
onEvent(GroupRoomChatEvent.LoadMore)
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

reverseLayout에서 상단 도달 감지가 반대로 동작하여 과도한 프리패칭 유발

firstVisibleItemIndex == 0 && firstVisibleItemScrollOffset == 0는 reverseLayout=true일 때 "맨 아래(최신)"를 의미합니다. 초기 진입 시 곧바로 LoadMore가 트리거되고, 로딩 완료 후에도 여전히 조건이 true라 연속 페이지 로딩을 끝까지 수행할 수 있습니다.

아래처럼 layoutInfo를 사용해 "화면 상단(=가장 큰 인덱스)이 보이는가"로 판단하도록 수정해 주세요.

-    val isScrolledToTop by remember {
-        derivedStateOf {
-            lazyListState.firstVisibleItemIndex == 0 && lazyListState.firstVisibleItemScrollOffset == 0
-        }
-    }
-
-    LaunchedEffect(isScrolledToTop) {
-        if (isScrolledToTop && !uiState.isLoadingMore && !uiState.isLastPage) {
-            onEvent(GroupRoomChatEvent.LoadMore)
-        }
-    }
+    val isAtTop by remember {
+        derivedStateOf {
+            val layoutInfo = lazyListState.layoutInfo
+            val totalItems = layoutInfo.totalItemsCount
+            val lastVisibleIndex = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1
+            totalItems > 0 && lastVisibleIndex >= totalItems - 1
+        }
+    }
+
+    LaunchedEffect(isAtTop, uiState.isLoadingMore, uiState.isLastPage) {
+        if (isAtTop && !uiState.isLoadingMore && !uiState.isLastPage) {
+            onEvent(GroupRoomChatEvent.LoadMore)
+        }
+    }

추가로, 필요하다면 snapshotFlow로 스크롤 이벤트를 수집하여 쓰로틀/디바운스도 적용할 수 있습니다.

📝 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
val isScrolledToTop by remember {
derivedStateOf {
lazyListState.firstVisibleItemIndex == 0 && lazyListState.firstVisibleItemScrollOffset == 0
}
}
LaunchedEffect(isScrolledToTop) {
if (isScrolledToTop && !uiState.isLoadingMore && !uiState.isLastPage) {
onEvent(GroupRoomChatEvent.LoadMore)
}
}
// Replace the old scroll‐to‐top check with a reverseLayout-aware one
val isAtTop by remember {
derivedStateOf {
val layoutInfo = lazyListState.layoutInfo
val totalItems = layoutInfo.totalItemsCount
val lastVisibleIndex = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1
// In reverseLayout, top == largest index
totalItems > 0 && lastVisibleIndex >= totalItems - 1
}
}
// Include loading flags in the effect’s key to avoid repeated triggers
LaunchedEffect(isAtTop, uiState.isLoadingMore, uiState.isLastPage) {
if (isAtTop && !uiState.isLoadingMore && !uiState.isLastPage) {
onEvent(GroupRoomChatEvent.LoadMore)
}
}
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomChatScreen.kt
around lines 81-91, the current top-detection uses firstVisibleItemIndex==0
which is inverted when reverseLayout=true and causes spurious LoadMore triggers;
replace that logic with layoutInfo-based detection that considers the largest
visible item index: get lazyListState.layoutInfo, check that
layoutInfo.visibleItemsInfo.maxByOrNull { it.index }?.index ==
layoutInfo.totalItemsCount - 1 (i.e. the highest index is visible), and
optionally verify its offset to ensure it's scrolled to the edge before firing
LoadMore; if you need to reduce noisy events, collect scroll changes via
snapshotFlow { lazyListState.layoutInfo } and apply debounce/throttle before
calling onEvent(GroupRoomChatEvent.LoadMore).

Comment on lines 65 to 79
).onSuccess { response ->
response?.let { data ->
_uiState.update {
it.copy(
isLoading = false,
isLoadingMore = false,
greetings = if (isRefresh) data.todayCommentList else it.greetings + data.todayCommentList,
isLastPage = data.isLast
)
}
nextCursor = data.nextCursor
}
}.onFailure { throwable ->
_uiState.update { it.copy(isLoading = false, isLoadingMore = false, error = throwable.message) }
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

null 응답 시 로딩 상태가 해제되지 않는 버그

onSuccess { response -> response?.let { ... } } 구조에서는 response가 null일 때 isLoading / isLoadingMore가 그대로 유지되어 화면이 영구 로딩 상태에 빠질 수 있습니다.

아래처럼 null 응답을 명시 처리해 로딩을 해제하고, 필요 시 페이지 종료로 간주하도록 수정해 주세요.

-            ).onSuccess { response ->
-                response?.let { data ->
-                    _uiState.update {
-                        it.copy(
-                            isLoading = false,
-                            isLoadingMore = false,
-                            greetings = if (isRefresh) data.todayCommentList else it.greetings + data.todayCommentList,
-                            isLastPage = data.isLast
-                        )
-                    }
-                    nextCursor = data.nextCursor
-                }
-            }.onFailure { throwable ->
+            ).onSuccess { response ->
+                if (response == null) {
+                    _uiState.update { it.copy(isLoading = false, isLoadingMore = false, isLastPage = true) }
+                    nextCursor = null
+                    return@onSuccess
+                }
+                _uiState.update {
+                    it.copy(
+                        isLoading = false,
+                        isLoadingMore = false,
+                        greetings = if (isRefresh) response.todayCommentList else it.greetings + response.todayCommentList,
+                        isLastPage = response.isLast
+                    )
+                }
+                nextCursor = response.nextCursor
+            }.onFailure { throwable ->
                 _uiState.update { it.copy(isLoading = false, isLoadingMore = false, error = throwable.message) }
             }

추가로, 새 요청 시작 시 error를 null로 리셋하는 것도 UX에 도움이 됩니다.

📝 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
).onSuccess { response ->
response?.let { data ->
_uiState.update {
it.copy(
isLoading = false,
isLoadingMore = false,
greetings = if (isRefresh) data.todayCommentList else it.greetings + data.todayCommentList,
isLastPage = data.isLast
)
}
nextCursor = data.nextCursor
}
}.onFailure { throwable ->
_uiState.update { it.copy(isLoading = false, isLoadingMore = false, error = throwable.message) }
}
).onSuccess { response ->
if (response == null) {
_uiState.update { it.copy(isLoading = false, isLoadingMore = false, isLastPage = true) }
nextCursor = null
return@onSuccess
}
_uiState.update {
it.copy(
isLoading = false,
isLoadingMore = false,
greetings = if (isRefresh) response.todayCommentList else it.greetings + response.todayCommentList,
isLastPage = response.isLast
)
}
nextCursor = response.nextCursor
}.onFailure { throwable ->
_uiState.update { it.copy(isLoading = false, isLoadingMore = false, error = throwable.message) }
}
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomChatViewModel.kt
around lines 65-79, the onSuccess block uses response?.let { ... } which leaves
isLoading/isLoadingMore true when response is null; update the onSuccess handler
to explicitly handle a null response by calling _uiState.update to set isLoading
= false, isLoadingMore = false (and optionally isLastPage = true) and avoid
touching greetings/nextCursor, and only set nextCursor when non-null;
additionally, ensure any new request path resets error to null at start of the
fetch so the UI clears previous errors.

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: 2

♻️ Duplicate comments (2)
app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomChatScreen.kt (1)

114-124: reverseLayout에서 최상단 감지 로직이 반대로 동작합니다 (이전 리뷰와 동일 이슈)

firstVisibleItemIndex/Offset 기준은 reverseLayout=true에서 “맨 아래(최신)”를 가리켜, 과도한 LoadMore 트리거가 발생할 수 있습니다. layoutInfo 기반으로 “화면 상단(가장 큰 인덱스) 노출”을 감지하도록 바꿔주세요.

-    val isScrolledToTop by remember {
-        derivedStateOf {
-            lazyListState.firstVisibleItemIndex == 0 && lazyListState.firstVisibleItemScrollOffset == 0
-        }
-    }
-
-    LaunchedEffect(isScrolledToTop) {
-        if (isScrolledToTop && !uiState.isLoadingMore && !uiState.isLastPage) {
-            onEvent(GroupRoomChatEvent.LoadMore)
-        }
-    }
+    val isAtTop by remember {
+        derivedStateOf {
+            val info = lazyListState.layoutInfo
+            val total = info.totalItemsCount
+            val lastVisible = info.visibleItemsInfo.lastOrNull()?.index ?: -1
+            total > 0 && lastVisible >= total - 1
+        }
+    }
+
+    LaunchedEffect(isAtTop, uiState.isLoadingMore, uiState.isLastPage) {
+        if (isAtTop && !uiState.isLoadingMore && !uiState.isLastPage) {
+            onEvent(GroupRoomChatEvent.LoadMore)
+        }
+    }

스크롤 이벤트 노이즈가 크면 snapshotFlow로 감지 후 throttle/debounce 적용도 고려해 주세요.

app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomChatViewModel.kt (1)

83-95: null 응답 시 로딩 해제 누락 (이전 리뷰와 동일 이슈)

response?.let { … } 구조는 null일 때 isLoading/More가 false로 내려가지 않아 영구 로딩 상태가 될 수 있습니다. null을 명시 처리해 주세요.

             ).onSuccess { response ->
-                response?.let { data ->
-                    _uiState.update {
-                        it.copy(
-                            isLoading = false,
-                            isLoadingMore = false,
-                            greetings = if (isRefresh) data.todayCommentList else it.greetings + data.todayCommentList,
-                            isLastPage = data.isLast
-                        )
-                    }
-                    nextCursor = data.nextCursor
-                }
+                if (response == null) {
+                    _uiState.update { it.copy(isLoading = false, isLoadingMore = false, isLastPage = true) }
+                    nextCursor = null
+                } else {
+                    _uiState.update {
+                        it.copy(
+                            isLoading = false,
+                            isLoadingMore = false,
+                            greetings = if (isRefresh) response.todayCommentList else it.greetings + response.todayCommentList,
+                            isLastPage = response.isLast
+                        )
+                    }
+                    nextCursor = response.nextCursor
+                }
🧹 Nitpick comments (6)
app/src/main/res/values/strings.xml (1)

186-186: 한글 띄어쓰기 수정 제안: ‘다섯번’ → ‘다섯 번’

사용자 노출 문자열의 맞춤법을 바로잡아 주세요.

-    <string name="group_room_chat_max">오늘의 한마디는 하루에 다섯번까지 작성할 수 있어요</string>
+    <string name="group_room_chat_max">오늘의 한마디는 하루에 다섯 번까지 작성할 수 있어요</string>
app/src/main/java/com/texthip/thip/ui/common/modal/ToastWithDate.kt (1)

27-29: 공용 컴포저블에 도메인 특화 기본 문구 결합 최소화 권장

ToastWithDate는 범용 토스트 컴포넌트로 보입니다. 기본 메시지를 특정 화면(모임방 ‘오늘의 한마디’ 제한) 문구로 고정하면 결합도가 올라갑니다. 기본 메시지는 null로 두고, 호출부에서 명시 전달하는 패턴을 유지하는 것이 확장성에 유리합니다.

app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomChatScreen.kt (2)

62-63: 수명주기 인지 수집으로 변경 권장: collectAsState → collectAsStateWithLifecycle

구성 변경/백스택 복귀 시 안전한 수집을 위해 Lifecycle-aware 수집으로 바꿔 주세요.

-    val uiState by viewModel.uiState.collectAsState()
+    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

아래 import 추가가 필요합니다(파일 상단):

import androidx.lifecycle.compose.collectAsStateWithLifecycle

184-195: 로딩 인디케이터 배치 위치가 reverseLayout과 상충합니다

reverseLayout=true에서는 DSL 상 앞쪽에 선언한 item이 화면의 “아래쪽”에 배치됩니다. 현재 isLoadingMore 인디케이터가 리스트 앞에 정의되어 실제로는 “아래(최신)”에 표시될 가능성이 큽니다. 상단(과거 데이터 방향)에 노출하려면 itemsIndexed 뒤쪽으로 옮기거나 stickyHeader를 사용하세요.

-                    if (uiState.isLoadingMore) {
-                        item {
-                            Box(
-                                modifier = Modifier
-                                    .fillMaxWidth()
-                                    .padding(vertical = 16.dp),
-                                contentAlignment = Alignment.Center
-                            ) {
-                                CircularProgressIndicator(modifier = Modifier.size(24.dp))
-                            }
-                        }
-                    }

아래처럼 itemsIndexed 블록 “뒤쪽”에 추가하는 걸 권장합니다(참고 코드):

// itemsIndexed(...) { ... }

if (uiState.isLoadingMore) {
    item {
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .padding(vertical = 16.dp),
            contentAlignment = Alignment.Center
        ) {
            CircularProgressIndicator(modifier = Modifier.size(24.dp))
        }
    }
}
app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomChatViewModel.kt (2)

62-79: 요청 시작 시 이전 에러를 초기화하고 로딩 플래그만 갱신하도록 보완

새 요청 시 error를 null로 리셋하면 UX가 개선되고, 리프레시 시 greetings를 비우는 현재 전략과도 일관됩니다.

             if (isRefresh) {
                 _uiState.update {
                     it.copy(
                         isLoading = true,
                         greetings = emptyList(),
-                        isLastPage = false
+                        isLastPage = false,
+                        error = null
                     )
                 }
                 nextCursor = null
             } else {
-                _uiState.update { it.copy(isLoadingMore = true) }
+                _uiState.update { it.copy(isLoadingMore = true, error = null) }
             }

121-123: 에러 토스트 타입 분기 사용 고려

모든 실패를 “일일 제한”으로 표시하면 사용자가 원인을 알기 어렵습니다. 서버 에러/네트워크 에러 등은 ShowErrorToast로 분기해 message를 UI로 전달하는 방식을 고려해 주세요. (UI는 이미 ToastType 외에 에러 토스트 경로를 받을 준비가 되어 있습니다.)

-            }.onFailure { throwable ->
-                _eventFlow.emit(GroupRoomChatEvent.ShowToast(ToastType.DAILY_GREETING_LIMIT))
+            }.onFailure { throwable ->
+                // 상태코드에 따라 제한 초과 vs 일반 오류 분기 (의사 코드)
+                // if (throwable is HttpException && throwable.code() == 429) {
+                //     _eventFlow.emit(GroupRoomChatEvent.ShowToast(ToastType.DAILY_GREETING_LIMIT))
+                // } else {
+                //     _eventFlow.emit(GroupRoomChatEvent.ShowErrorToast(throwable.message ?: "알 수 없는 오류"))
+                // }
+                _eventFlow.emit(GroupRoomChatEvent.ShowToast(ToastType.DAILY_GREETING_LIMIT))
📜 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 1cb7a57 and 1e8fcca.

📒 Files selected for processing (5)
  • app/src/main/java/com/texthip/thip/ui/common/modal/ToastWithDate.kt (3 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomChatScreen.kt (6 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomChatViewModel.kt (2 hunks)
  • app/src/main/res/values/strings.xml (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomChatScreen.kt (2)
app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomChatViewModel.kt (1)
  • onEvent (55-60)
app/src/main/java/com/texthip/thip/ui/common/modal/ToastWithDate.kt (1)
  • ToastWithDate (24-61)
🔇 Additional comments (1)
app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt (1)

241-244: Toast 색상 파라미터 적용 LGTM

새로운 ToastWithDate(color) API를 적절히 사용했습니다. 의도한 경고성 메시지 톤(빨간색)과 잘 맞습니다.

Comment on lines +27 to 30
message: String = stringResource(R.string.group_room_chat_max),
color: Color = colors.White,
date: String? = null
) {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Composable 기본 파라미터에서 stringResource 사용은 컴파일 에러를 유발합니다

@composable 컨텍스트가 아닌 기본 인자(default argument)에서 stringResource를 호출하면 Compose 컴파일러가 허용하지 않습니다. 메시지는 null 허용으로 바꾸고, 함수 본문에서 stringResource로 대체값을 적용하세요.

-@Composable
-fun ToastWithDate(
-    modifier: Modifier = Modifier,
-    message: String = stringResource(R.string.group_room_chat_max),
-    color: Color = colors.White,
-    date: String? = null
-) {
+@Composable
+fun ToastWithDate(
+    modifier: Modifier = Modifier,
+    message: String? = null,
+    color: Color = colors.White,
+    date: String? = null
+) {
+    val resolvedMessage = message ?: stringResource(R.string.group_room_chat_max)
     ...
-            Text(
-                text = message,
+            Text(
+                text = resolvedMessage,
                 color = color,
                 style = typography.view_m500_s12_h20
             )

Also applies to: 47-51

Comment on lines +265 to +274
ToastType.DAILY_GREETING_LIMIT -> {
ToastWithDate(color = colors.Red)
}

ToastType.FIRST_WRITE -> {
ToastWithDate()
}

null -> {}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

FIRST_WRITE 토스트에서 잘못된 기본 문구가 노출됩니다

ToastWithDate()를 그대로 호출하면 “오늘의 한마디 일일 제한” 문구(기본값)가 표시됩니다. FIRST_WRITE일 때는 성공 메시지로 교체해야 합니다.

-                ToastType.DAILY_GREETING_LIMIT -> {
-                    ToastWithDate(color = colors.Red)
-                }
+                ToastType.DAILY_GREETING_LIMIT -> {
+                    ToastWithDate(
+                        message = stringResource(R.string.group_room_chat_max),
+                        color = colors.Red
+                    )
+                }
 
-                ToastType.FIRST_WRITE -> {
-                    ToastWithDate()
-                }
+                ToastType.FIRST_WRITE -> {
+                    ToastWithDate(
+                        message = stringResource(R.string.complete_comment)
+                    )
+                }

필요 시 FIRST_WRITE 전용 문자열을 추가하는 것도 고려해 주세요(예: “첫 한마디가 등록되었어요!”).

📝 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
ToastType.DAILY_GREETING_LIMIT -> {
ToastWithDate(color = colors.Red)
}
ToastType.FIRST_WRITE -> {
ToastWithDate()
}
null -> {}
}
ToastType.DAILY_GREETING_LIMIT -> {
ToastWithDate(
message = stringResource(R.string.group_room_chat_max),
color = colors.Red
)
}
ToastType.FIRST_WRITE -> {
ToastWithDate(
message = stringResource(R.string.complete_comment)
)
}
null -> {}
}

…into api/THIP-TextHip#97-feed_comments

# Conflicts:
#	app/src/main/java/com/texthip/thip/ui/feed/screen/FeedCommentScreen.kt
@Nico1eKim Nico1eKim merged commit b3ea1fb 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

🧹 Nitpick comments (14)
app/src/main/res/values/strings.xml (1)

186-186: 맞춤법(띄어쓰기) 및 문구 다듬기 제안

사용자 노출 문구의 가독성과 일관성을 위해 띄어쓰기와 표현을 조금 다듬는 것을 권합니다.

  • ‘다섯번’ → ‘다섯 번’(맞춤법)
  • 안내문 스타일 통일을 위해 문장부호 마침표 추가
  • 횟수 표기를 ‘5회’로 간결화

아래처럼 수정 제안드립니다.

-    <string name="group_room_chat_max">오늘의 한마디는 하루에 다섯번까지 작성할 수 있어요</string>
+    <string name="group_room_chat_max">오늘의 한마디는 하루에 최대 5회까지 작성할 수 있어요.</string>
app/src/main/java/com/texthip/thip/ui/feed/screen/FeedCommentScreen.kt (13)

72-74: collectAsState 대신 collectAsStateWithLifecycle 사용 권장 (수명 주기 안전성)

Compose 화면이 백그라운드에 있을 때도 Flow 수집이 이어질 수 있습니다. lifecycle-aware 수집으로 변경해 리소스 누수/불필요 작업을 방지하세요.

적용 예:

-    val feedDetailUiState by feedDetailViewModel.uiState.collectAsState()
-    val commentsUiState by commentsViewModel.uiState.collectAsState()
+    val feedDetailUiState by feedDetailViewModel.uiState.collectAsStateWithLifecycle()
+    val commentsUiState by commentsViewModel.uiState.collectAsStateWithLifecycle()

추가 import:

import androidx.lifecycle.compose.collectAsStateWithLifecycle

69-71: Preview에서 hiltViewModel() 호출로 미리보기 크래시 가능

이 컴포저블은 기본 인자로 hiltViewModel()을 사용합니다. Preview에서는 Hilt 컨텍스트가 없어서 미리보기가 깨집니다. Preview에서만 페이크 VM을 주입하거나, 실제 화면은 hiltViewModel()을 사용하고 Preview는 UI-state를 직접 주입하는 분리된 내부 컴포저블(예: FeedCommentScreenContent)을 두는 패턴을 권장합니다.

원하시면 Preview용 페이크 ViewModel과 Content 컴포저블 골격을 생성해드릴게요.


76-78: 하드코딩된 postType 문자열 "FEED" → 타입 안전한 상수/enum으로 교체 권장

문자열 타이포로 런타임 버그가 발생하기 쉽습니다. PostType 같은 enum/@StringDef/상수 객체를 사용해 타입 안전성을 높이세요.

적용 예:

commentsViewModel.initialize(postId = feedId.toLong(), postType = PostType.FEED)
// 또는 Constants.POST_TYPE_FEED

95-109: 에러 메시지 노출 방식 보완 및 !! 제거 권장

Double-bang(!!)은 가드가 있더라도 유지보수 시 리스크가 됩니다. 또한 서버/개발용 원문 메시지를 그대로 노출하기보다는 사용자 친화적 문구 + 재시도 액션을 제공하는 것이 UX에 좋습니다.

적용 예:

-                    text = feedDetailUiState.error!!,
+                    text = feedDetailUiState.error.orEmpty(),

그리고 “재시도” 버튼(예: TextButton)으로 feedDetailViewModel.loadFeedDetail(feedId) 재호출을 제공해 주세요.


118-134: early return 패턴 주의: 로컬 상태 재생성 가능성

feedDetail이 null → 유효로 토글되는 경우, 이후에 선언된 remember 상태들이 새로 생성됩니다. 현재 플로우 특성상 문제될 가능성은 낮으나, 상태 유지가 중요하다면 분기 렌더링(when)으로 감싸거나 rememberSaveable를 함께 사용하는 쪽을 권장합니다.


127-134: 입력/선택 상태는 rememberSaveable로 보존 권장 (회전/프로세스 중단 대비)

댓글 입력/답글 대상/선택된 항목은 회전 등 구성 변경 시 소실됩니다. rememberSaveable로 간단히 개선 가능합니다.

적용 예:

-import androidx.compose.runtime.remember
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable

-    var commentInput by remember { mutableStateOf("") }
-    var replyingToCommentId by remember { mutableStateOf<Int?>(null) }
-    var replyingToNickname by remember { mutableStateOf<String?>(null) }
-    var selectedCommentId by remember { mutableStateOf<Int?>(null) }
+    var commentInput by rememberSaveable { mutableStateOf("") }
+    var replyingToCommentId by rememberSaveable { mutableStateOf<Int?>(null) }
+    var replyingToNickname by rememberSaveable { mutableStateOf<String?>(null) }
+    var selectedCommentId by rememberSaveable { mutableStateOf<Int?>(null) }

237-302: 댓글 영역: 에러 상태 분기 누락 — 빈 목록과 구분 필요

현재 when 블록은 isLoading → 빈 목록 → 목록 렌더 순서이며, 에러 상태가 발생해도 빈 목록 UI로 보일 수 있습니다. 에러 분기를 추가하고 재시도 액션을 제공해 주세요.

적용 예:

-            when {
-                commentsUiState.isLoading -> {
+            when {
+                commentsUiState.isLoading -> {
                     item { /* ...로딩 UI... */ }
-                }
+                }
+                commentsUiState.error != null -> {
+                    item {
+                        Column(
+                            modifier = Modifier
+                                .fillMaxWidth()
+                                .padding(vertical = 40.dp),
+                            horizontalAlignment = Alignment.CenterHorizontally,
+                            verticalArrangement = Arrangement.spacedBy(8.dp)
+                        ) {
+                            Text(
+                                text = stringResource(R.string.error_loading_comments),
+                                style = typography.smalltitle_sb600_s18_h24,
+                                color = colors.White
+                            )
+                            Text(
+                                text = commentsUiState.error.orEmpty(),
+                                style = typography.copy_r400_s14,
+                                color = colors.Grey
+                            )
+                            // 재시도 버튼 등 추가
+                        }
+                    }
+                }
                 // 댓글 없음
                 commentsUiState.comments.isEmpty() -> {
                     item { /* ...빈 상태 UI... */ }
                 }
                 else -> {
                     items(
                         items = commentsUiState.comments,
                         key = { comment -> comment.commentId ?: comment.hashCode() }
                     ) { commentItem ->
                         CommentSection(/* ... */)
                     }
                 }
             }

256-271: 빈 상태 UI의 고정 높이(400.dp) 제거 권장 — 다양한 화면/키보드 영역 대응

고정 높이는 작은/큰 화면, 키보드 표시 상태에서 어색할 수 있습니다. LazyColumn의 viewport를 채우도록 fillParentMaxHeight() 사용을 권장합니다.

적용 예:

-                        Column(
-                            modifier = Modifier
-                                .fillMaxWidth()
-                                .height(400.dp),
+                        Column(
+                            modifier = Modifier
+                                .fillMaxWidth()
+                                .fillParentMaxHeight(),
                             verticalArrangement = Arrangement.Center,
                             horizontalAlignment = Alignment.CenterHorizontally
                         ) {

276-279: Lazy keys 안정성 확인 요청 (임시 키 hashCode 사용)

commentId가 null인 항목에 hashCode()를 키로 쓰면 동일 데이터라도 재생성 시 키가 달라져 스크롤 점프/애니메이션/상태 보존 문제가 발생할 수 있습니다. 임시 로컬 ID(예: UUID)나 서버 생성 전 로컬 키를 별도로 유지하는 걸 권장합니다.

예: comment.localId(로컬 생성 시 부여) 또는 "temp-${createdAt}-${authorId}" 등 충돌 가능성이 낮은 조합.


309-323: 댓글 전송 중 중복 전송 방지 처리 추천

isSending 플래그(또는 UI state)로 전송 중 추가 탭을 무시하거나 버튼을 비활성화하는 처리가 보이지 않습니다. 네트워크 지연 시 중복 댓글 생성 위험이 있습니다.

적용 예:

  • CommentsUiState에 isSubmitting 플래그 추가
  • onSendClick에서 isSubmitting이 true면 return
  • 완료/실패 시 isSubmitting false로 복귀

417-421: 이미지 뷰어에 3장 제한(take(3)) 필요성 확인

썸네일은 3장까지만 노출해도, 뷰어는 전체를 볼 수 있도록 하는 게 자연스러운 경우가 많습니다. 의도된 UX가 아니라면 이미지 전체 목록을 전달해 주세요.

-    imageUrls = images.take(3),
+    imageUrls = images,

381-385: BottomSheet 연동은 적절합니다 — TODO 항목 처리 제안

BottomSheet 구성은 깔끔합니다. 다만 ‘신고/삭제’에 TODO가 남아있습니다. PR 설명에 “해결하지 못한 과제: 없음”으로 되어 있어 일관성 측면에서 TODO 제거 또는 추적 이슈 링크화를 권장합니다. 원하시면 API 연동 코드 뼈대를 추가해드릴게요.


426-434: Preview 크래시 방지: 미리보기에서는 페이크 VM 주입

현재 Preview는 기본 인자를 사용해 hiltViewModel()을 호출하게 됩니다. Preview에서만 목 ViewModel을 주입하도록 변경해 주세요.

예시:

@Preview
@Composable
private fun FeedCommentScreenPrev() {
    ThipTheme {
        FeedCommentScreen(
            feedId = 1,
            feedDetailViewModel = FakeFeedDetailViewModel(), // Preview 전용
            commentsViewModel = FakeCommentsViewModel()
        )
    }
}

원하시면 Fake*ViewModel 두 개도 함께 생성해 드릴 수 있어요.

📜 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 1e8fcca and 082d953.

📒 Files selected for processing (2)
  • app/src/main/java/com/texthip/thip/ui/feed/screen/FeedCommentScreen.kt (9 hunks)
  • app/src/main/res/values/strings.xml (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
app/src/main/java/com/texthip/thip/ui/feed/screen/FeedCommentScreen.kt (1)
app/src/main/java/com/texthip/thip/ui/group/note/component/CommentSection.kt (1)
  • CommentSection (19-81)
🔇 Additional comments (3)
app/src/main/res/values/strings.xml (1)

186-186: group_room_chat_max 중복 정의 없음 확인

리포지토리 전체 검색 결과, 해당 문자열 리소스는 app/src/main/res/values/strings.xml 한 곳에서만 정의되어 있으며 다른 values 디렉터리에도 중복이 없습니다.
따라서 중복 충돌 이슈는 발생하지 않습니다.

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

81-92: 로딩 상태 분기 처리 깔끔합니다

초기 로딩 시 전역 로딩 인디케이터로 빠르게 반환하는 구조가 명확하고, 불필요한 레이아웃 계산을 줄일 수 있습니다.


147-149: 바깥 터치로 포커스/선택 해제 UX 좋습니다

detectTapGestures로 키보드/선택 해제 처리 깔끔합니다. 스크롤 제스처와의 충돌도 없어 보입니다.

Comment on lines +281 to +299
commentItem = commentItem,
actionMode = CommentActionMode.POPUP,
selectedCommentId = selectedCommentId,
onEvent = commentsViewModel::onEvent,
onReplyClick = { commentId, nickname ->
replyingToCommentId = commentId
replyingToNickname = nickname
selectedCommentId = null
},
onCommentLongPress = { comment ->
selectedCommentId = comment.commentId
},
onReplyLongPress = { reply ->
selectedCommentId = reply.commentId
},
onDismissPopup = {
selectedCommentId = null
}
}


if (index == CommentList.lastIndex) {
Spacer(modifier = Modifier.height(40.dp))
}
)
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

참고: CommentSection 내부 레이아웃 잠재 이슈 (fillMaxHeight 사용)

관련 파일(app/src/main/java/com/texthip/thip/ui/group/note/component/CommentSection.kt, Lines 18-80)에서 Column(modifier = Modifier.fillMaxHeight())가 있어 LazyColumn 아이템이 화면 높이를 점유할 가능성이 있습니다. 각 댓글이 뷰포트를 가득 차지하면 스크롤/배치가 비정상적일 수 있으니 wrapContentHeight()/fillMaxWidth()로 조정하세요.

적용 예(외부 파일 수정):

// CommentSection.kt 내
Column(
    modifier = Modifier
        .fillMaxWidth()
        .padding(horizontal = 20.dp, vertical = 12.dp),
    verticalArrangement = Arrangement.spacedBy(12.dp),
    horizontalAlignment = Alignment.Start
) { /* ... */ }
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/ui/feed/screen/FeedCommentScreen.kt around
lines 281-299, the CommentSection composable used by this screen likely contains
a Column with Modifier.fillMaxHeight() (see
app/src/main/java/com/texthip/thip/ui/group/note/component/CommentSection.kt
lines ~18-80), which can make each LazyColumn item occupy the full viewport and
break scrolling/placement; update that Column modifier to use
Modifier.fillMaxWidth() (or wrapContentHeight()) instead of fillMaxHeight(), add
appropriate horizontal padding and verticalArrangement (e.g., spacedBy) so items
size to content and align correctly, and remove any forcing height constraints
so replies and comments render and scroll normally.

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-296)]

1 participant