Conversation
Walkthrough댓글/룸 관련 데이터 모델 확장 및 API/리포지토 시그니처 변경, 댓글 UI에 액션 모드(POPUP/BOTTOM_SHEET)·팝업 컴포넌트 추가, 피드 댓글 화면을 ViewModel 분리(MVVM)로 리팩터링하고 댓글 생성/조회 로직을 ViewModel에서 응답 기반으로 즉시 갱신하도록 변경. Changes
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Possibly related issues
Possibly related PRs
Suggested reviewers
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 unit tests
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
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: 정렬 기준 확인 필요: 팝업 정렬/오프셋이 전체 윈도우 기준일 수 있음
Popup의alignment = 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: 수정 가능” 주석이 남아 있습니다. 로직상
commentIdnull이면 액션 비활성화가 맞다면, 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.
📒 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가 좋습니다. 스크롤 제스처와의 간섭도 적을 것으로 보입니다.
| Popup( | ||
| alignment = Alignment.BottomEnd, | ||
| offset = IntOffset(x = 0, yOffsetPx), | ||
| onDismissRequest = onDismissRequest, | ||
| properties = PopupProperties(focusable = true) | ||
| ) { |
There was a problem hiding this comment.
컴파일 오류: 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.
| 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.
There was a problem hiding this comment.
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.
📒 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)과 동일한 필드로 구성되어 있어 디자인 확인에 유용합니다.
| val isScrolledToTop by remember { | ||
| derivedStateOf { | ||
| lazyListState.firstVisibleItemIndex == 0 && lazyListState.firstVisibleItemScrollOffset == 0 | ||
| } | ||
| } | ||
|
|
||
| LaunchedEffect(isScrolledToTop) { | ||
| if (isScrolledToTop && !uiState.isLoadingMore && !uiState.isLastPage) { | ||
| onEvent(GroupRoomChatEvent.LoadMore) | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| 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).
| ).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) } | ||
| } |
There was a problem hiding this comment.
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.
| ).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.
There was a problem hiding this comment.
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.
📒 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를 적절히 사용했습니다. 의도한 경고성 메시지 톤(빨간색)과 잘 맞습니다.
| message: String = stringResource(R.string.group_room_chat_max), | ||
| color: Color = colors.White, | ||
| date: String? = null | ||
| ) { |
There was a problem hiding this comment.
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
| ToastType.DAILY_GREETING_LIMIT -> { | ||
| ToastWithDate(color = colors.Red) | ||
| } | ||
|
|
||
| ToastType.FIRST_WRITE -> { | ||
| ToastWithDate() | ||
| } | ||
|
|
||
| null -> {} | ||
| } |
There was a problem hiding this comment.
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.
| 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
There was a problem hiding this comment.
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.
📒 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로 키보드/선택 해제 처리 깔끔합니다. 스크롤 제스처와의 충돌도 없어 보입니다.
| 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)) | ||
| } | ||
| ) |
There was a problem hiding this comment.
🛠️ 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.
➕ 이슈 링크
🔎 작업 내용
📸 스크린샷
피드 댓글 조회, 생성, 좋아요, 삭제
2025-08-18.4.59.26.mov
오늘의 한마디 조회
KakaoTalk_Video_2025-08-18-20-21-41.mp4
😢 해결하지 못한 과제
📢 리뷰어들에게
Summary by CodeRabbit
New Features
Refactor
Style / UI