Skip to content

[API] 댓글 관련 api 연결 완료#84

Merged
rbqks529 merged 19 commits intoTHIP-TextHip:developfrom
Nico1eKim:api/#81-comments
Aug 15, 2025
Merged

[API] 댓글 관련 api 연결 완료#84
rbqks529 merged 19 commits intoTHIP-TextHip:developfrom
Nico1eKim:api/#81-comments

Conversation

@Nico1eKim
Copy link
Member

@Nico1eKim Nico1eKim commented Aug 13, 2025

➕ 이슈 링크


🔎 작업 내용

  • 댓글 조회 api 연결했습니다
  • 댓글 좋아요 api 연결했습니다
  • 댓글/답글 작성 api 연결했습니다

📸 스크린샷

KakaoTalk_Video_2025-08-14-03-06-18.mp4

😢 해결하지 못한 과제

  • 댓글 삭제는 서버에서 api 수정해주면 작업 하겠습니다

  • 현재 댓글을 작성하고 나서 화면이 전체 재랜더링 되는데 서버에 댓글 정보 반환해달라고 요청했습니다. 해당 부분 수정되면 하나면 재랜더링 되도록 수정하겠습니다


📢 리뷰어들에게

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

Summary by CodeRabbit

• 신기능
• 그룹 노트에 댓글 시스템 추가: 댓글/대댓글 조회, 페이지네이션, 좋아요, 작성이 가능한 바텀시트 UI 도입.

• 개선사항
• 피드 프로필 이미지를 URL 기반 비동기 로딩으로 전환.
• 그룹 룸 헤더와 멤버 목록에 동적 색상 적용(카테고리/별칭 색상 표시).
• 키보드 등장 시 화면 자동 리사이즈 및 IME 겹침 최소화를 위한 고급 패딩 처리.
• 내비게이션에서 동적 roomId 사용으로 이동 정확도 향상.
• 피드 댓글 화면에서 댓글/대댓글 표시 일시 비활성화.

…into api/THIP-TextHip#81-comments

# Conflicts:
#	app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt
@coderabbitai
Copy link

coderabbitai bot commented Aug 13, 2025

Walkthrough

댓글 기능을 위한 전체 계층을 추가·변경: Retrofit 서비스/요청·응답 모델/리포지토리/DI 바인딩/뷰모델/컴포저블 갱신 및 바텀시트 연동. 방/사용자 색상 필드 추가와 전달, 네비게이션의 roomId 동적화, IME 패딩 유틸과 매니페스트 입력 모드, 이미지 로더 교체 포함.

Changes

Cohort / File(s) Summary
Comments API: service, models, repository, DI
.../data/service/CommentsService.kt, .../data/repository/CommentsRepository.kt, .../data/di/ServiceModule.kt, .../data/model/comments/request/*, .../data/model/comments/response/*
신규 댓글 API 인터페이스 추가, 요청/응답 모델 정의, 리포지토리 구현, DI에 CommentsService 바인딩 추가.
Comments state & UI (Group Note)
.../ui/group/note/viewmodel/CommentsViewModel.kt, .../ui/group/note/component/CommentBottomSheet.kt, .../ui/group/note/component/CommentItem.kt, .../ui/group/note/component/ReplyItem.kt, .../ui/group/note/component/CommentSection.kt, .../ui/group/note/screen/GroupNoteScreen.kt
댓글 뷰모델/UiState/이벤트 추가, 바텀시트와 아이템 컴포저블을 새 모델(CommentList/ReplyList) 기반으로 변경, 좋아요/페이징/작성 이벤트 연동, GroupNoteScreen에서 뷰모델 상태 바인딩.
Feed: 댓글 아이템 렌더 비활성화
.../ui/feed/screen/FeedCommentScreen.kt
CommentItem/ReplyItem 렌더 섹션 주석 처리.
Room 색상 확장 및 전달
.../data/model/rooms/response/RoomsPlayingResponse.kt, .../data/model/rooms/response/RoomsUsersResponse.kt, .../ui/group/room/screen/GroupRoomScreen.kt, .../ui/group/room/component/GroupRoomHeader.kt, .../ui/group/room/component/GroupRoomMatesList.kt, .../ui/group/room/screen/GroupRoomMatesScreen.kt
categoryColor/aliasColor 필드 추가 및 헤더/리스트에 헥스 색상 적용, 프리뷰 갱신.
Navigation
.../ui/navigator/navigations/GroupNavigation.kt
roomId 하드코딩(1) 제거, route.roomId 사용.
Utils & system behavior
.../utils/color/HexToColor.kt, .../utils/rooms/AdvancedImePadding.kt, app/src/main/AndroidManifest.xml
hexToColor 구현을 toColorInt로 변경, 고급 IME 패딩 모디파이어 추가, MainActivity에 windowSoftInputMode=adjustResize 적용.
Profile image rendering
.../ui/common/header/ProfileBarFeed.kt
프로필 이미지 파라미터를 URL String?로 변경, Coil AsyncImage 사용.
Minor cleanup
.../ui/common/header/ProfileBar.kt
주석 제거(동작 변화 없음).

Sequence Diagram(s)

sequenceDiagram
  participant UI as GroupNoteScreen
  participant Sheet as CommentBottomSheet
  participant VM as CommentsViewModel
  participant Repo as CommentsRepository
  participant Svc as CommentsService

  UI->>VM: initialize(postId, postType)
  VM->>Repo: getComments(postId, postType, cursor)
  Repo->>Svc: GET /comments/{postId}?postType&cursor
  Svc-->>Repo: BaseResponse<CommentsResponse>
  Repo-->>VM: Result<CommentsResponse>
  VM-->>Sheet: uiState(comments, isLoading=false)

  Sheet->>VM: onEvent(LoadMoreComments)
  VM->>Repo: getComments(..., cursor=nextCursor)
  Repo->>Svc: GET /comments/{postId}?cursor=...
  Svc-->>Repo: BaseResponse<CommentsResponse>
  Repo-->>VM: Result<CommentsResponse>
  VM-->>Sheet: uiState(append, isLast)

  Sheet->>VM: onEvent(LikeComment(commentId))
  VM->>Repo: likeComment(commentId, type=toggle)
  Repo->>Svc: POST /comments/{id}/likes { type }
  Svc-->>Repo: BaseResponse<CommentsLikesResponse>
  Repo-->>VM: Result<...>
  VM-->>Sheet: uiState(update isLike)

  Sheet->>VM: onEvent(CreateComment(content, parentId))
  VM->>Repo: createComment(postId, content, isReply, parentId)
  Repo->>Svc: POST /comments/{postId} { content, ... }
  Svc-->>Repo: BaseResponse<CommentsCreateResponse>
  Repo-->>VM: Result<...>
  VM->>Repo: getComments(refresh)
  Repo->>Svc: GET /comments/{postId}
  Svc-->>Repo: BaseResponse<CommentsResponse>
  Repo-->>VM: Result<CommentsResponse>
  VM-->>Sheet: uiState(refresh list)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Assessment against linked issues

Objective Addressed Explanation
댓글 관련 API를 앱에 연결(서비스/모델/리포지토리/뷰모델/화면 연동) [#81]

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Room 카테고리 색상 필드 추가 및 UI 반영 (app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsPlayingResponse.kt, app/src/main/java/com/texthip/thip/ui/group/room/*) 댓글 API 연결과 직접적 연관 없음.
사용자 aliasColor 처리 및 적용 (app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsUsersResponse.kt, .../GroupRoomMatesList.kt) 댓글 기능 범위를 벗어난 룸/멤버 색상 표시 변경.
네비게이션에서 roomId 동적화 (app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt) 댓글 API 연동과 무관한 라우팅 변경.
ProfileBarFeed에서 Coil AsyncImage로 전환 및 파라미터 타입 변경 (app/src/main/java/com/texthip/thip/ui/common/header/ProfileBarFeed.kt) 댓글 API 목표와 직접 관련되지 않음.
IME 패딩 유틸 추가 및 매니페스트 windowSoftInputMode=adjustResize (app/src/main/java/com/texthip/thip/utils/rooms/AdvancedImePadding.kt, app/src/main/AndroidManifest.xml) 입력창 레이아웃 개선으로, 댓글 API 연동 범위를 넘어섬.

Possibly related issues

Possibly related PRs

Suggested reviewers

  • rbqks529
  • JJUYAAA

Poem

깡총, 깡총, 코드 숲에선
댓글 길 따라 선선한 바람~
서비스-리포-뷰모델 줄 맞춰,
바텀시트엔 말풍선이 퐁!
색깔도 반짝, 방도 화사해—
오늘도 토끼는 PR 위에 춤춰요 🥕✨

✨ 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: 15

🔭 Outside diff range comments (1)
app/src/main/java/com/texthip/thip/ui/common/header/ProfileBarFeed.kt (1)

41-57: 프로필 이미지 로딩 안정화: 공백 처리, 크로핑, 크로스페이드 추가

  • 빈 문자열("")이 들어와도 네트워크 요청이 발생하지 않도록 isNullOrBlank 체크로 변경
  • 원형 클리핑 시 테두리 잔여 공간이 보이지 않게 ContentScale.Crop 적용
  • 로딩 UX와 깜빡임 완화를 위해 Coil crossfade 적용

다음 변경을 제안합니다:

-        Row {
-            if (profileImage != null) {
-                AsyncImage(
-                    model = profileImage,
-                    contentDescription = "프로필 이미지",
-                    modifier = Modifier
-                        .size(24.dp)
-                        .clip(CircleShape)
-                )
+        Row {
+            if (!profileImage.isNullOrBlank()) {
+                AsyncImage(
+                    model = ImageRequest.Builder(LocalContext.current)
+                        .data(profileImage)
+                        .crossfade(true)
+                        .build(),
+                    contentDescription = null,
+                    modifier = Modifier
+                        .size(24.dp)
+                        .clip(CircleShape),
+                    contentScale = ContentScale.Crop
+                )
             } else {
                 Box(
                     modifier = Modifier
                         .size(24.dp)
                         .clip(CircleShape)
                         .background(colors.Grey)
                 )
             }

필요 import:

+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import coil.request.ImageRequest
🧹 Nitpick comments (25)
app/src/main/AndroidManifest.xml (1)

20-20: adjustResize와 Compose/insets 동작 상호작용 확인 필요

edge-to-edge(예: WindowCompat.setDecorFitsSystemWindows(false)) 또는 투명 status/navigation bar를 쓰는 경우 adjustResize가 기대대로 동작하지 않거나 imePadding 처리와 중복될 수 있습니다. 댓글 작성 화면처럼 입력창이 하단에 있는 화면에서 키보드 오버랩이 없는지 실제 기기(삼성/픽셀), 제스처/버튼 내비, 다크모드 조합으로 스모크 테스트 권장합니다. 필요 시 테마 레벨로 이동해 일관성도 확보하세요.

app/src/main/java/com/texthip/thip/data/model/comments/request/CommentsLikesRequest.kt (1)

1-8: Boolean 필드명 'type'은 의미 모호 — 가독성 향상과 확장성 확보 제안

불리언에 'type'은 의미가 불분명합니다. 로컬 이름을 의미 있게 개선하고, 서버 키는 @SerialName으로 유지하는 것을 권장합니다. 장기적으로는 enum(Like, Cancel 등)도 고려해 주세요.

 package com.texthip.thip.data.model.comments.request
 
 import kotlinx.serialization.Serializable
+import kotlinx.serialization.SerialName
 
 @Serializable
 data class CommentsLikesRequest(
-    val type: Boolean
+    @SerialName("type")
+    val like: Boolean
 )

선택: 서버가 Boolean 대신 문자열/정수로 액션을 받도록 바뀐다면 enum + @SerialName으로의 전환이 자연스럽습니다.

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

5-9: 의미 문서화: isLiked의 "처리 후 최종 상태" 명시 필요

API 소비자가 혼동하지 않도록 isLiked가 “토글 처리 이후의 최종 상태”인지 “요청값”인지 KDoc으로 명시해 주세요.

예시:

 @Serializable
+/**
+ * 댓글 좋아요 처리 결과
+ * @param commentId 대상 댓글 ID
+ * @param isLiked 서버 처리 이후의 최종 좋아요 상태(true=좋아요 상태)
+ */
 data class CommentsLikesResponse(
app/src/main/java/com/texthip/thip/ui/common/header/ProfileBarFeed.kt (1)

45-45: 접근성(a11y): contentDescription 재검토

동일 행에 닉네임 텍스트가 있어 스크린리더가 중복 낭독할 가능성이 있습니다. 장식용 이미지라면 contentDescription = null을 권장합니다. 반대로 이미지 자체에 의미가 있다면 닉네임을 포함해 구체화해 주세요(예: "$nickname 프로필 이미지").

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

6-8: 향후 서버 응답 확장 대비

PR 설명대로 서버가 생성된 댓글 전체 데이터를 반환하도록 변경되면, 이 응답 모델을 해당 도메인 모델(예: CommentItem/CommentList)로 교체하거나 포함하도록 확장하면 화면 전체 리렌더 없이 신규 아이템만 갱신할 수 있습니다. 준비되면 데이터 모델/매퍼 변경 작업 도와드릴게요.

app/src/main/java/com/texthip/thip/utils/color/HexToColor.kt (1)

6-13: 입력 포맷 허용 범위 확장: '#' 누락/0x 프리픽스 대응

백엔드/디자인 시스템에서 '#RRGGBB' 대신 'RRGGBB' 또는 '0xRRGGBB' 형태로 내려오는 경우를 방어하면 런타임 예외를 줄일 수 있습니다.

아래처럼 정규화한 뒤 파싱하도록 제안합니다:

 fun hexToColor(hex: String): Color {
     return try {
-        Color(hex.toColorInt())
+        val normalized = hex.trim().let {
+            var h = it
+            if (h.startsWith("0x", true)) h = h.substring(2)
+            if (!h.startsWith("#")) h = "#$h"
+            h
+        }
+        Color(normalized.toColorInt())
     } catch (e: IllegalArgumentException) {
         //잘못된 형식이면 기본 색
         Color.White
     }
 }

추가로 3자리 축약형(#RGB)이나 알파 포함(#AARRGGBB) 지원 범위를 문서화해두면 호출부에서의 혼선을 줄일 수 있습니다.

app/src/main/java/com/texthip/thip/data/model/comments/request/CommentsCreateRequest.kt (1)

10-10: postType 문자열 오타 방지

postType은 허용 값이 정해진 도메인 값으로 보입니다. 문자열 직접 사용은 오타 리스크가 있어, enum(@serializable + @SerialName) 또는 상수 객체로 중앙집중 관리하는 것을 권장합니다.

예시(값은 실제 서버 스펙에 맞게 교체 필요):

@Serializable
enum class PostType {
    @SerialName("GROUP_NOTE") GROUP_NOTE,
    @SerialName("ROOM_FEED") ROOM_FEED,
}

또는

object PostTypes {
    const val GROUP_NOTE = "GROUP_NOTE"
    const val ROOM_FEED = "ROOM_FEED"
}

원하시면 enum/매퍼 적용 PR 보조해 드리겠습니다.

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

35-35: hex 색상 파싱 실패 대비 및 리컴포지션 비용 최적화 제안

  • 현재 매 렌더링마다 hexToColor(member.aliasColor)가 호출됩니다. remember로 메모이즈하면 불필요한 변환을 줄일 수 있습니다.
  • 백엔드에서 #RRGGBB 형식이 아닌 값(예: 빈 문자열, RRGGBB, #RGB)이 오면 hexToColor에서 Color.White로 떨어지는데, 배경과의 대비가 낮아질 수 있습니다. 기본값을 테마 컬러로 넘기거나 hexToColor에서 입력 정규화(자동 # 부착 등)를 권장합니다.

적용 예시(이 파일 내 변경):

+import androidx.compose.runtime.remember
@@
-                bottomTextColor = hexToColor(member.aliasColor),
+                bottomTextColor = remember(member.aliasColor) { hexToColor(member.aliasColor) },

HexToColor.kt(참고용, 별도 파일)도 아래처럼 방어적으로 개선 가능합니다:

// app/src/main/java/com/texthip/thip/utils/color/HexToColor.kt
fun hexToColor(hex: String): Color {
    val raw = hex.trim()
    if (raw.isEmpty()) return Color.White
    val normalized = if (raw.startsWith("#")) raw else "#$raw"
    return try {
        Color(normalized.toColorInt())
    } catch (_: IllegalArgumentException) {
        Color.White
    }
}

61-70: 프리뷰 다양화로 시각 검증성 향상

두 사용자 모두 같은 aliasColor를 사용하고 있어 프리뷰 상 변화가 드러나지 않습니다. 서로 다른 색상을 부여해 대비, 가독성, 디자인 의도를 시각적으로 검증하는 것을 권장합니다.

예시:

-                        aliasColor= "#A0F8E8",
+                        aliasColor= "#FFD47E",
app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt (1)

41-44: DI 제공 함수 네이밍 일관성

다른 함수들은 provideXxxService를 사용하고 있습니다. providesCommentsServiceprovideCommentsService로 맞추면 일관성이 좋아집니다.

-    fun providesCommentsService(retrofit: Retrofit): CommentsService =
+    fun provideCommentsService(retrofit: Retrofit): CommentsService =
         retrofit.create(CommentsService::class.java)
app/src/main/java/com/texthip/thip/ui/group/room/component/GroupRoomHeader.kt (1)

40-41: categoryColor를 String으로 노출하는 API 설계 — Color 타입으로의 전환 고려

현재는 서버 응답 연동을 위해 String을 받지만, UI 컴포저블의 관점에선 Color로 받는 편이 테스트/재사용성에 유리합니다. 선택지:

  • 최소 변경: 현 상태 유지 + 호출부에서만 String을 전달
  • 권장(선택): GroupRoomHeader가 Color를 받고, 호출부에서 hexToColor 변환 수행

가능한 변경 예시(해당 범위 내 차이만 표기):

-    categoryColor: String = "#A0F8E8",
+    categoryColor: Color = Color(0xFFA0F8E8),

그리고 Line 176의 사용부도 다음과 같이 단순화됩니다:

-                    color = hexToColor(categoryColor)
+                    color = categoryColor

참고: 이 경우 GroupRoomScreen 등 호출부에서 hexToColor(roomDetails.categoryColor)로 변환해 전달해야 합니다.

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

70-74: 문자열 결합 대신 다국어 포맷 문자열 사용 권장

"@작성자 " 형태를 문자열 리소스에 플레이스홀더로 정의해 결합하세요. 현재처럼 여러 stringResource를 더하는 방식은 지역화/공백 처리에서 리스크가 있습니다.

예시(해당 범위 내 차이만 표기 — 새로운 문자열 리소스 추가 필요):

-                                append(
-                                    stringResource(R.string.annotation) + data.parentCommentCreatorNickname + stringResource(
-                                        R.string.space_bar
-                                    )
-                                )
+                                append(stringResource(R.string.reply_annotation_format, data.parentCommentCreatorNickname))

리소스 예:

<!-- values/strings.xml -->
<string name="reply_annotation_format">@%1$s </string>

필요하다면 strings.xml 추가/변경 패치도 도와드리겠습니다.


90-93: 좋아요 영역 접근성 보완: 역할/설명 추가

Column에 clickable만 적용하면 접근성 서비스에서 버튼으로 인식하지 못할 수 있습니다. Role.Button과 contentDescription을 부여하세요.

예시(해당 범위 내 차이만 표기 — semantics import 필요):

-                Column(
-                    modifier = Modifier.clickable(onClick = onLikeClick),
+                Column(
+                    modifier = Modifier
+                        .semantics { contentDescription = stringResource(R.string.like) }
+                        .clickable(role = Role.Button, onClick = onLikeClick),

필요 import:

import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.Role
app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt (1)

487-495: 전송 문자열 트리밍 및 공백 방지

UI에서 isNotBlank 체크만 하고 있어 공백이 섞인 입력이 그대로 전달됩니다. 전송 전 trim을 적용해 의도치 않은 공백 전송을 막는 편이 안전합니다.

아래와 같이 개선을 제안합니다.

-            onSendReply = { text, parentId, _ ->
-                if (text.isNotBlank()) {
+            onSendReply = { text, parentId, _ ->
+                val trimmed = text.trim()
+                if (trimmed.isNotEmpty()) {
                     commentsViewModel.onEvent(
                         CommentsEvent.CreateComment(
-                            content = text,
+                            content = trimmed,
                             parentId = parentId
                         )
                     )
                 }
             }
app/src/main/java/com/texthip/thip/data/service/CommentsService.kt (2)

23-28: 파라미터 명명 일관성: request로 통일

likeComment의 @Body 파라미터명이 response로 되어 있어 createComment와 일관성이 떨어집니다. request로 맞춰 주세요.

-    suspend fun likeComment(
-        @Path("commentId") commentId: Long,
-        @Body response: CommentsLikesRequest
-    ): BaseResponse<CommentsLikesResponse>
+    suspend fun likeComment(
+        @Path("commentId") commentId: Long,
+        @Body request: CommentsLikesRequest
+    ): BaseResponse<CommentsLikesResponse>

15-22: postType의 문자열 하드코딩을 타입 세이프하게 전환 제안

"RECORD"/"VOTE" 문자열 오탈자로 인한 런타임 오류를 방지하기 위해 enum 또는 @stringdef 기반의 타입을 사용하면 안전합니다. Retrofit 직렬화는 @SerializedName 등으로 매핑 가능합니다.

원하시면 PostType enum과 어댑터(또는 직렬화 어노테이션)까지 포함한 변경안을 드리겠습니다.

Also applies to: 29-34

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

26-34: Service 파라미터명 변경에 맞춰 인자명 수정 필요

Service의 likeComment @Body 파라미터명을 request로 정리했다면, 여기서도 동일하게 맞춰 주세요.

-        commentsService.likeComment(
-            commentId = commentId,
-            response = CommentsLikesRequest(type)
-        ).handleBaseResponse().getOrThrow()
+        commentsService.likeComment(
+            commentId = commentId,
+            request = CommentsLikesRequest(type)
+        ).handleBaseResponse().getOrThrow()

추가로, 가독성을 위해 Repository 메서드 시그니처의 type: Boolean을 isLike: Boolean으로 변경하는 것도 권장합니다.

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

20-23: 사용되지 않는 onSendReply 파라미터 제거 제안

CommentSection 내부에서 onSendReply를 사용하지 않습니다. API 표면을 줄여 혼동을 방지하는 편이 좋습니다. 미사용이라면 제거를 권장합니다.

-    onSendReply: (String, Int?, String?) -> Unit = { _, _, _ -> },
     onReplyClick: (commentId: Int, nickname: String) -> Unit,
     onEvent: (CommentsEvent) -> Unit = { _ -> }

Preview에서도 해당 인자를 제거합니다.

-            CommentSection(
-                commentItem =
+            CommentSection(
+                commentItem =
                 ...
-                onSendReply = { _, _, _ -> },
                 onReplyClick = { commentId, nickname ->
                     // Handle reply click
                 }
             )

Also applies to: 79-83

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

98-111: Preview 더미 데이터의 가독성 개선 소소한 제안

aliasName= 등 일부 할당에 공백이 빠져 있어(코드 스타일) 일관성을 위해 정렬/공백을 맞추면 가독성이 향상됩니다. 기능에는 영향 없습니다.

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

60-78: 댓글 작성 시 로딩 상태 표시 추가 권장

댓글 작성 중 사용자가 중복 작성을 시도하거나 작성 진행 상황을 알 수 없는 문제가 있습니다. 작성 중 로딩 상태를 추가하여 UX를 개선하는 것이 좋겠습니다.

UiState에 isCreatingComment 필드를 추가하고:

 data class CommentsUiState(
     val isLoading: Boolean = false,
     val isLoadingMore: Boolean = false,
+    val isCreatingComment: Boolean = false,
     val error: String? = null,
     val isLast: Boolean = false,
     val comments: List<CommentList> = emptyList()
 )

createComment 메서드를 다음과 같이 수정:

 private fun createComment(content: String, parentId: Int?) {
     if (content.isBlank()) return
+    if (_uiState.value.isCreatingComment) return

     viewModelScope.launch {
         val isReply = parentId != null
+        _uiState.update { it.copy(isCreatingComment = true) }

         commentsRepository.createComment(
             postId = currentPostId,
             content = content,
             isReplyRequest = isReply,
             parentId = parentId,
             postType = currentPostType
         ).onSuccess {
+            _uiState.update { it.copy(isCreatingComment = false) }
             fetchComments(isRefresh = true)
         }.onFailure { throwable ->
-            _uiState.update { it.copy(error = "댓글 작성 실패: ${throwable.message}") }
+            _uiState.update { it.copy(isCreatingComment = false, error = "댓글 작성 실패: ${throwable.message}") }
         }
     }
 }

80-105: 좋아요 처리 시 에러 메시지 표시 필요

좋아요 실패 시 UI를 원상복구하지만 사용자에게 실패 이유를 알려주지 않습니다. 에러 메시지를 표시하면 더 나은 UX를 제공할 수 있습니다.

 viewModelScope.launch {
     commentsRepository.likeComment(commentId.toLong(), !currentIsLiked)
-        .onFailure {
+        .onFailure { throwable ->
             _uiState.update {
                 val originalComments = it.comments.toMutableList()
                 originalComments[commentIndex] = comment
-                it.copy(comments = originalComments)
+                it.copy(
+                    comments = originalComments,
+                    error = "좋아요 처리 실패: ${throwable.message}"
+                )
             }
         }
 }

107-140: 대댓글 좋아요 처리에도 에러 메시지 표시 필요

toggleCommentLike와 동일하게 대댓글 좋아요 실패 시에도 사용자에게 에러를 알려주는 것이 일관성 있습니다.

 viewModelScope.launch {
     commentsRepository.likeComment(replyId.toLong(), !currentIsLiked)
-        .onFailure {
+        .onFailure { throwable ->
             _uiState.update {
                 val originalComments = it.comments.toMutableList()
                 originalComments[parentCommentIndex] = parentComment
-                it.copy(comments = originalComments)
+                it.copy(
+                    comments = originalComments,
+                    error = "좋아요 처리 실패: ${throwable.message}"
+                )
             }
         }
 }
app/src/main/java/com/texthip/thip/ui/group/note/component/CommentBottomSheet.kt (3)

132-145: 무한 스크롤 트리거 조건 최적화 필요

현재 마지막 아이템이 보일 때 추가 로드를 트리거하는데, 사용자 경험을 개선하기 위해 마지막에서 2-3개 아이템 전에 미리 로드를 시작하는 것이 좋습니다.

 val isScrolledToEnd by remember {
     derivedStateOf {
         val layoutInfo = lazyListState.layoutInfo
         if (layoutInfo.totalItemsCount == 0) return@derivedStateOf false
         val lastVisibleItemIndex = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
-        lastVisibleItemIndex >= layoutInfo.totalItemsCount - 1
+        // 마지막에서 3개 아이템 전에 미리 로드 시작
+        lastVisibleItemIndex >= layoutInfo.totalItemsCount - 3
     }
 }

147-172: LazyColumn 성능 최적화 가능

댓글이 많을 경우 성능 최적화를 위해 contentType 파라미터를 추가하는 것이 좋습니다.

 LazyColumn(state = lazyListState) {
     items(
         items = commentList,
-        key = { it.commentId }
+        key = { it.commentId },
+        contentType = { "comment" }
     ) { comment ->
         CommentSection(
             commentItem = comment,
             onReplyClick = onReplyClick,
             onEvent = onEvent
         )
     }

     if (isLoadingMore) {
-        item {
+        item(
+            key = "loading",
+            contentType = "loading"
+        ) {
             Column(

204-229: Preview 함수에서 onEvent 처리 누락

Preview에서 onEvent = {}로 빈 람다를 전달하고 있어 실제 이벤트 처리를 테스트할 수 없습니다. 디버깅을 위해 로그를 추가하면 도움이 됩니다.

-                onEvent = {},
+                onEvent = { event ->
+                    println("Event triggered: $event")
+                },
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 667bca2 and e8cc4e4.

📒 Files selected for processing (27)
  • app/src/main/AndroidManifest.xml (1 hunks)
  • app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt (2 hunks)
  • app/src/main/java/com/texthip/thip/data/model/comments/request/CommentsCreateRequest.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/model/comments/request/CommentsLikesRequest.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/model/comments/response/CommentsCreateResponse.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/model/comments/response/CommentsLikesResponse.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/model/comments/response/CommentsResponse.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsPlayingResponse.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsUsersResponse.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/repository/CommentsRepository.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/service/CommentsService.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/ui/common/header/ProfileBar.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/ui/common/header/ProfileBarFeed.kt (2 hunks)
  • app/src/main/java/com/texthip/thip/ui/feed/screen/FeedCommentScreen.kt (2 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/note/component/CommentBottomSheet.kt (4 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/note/component/CommentItem.kt (3 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/note/component/CommentSection.kt (3 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/screen/GroupNoteScreen.kt (5 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/CommentsViewModel.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/room/component/GroupRoomHeader.kt (3 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/room/component/GroupRoomMatesList.kt (3 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomMatesScreen.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomScreen.kt (2 hunks)
  • app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt (5 hunks)
  • app/src/main/java/com/texthip/thip/utils/color/HexToColor.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/utils/rooms/AdvancedImePadding.kt (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (6)
app/src/main/java/com/texthip/thip/ui/group/room/component/GroupRoomHeader.kt (1)
app/src/main/java/com/texthip/thip/utils/color/HexToColor.kt (1)
  • hexToColor (6-13)
app/src/main/java/com/texthip/thip/ui/group/note/component/ReplyItem.kt (1)
app/src/main/java/com/texthip/thip/utils/color/HexToColor.kt (1)
  • hexToColor (6-13)
app/src/main/java/com/texthip/thip/ui/group/room/component/GroupRoomMatesList.kt (1)
app/src/main/java/com/texthip/thip/utils/color/HexToColor.kt (1)
  • hexToColor (6-13)
app/src/main/java/com/texthip/thip/ui/group/note/component/CommentItem.kt (2)
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/group/note/component/CommentSection.kt (2)
app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/CommentsViewModel.kt (1)
  • onEvent (48-58)
app/src/main/java/com/texthip/thip/ui/group/note/component/ReplyItem.kt (1)
  • ReplyItem (28-109)
app/src/main/java/com/texthip/thip/ui/group/note/component/CommentBottomSheet.kt (3)
app/src/main/java/com/texthip/thip/ui/common/bottomsheet/CustomBottomSheet.kt (1)
  • CustomBottomSheet (44-129)
app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/CommentsViewModel.kt (1)
  • onEvent (48-58)
app/src/main/java/com/texthip/thip/ui/group/note/component/CommentSection.kt (1)
  • CommentSection (17-55)
🔇 Additional comments (25)
app/src/main/java/com/texthip/thip/ui/common/header/ProfileBar.kt (1)

35-35: 의미적 변화 없음(LGTM)

기능적 변화가 없는 주석 제거 수준의 변경으로 보입니다. 기본값(colors.NeonGreen) 유지 문제 없습니다.

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

28-34: Public API 시그니처 변경: profileImage: String? 호출부 모두 확인 완료

rg 검색 결과, ProfileBarFeed( 호출부는 아래 세 곳으로 모두 파악되며 문자열 기반으로 이미 업데이트되어 누락된 부분이 없습니다.

  • app/src/main/java/com/texthip/thip/ui/group/note/component/ReplyItem.kt
  • app/src/main/java/com/texthip/thip/ui/group/note/component/CommentItem.kt
  • app/src/main/java/com/texthip/thip/ui/common/header/ProfileBarFeed.kt (Preview)

따라서 Painter? → String? 변경에 따른 추가 수정 사항은 없습니다.

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

28-41: ReplyList.commentId 의미가 모호합니다 — 식별자/관계 필드 확인 필요

ReplyListcommentId가 “대댓글 자체의 식별자(replyId)”인지, “부모 댓글의 식별자(parentCommentId)”인지 모호합니다. 삭제/신고/좋아요 등 액션에서 혼선이 생길 수 있습니다.

권장:

  • 서버 스키마 확인 후
    • 대댓글 고유 식별자라면 replyId로 명시적 네이밍
    • 부모 댓글 참조라면 parentCommentId 필드 별도 추가
  • 기존 commentId 유지가 필요하다면 KDoc/주석으로 의미를 명확히 기록

예(서버가 replyId를 내려주는 경우):

 @Serializable
 data class ReplyList(
-    val commentId: Int,
+    val replyId: Int,
     val parentCommentCreatorNickname: String,
     val creatorId: Int,
@@
 )
app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomMatesScreen.kt (1)

107-116: 프리뷰 데이터 갱신 LGTM

aliasColor 프리뷰 반영으로 실제 UI 색상 바인딩을 확인할 수 있어 유용합니다. 본문 GroupRoomMatesList에서도 정상 소비되고 있어 일관성 있습니다.

app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt (5)

208-219: roomId를 라우트 파라미터로 전달하도록 변경 — 적절합니다

GroupRoomScreen에 roomId를 동적으로 전달하고, 이후 하위 네비게이션에서도 동일 roomId를 캡처해 사용하는 흐름이 명확합니다.


227-235: Mates 화면으로의 네비게이션에 roomId 전달 — 일관성 좋습니다

GroupRoomMatesScreen에도 동일한 roomId를 전달하도록 정리되어 전체 내비게이션 컨텍스트가 일관됩니다.


250-277: 노트 화면에 roomId 전달 — ViewModel 스코프와 함께 올바른 사용

hiltViewModel(backStackEntry)와 함께 roomId를 전달하여 화면/상태 스코프가 안정적으로 유지됩니다.


285-299: 노트 생성 화면에 roomId 전달 — 후속 결과 전달(savedStateHandle)과 잘 결합

생성 후 탭 인덱스 결과 반영 로직과 충돌 없이 동작할 것으로 보입니다.


306-319: 투표 생성 화면에 roomId 전달 — 라우트 파라미터화 완료

투표 생성 흐름에도 동일하게 적용되어 내비게이션 파라미터 관리가 통일되었습니다.

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

29-29: 서버 hex 문자열 → Color 변환 도입 — 유효하지 않은 값에 대한 폴백까지 OK

hexToColor를 통해 서버에서 내려오는 hex 문자열을 안전하게 Color로 변환하고(유효하지 않으면 White), UI 안정성을 높였습니다.


174-177: 텍스트 색상에 서버 제공 카테고리 컬러 적용 — 일관성 있는 렌더링

카테고리 텍스트에만 색을 적용해 가독성을 해치지 않으면서 강조 효과를 주고 있습니다. 폴백 Color.White 처리 덕분에 서버 값 이상에도 안전합니다.

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

165-167: Header에 categoryColor 전달 — 모델-UI 매핑이 깔끔합니다

RoomsPlayingResponse.categoryColor → GroupRoomHeader.categoryColor 전달로 데이터-프레젠테이션 간 결합이 명확해졌습니다. hexToColor 쪽 폴백이 있어 안전합니다.


286-286: 프리뷰에 categoryColor 예시 추가 — 디자인 확인에 유용합니다

샘플 hex 값을 포함하여 프리뷰에서 컬러 렌더링을 빠르게 검증할 수 있습니다.

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

31-34: 상태 외부화: onReplyClick·onLikeClick 콜백과 isLike 바인딩 — 방향성 좋습니다

내부 상태를 제거하고 data.isLike/likeCount로 단방향 바인딩하며, 액션을 콜백으로 위임한 설계는 테스트/상태관리 측면에서 바람직합니다.


49-54: ProfileBarFeed 바인딩 정합성 확인

이미지 URL, 닉네임, 칭호/색상, 날짜가 새 데이터 모델(ReplyList)에 맞게 정상 매핑되었습니다. aliasColor는 hexToColor 변환으로 안전 처리됩니다.


95-98: isLike에 따른 아이콘 토글 렌더링 — 의도대로 동작

filled/outlined 리소스 전환으로 상태 표현이 명확합니다. 서버 반영 후 UI 업데이트만 보장되면 UX도 자연스럽게 이어집니다.


122-134: 프리뷰 데이터가 실제 모델과 동기화되어 있음

ReplyList 기반의 더미 데이터가 컴포넌트 기대 형태를 잘 보여줍니다. 디자인 확인에 유용합니다.

app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt (4)

59-61: 새 코멘트 아키텍처로의 의존성 정리 좋습니다

CommentsEvent/CommentsViewModel 도입 및 import 추가가 변경된 흐름(이벤트 기반)과 일관됩니다.


366-369: 댓글 시트 오픈 처리 OK

게시물 선택과 바텀시트 표시를 함께 처리해 UX 흐름이 명확합니다.

Also applies to: 380-383


473-479: 초기화 타이밍 적절하나, 상태 초기화(or 중복 로딩 방지) 여부 확인 필요

선택된 게시물 변경 시 initialize를 트리거하는 구조는 맞습니다. 다만 ViewModel이 기존 목록을 잠깐 보여주는 플리커가 없도록 초기화(로딩 상태 전환, 이전 댓글 캐시 클리어 등)가 이 이벤트에서 보장되는지 확인이 필요합니다. 빠르게 게시물 전환 시 중복 네트워크 요청이 겹치지 않도록 취소 처리도 고려해 주세요.

원하시면 CommentsViewModel에 초기화/취소 로직(예: 기존 Job cancel, UI 상태 reset)을 위한 보일러플레이트를 제안드릴 수 있습니다.


481-483: BottomSheet에 상태/이벤트 주입 방식 적절합니다

단방향 데이터 흐름을 유지하면서 UI-VM 경계를 명확히 했습니다.

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

14-24: 런타임 예외를 Result로 감싸는 패턴 적절합니다

handleBaseResponse().getOrThrow()를 runCatching으로 감싼 구조로, 호출부에서 성공/실패를 일관되게 다룰 수 있습니다.

Also applies to: 36-52

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

33-36: 이벤트 라우팅과 식별자 전달이 명확합니다

  • 최상위 댓글 좋아요: CommentsEvent.LikeComment(commentId)
  • 대댓글 좋아요: CommentsEvent.LikeReply(parentCommentId, replyId)
  • 대댓글 쓰기: (parent commentId, nickname) 전달

이벤트 기반 상호작용이 깔끔하게 정리되어 있습니다.

Also applies to: 38-49

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

37-43: 데이터 바인딩과 상호작용 연결 상태 양호

  • 프로필/별칭/색상/날짜/내용 바인딩이 새로운 CommentList 모델과 일치합니다.
  • 좋아요 아이콘은 data.isLike에 맞춰 토글되고, 클릭 시 외부 onLikeClick을 호출합니다.
  • 답글 쓰기 클릭은 작성자 닉네임을 콜백으로 전달해 상위에서 parentId/mention 관리가 용이합니다.

Also applies to: 59-63, 66-75

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

142-177: 무한 스크롤 동시 요청 방지 로직 적절
기존 구현에서 아래 사항이 모두 만족되어 추가 검증이 불필요합니다:

  • isLoading/isLoadingMore 플래그로 빠른 스크롤 시 중복 요청이 차단됩니다.
  • 네트워크 오류(onFailure) 발생 시 nextCursor를 그대로 유지해 올바른 페이지 재시도가 가능합니다.
  • 에러 시에도 _uiState.comments가 초기화되지 않아 캐시된 데이터와의 동기화가 유지됩니다.

위 로직으로 보아 현재 구현은 edge case를 올바르게 처리하고 있어, 더 이상의 수정은 필요하지 않습니다.

Comment on lines +7 to +11
val content: String,
val isReplyRequest: Boolean,
val parentId: Int? = null,
val postType: String,
)
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

타입 일관성 및 무결성 보강(빈 콘텐츠/답글 규칙 검증)

  • parentId는 댓글 ID이므로 Long?으로 일관화 권장
  • 잘못된 조합 전송 방지를 위해 기본 무결성 검증을 추가(빈 content 금지, isReplyRequest에 따른 parentId 존재성)

아래 변경 제안은 컴파일 타임에 깨지는 부분이 없는지 전체 빌드 확인이 필요합니다.

 @Serializable
 data class CommentsCreateRequest(
     val content: String,
     val isReplyRequest: Boolean,
-    val parentId: Int? = null,
+    val parentId: Long? = null,
     val postType: String,
-)
+) {
+    init {
+        require(content.isNotBlank()) { "content must not be blank" }
+        if (isReplyRequest) {
+            require(parentId != null) { "parentId is required when isReplyRequest = true" }
+        } else {
+            require(parentId == null) { "parentId must be null when isReplyRequest = false" }
+        }
+    }
+}

무결성 체크를 모델에 두는 것이 부담이라면 Repository 레이어에서 요청 생성 전에 동일한 검증을 수행하는 것도 괜찮습니다.


🏁 Script executed:

#!/bin/bash
set -e

echo "🔍 Searching for usages of CommentsCreateRequest in the codebase"
rg "CommentsCreateRequest" -n .

echo "🔍 Searching for all occurrences of parentId to see how it’s consumed"
rg "parentId" -n .

Length of output: 2313


🏁 Script executed:

#!/bin/bash
set -e

echo "🔍 CommentsService.kt 상단 확인"
sed -n '1,200p' app/src/main/java/com/texthip/thip/data/service/CommentsService.kt

echo
echo "🔍 CommentsRepository.kt 주요 메서드 시그니처 확인"
sed -n '1,200p' app/src/main/java/com/texthip/thip/data/repository/CommentsRepository.kt

Length of output: 3197


CommentsCreateRequest의 parentId 타입 일관화 및 무결성 검증 추가 필요
parentId가 댓글 ID이므로 Int→Long으로 변경 시, 요청 모델뿐 아니라 호출부 시그니처도 함께 수정해야 컴파일 오류를 방지할 수 있습니다. 또한 빈 content 전송과 isReplyRequest/parentId 조합을 런타임에 검증하도록 init 블록을 추가하세요.

수정이 필요한 위치:

  • app/src/main/java/com/texthip/thip/data/model/comments/request/CommentsCreateRequest.kt
  • app/src/main/java/com/texthip/thip/data/repository/CommentsRepository.kt (createComment 메서드의 parentId: Int? → Long?)
  • app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/CommentsViewModel.kt (CreateComment 이벤트와 createComment 함수 파라미터)
  • app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt (onSendReply 콜백의 parentId 타입)

예시 변경안 (CommentsCreateRequest.kt):

 @Serializable
 data class CommentsCreateRequest(
     val content: String,
     val isReplyRequest: Boolean,
-    val parentId: Int? = null,
+    val parentId: Long? = null,
     val postType: String,
 ) {
     init {
         require(content.isNotBlank()) { "content must not be blank" }
         if (isReplyRequest) {
             require(parentId != null) { "parentId is required when isReplyRequest = true" }
         } else {
             require(parentId == null) { "parentId must be null when isReplyRequest = false" }
         }
     }
 }

변경 후 전체 빌드 및 테스트를 반드시 확인해주세요.

🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/data/model/comments/request/CommentsCreateRequest.kt
around lines 7-11, change parentId type from Int? to Long?, add an init block
that validates content is not blank and enforces consistency between
isReplyRequest and parentId (if isReplyRequest==true then parentId must be
non-null, if false then parentId must be null) and throw
IllegalArgumentException on violation; then update all call sites' signatures to
use Long? — specifically update
app/src/main/java/com/texthip/thip/data/repository/CommentsRepository.kt
(createComment parentId: Int? → Long?),
app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/CommentsViewModel.kt
(CreateComment event and createComment function parameter types), and
app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt
(onSendReply callback parentId type) so types match, rebuild and run tests to
ensure no compilation errors.


@Serializable
data class CommentsCreateResponse(
val commentId: Int,
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

ID 타입 일관성: Int → Long으로 변경 권장

댓글·게시글 ID 전반을 Long으로 일관되게 사용하는 편이 안전합니다(특히 Service/Repository에서 Long 사용 중). 응답 commentId도 Long으로 맞추는 것을 제안드립니다.

 data class CommentsCreateResponse(
-    val commentId: Int,
+    val commentId: Long,
 )
📝 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 commentId: Int,
data class CommentsCreateResponse(
val commentId: Long,
)
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/data/model/comments/response/CommentsCreateResponse.kt
around line 7, the commentId is declared as Int but your codebase uses Long for
IDs; change the type to Long to keep ID type consistent, update any
constructor/serialization annotations if needed, and adjust mapping code that
builds CommentsCreateResponse so it assigns the Long id from the entity/service
rather than converting to Int; also update any unit tests or callers expecting
Int to use Long.


@Serializable
data class CommentsLikesResponse(
val commentId: Int,
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

ID 타입 일관성: Int → Long으로 변경 권장

Service 레이어에서 path param이 Long(예: likeComment(commentId: Long))으로 사용되고 있어 응답 모델의 commentId도 Long으로 맞추는 편이 안전합니다. 타입 불일치가 있으면 매핑·캐스팅 비용과 잠재적 오버플로/트렁케이션 이슈가 생길 수 있습니다.

다음과 같이 변경을 제안합니다:

 data class CommentsLikesResponse(
-    val commentId: Int,
+    val commentId: Long,
     val isLiked: Boolean,
 )
📝 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 commentId: Int,
data class CommentsLikesResponse(
val commentId: Long,
val isLiked: Boolean,
)
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/data/model/comments/response/CommentsLikesResponse.kt
around line 7, the commentId field is declared as Int but the service layer
expects Long (e.g., likeComment(commentId: Long)); change the commentId type
from Int to Long to ensure type consistency. Update any related constructors,
data class properties, serialization/deserialization annotations,
equals/hashCode or copy usages, and adjust unit tests or mapping code that
reference this field to use Long so all layers align.

Comment on lines +6 to +10
data class CommentsResponse(
val commentList: List<CommentList>,
val nextCursor: String?,
val isLast: Boolean,
)
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

디코딩 탄탄함 확보: 기본값으로 방어적 직렬화

네트워크 응답에서 일부 필드가 누락될 수 있는 상황(서버 진화, 실험 플래그 등)을 고려해 commentList, nextCursor에 기본값을 두면 파싱 실패를 줄일 수 있습니다.

 @Serializable
 data class CommentsResponse(
-    val commentList: List<CommentList>,
-    val nextCursor: String?,
+    val commentList: List<CommentList> = emptyList(),
+    val nextCursor: String? = null,
     val isLast: Boolean,
 )
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/data/model/comments/response/CommentsResponse.kt
around lines 6 to 10, the data class lacks defensive defaults so missing fields
in network responses can cause parsing failures; update the primary constructor
to give commentList a default emptyList(), nextCursor a default null (even if
nullable), and isLast a sensible default (e.g., false) so deserialization
succeeds when those fields are absent.

Comment on lines +12 to +41
@Serializable
data class CommentList(
val commentId: Int,
val creatorId: Int,
val creatorProfileImageUrl: String,
val creatorNickname: String,
val aliasName: String,
val aliasColor: String,
val postDate: String,
val content: String,
val likeCount: Int,
val isDeleted: Boolean,
val isLike: Boolean,
val replyList: List<ReplyList>,
)

@Serializable
data class ReplyList(
val commentId: Int,
val parentCommentCreatorNickname: String,
val creatorId: Int,
val creatorProfileImageUrl: String,
val creatorNickname: String,
val aliasName: String,
val aliasColor: String,
val postDate: String,
val content: String,
val likeCount: Int,
val isLike: Boolean,
) No newline at end of file
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

서버 키 네이밍(camelCase/snake_case) 및 직렬화 설정 점검

프로퍼티들이 camelCase입니다. 서버가 snake_case로 응답한다면 @SerialName 매핑 또는 Retrofit의 Kotlinx Serialization 설정이 필요합니다.

다음 스크립트로 프로젝트 내 직렬화 설정과 @SerialName 사용 여부를 점검해 주세요. 예상:

  • Kotlinx Json 설정에서 ignoreUnknownKeys = true 등 옵션 확인
  • Retrofit에 asConverterFactory로 Kotlinx Serialization 연결 여부 확인
  • 기존 모델들에서 @SerialName 사용 패턴 확인

🏁 Script executed:

#!/bin/bash
# 직렬화 설정/사용 패턴 점검
echo "[Json 설정 탐색]"
rg -n "Json\\(" -A 5

echo
echo "[Retrofit + Kotlinx Serialization 연결(asConverterFactory) 탐색]"
rg -n "asConverterFactory|kotlinx.serialization" -A 3

echo
echo "[@SerialName 사용 사례 탐색]"
rg -n "@SerialName\\(" -A 1

echo
echo "[CommentsService/Response 사용처 탐색]"
rg -n "CommentsService|CommentsResponse|CommentList|ReplyList" -A 3

Length of output: 63158


서버 응답 필드 네이밍 일관성 검토 및 직렬화 설정 보완 필요

현재 NetworkModule.provideJson() 에는
• isLenient, prettyPrint, encodeDefaults, explicitNulls 만 설정되어 있으며
• ignoreUnknownKeys 및 snake_case ↔ camelCase 자동 변환(namingStrategy)이 빠져 있습니다.

또한 CommentsResponse.ktCommentList/ReplyList 에는 @SerialName 어노테이션이 없어,
서버가 snake_case 로 응답할 경우 필드 매핑 에러가 발생할 수 있습니다.

다음 중 한 가지 방법으로 수정해주세요.

  • NetworkModule.provideJson() 에 ignoreUnknownKeys = true
    json { …; namingStrategy = JsonNamingStrategy.SnakeCase } 추가
  • CommentList/ReplyList 각 프로퍼티에 @SerialName("snake_case_name") 어노테이션 부여

Retrofit-Kotlinx-Serialization 연결(asConverterFactory)은 정상 확인됐습니다.

🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/data/model/comments/response/CommentsResponse.kt
around lines 12 to 41, the data classes lack explicit serialization mapping for
snake_case server responses; either enable snake_case handling globally or
annotate fields—Preferred fix: update NetworkModule.provideJson() to set
ignoreUnknownKeys = true and configure namingStrategy =
JsonNamingStrategy.SnakeCase so server snake_case names map to camelCase
properties automatically; alternative fix: add @SerialName("snake_case_name") to
each property in CommentList and ReplyList to match the server field names.

Comment on lines 40 to 45
fun CommentBottomSheet(
commentResponse: List<CommentItem>,
uiState: CommentsUiState,
onEvent: (CommentsEvent) -> Unit,
onDismiss: () -> Unit,
onSendReply: (String, Int?, String?) -> Unit
onSendReply: (text: String, parentCommentId: Int?, replyToNickname: String?) -> Unit
) {
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

onSendReply 콜백이 CommentsEvent.CreateComment와 중복

onSendReply 파라미터와 CommentsEvent.CreateComment가 동일한 목적을 가지고 있어 중복됩니다. 일관성을 위해 onEvent를 통해 처리하는 것이 좋습니다.

 fun CommentBottomSheet(
     uiState: CommentsUiState,
     onEvent: (CommentsEvent) -> Unit,
-    onDismiss: () -> Unit,
-    onSendReply: (text: String, parentCommentId: Int?, replyToNickname: String?) -> Unit
+    onDismiss: () -> Unit
 ) {

그리고 Line 101-110 부분을 다음과 같이 수정:

 onSendClick = {
-    onSendReply(
-        inputText,
-        replyingToCommentId,
-        replyingToNickname
-    )
+    onEvent(CommentsEvent.CreateComment(
+        content = inputText,
+        parentId = replyingToCommentId
+    ))
     inputText = ""
     replyingToCommentId = null
     replyingToNickname = null
 },
📝 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
fun CommentBottomSheet(
commentResponse: List<CommentItem>,
uiState: CommentsUiState,
onEvent: (CommentsEvent) -> Unit,
onDismiss: () -> Unit,
onSendReply: (String, Int?, String?) -> Unit
onSendReply: (text: String, parentCommentId: Int?, replyToNickname: String?) -> Unit
) {
// Updated function signature (remove onSendReply)
fun CommentBottomSheet(
uiState: CommentsUiState,
onEvent: (CommentsEvent) -> Unit,
onDismiss: () -> Unit
) {
Suggested change
fun CommentBottomSheet(
commentResponse: List<CommentItem>,
uiState: CommentsUiState,
onEvent: (CommentsEvent) -> Unit,
onDismiss: () -> Unit,
onSendReply: (String, Int?, String?) -> Unit
onSendReply: (text: String, parentCommentId: Int?, replyToNickname: String?) -> Unit
) {
// Updated send-click handler to use onEvent instead of onSendReply
onSendClick = {
onEvent(CommentsEvent.CreateComment(
content = inputText,
parentId = replyingToCommentId
))
inputText = ""
replyingToCommentId = null
replyingToNickname = null
},
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/ui/group/note/component/CommentBottomSheet.kt
around lines 40 to 45, the onSendReply parameter duplicates the purpose of
CommentsEvent.CreateComment and should be removed; change the function signature
to drop onSendReply and inside the sheet where replies are sent (around lines
101-110) dispatch a CommentsEvent.CreateComment via onEvent with the text,
parentCommentId and replyToNickname, and update any callers of
CommentBottomSheet to no longer pass onSendReply and instead rely on onEvent for
creating comments.

Comment on lines +69 to 93
Box(modifier = Modifier.weight(1f)) {
if (uiState.isLoading) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
CircularProgressIndicator()
}
} else if (uiState.comments.isEmpty()) {
EmptyCommentView()
} else {
CommentLazyList(
commentList = uiState.comments,
isLoadingMore = uiState.isLoadingMore,
isLastPage = uiState.isLast,
onLoadMore = { onEvent(CommentsEvent.LoadMoreComments) },
onReplyClick = { commentId, nickname ->
replyingToCommentId = commentId
replyingToNickname = nickname
},
onEvent = onEvent
)
}
} else {
CommentList(
commentList = commentResponse,
onSendReply = { replyText, commentId, replyTo ->
onSendReply(replyText, commentId, replyTo)
inputText = ""
},
onReplyClick = { replyItem ->
replyingTo = replyItem
}
)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

에러 상태 처리 누락

CommentsUiStateerror 필드가 있지만 UI에서 에러를 표시하지 않고 있습니다. 사용자에게 에러 상황을 알려주는 것이 중요합니다.

 Box(modifier = Modifier.weight(1f)) {
     if (uiState.isLoading) {
         Column(
             modifier = Modifier.fillMaxSize(),
             verticalArrangement = Arrangement.Center,
             horizontalAlignment = Alignment.CenterHorizontally
         ) {
             CircularProgressIndicator()
         }
+    } else if (uiState.error != null) {
+        Column(
+            modifier = Modifier.fillMaxSize(),
+            verticalArrangement = Arrangement.Center,
+            horizontalAlignment = Alignment.CenterHorizontally
+        ) {
+            Text(
+                text = uiState.error,
+                style = typography.copy_r400_s14,
+                color = colors.Red,
+                modifier = Modifier.padding(horizontal = 20.dp)
+            )
+        }
     } else if (uiState.comments.isEmpty()) {
         EmptyCommentView()
     } else {
📝 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
Box(modifier = Modifier.weight(1f)) {
if (uiState.isLoading) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
CircularProgressIndicator()
}
} else if (uiState.comments.isEmpty()) {
EmptyCommentView()
} else {
CommentLazyList(
commentList = uiState.comments,
isLoadingMore = uiState.isLoadingMore,
isLastPage = uiState.isLast,
onLoadMore = { onEvent(CommentsEvent.LoadMoreComments) },
onReplyClick = { commentId, nickname ->
replyingToCommentId = commentId
replyingToNickname = nickname
},
onEvent = onEvent
)
}
} else {
CommentList(
commentList = commentResponse,
onSendReply = { replyText, commentId, replyTo ->
onSendReply(replyText, commentId, replyTo)
inputText = ""
},
onReplyClick = { replyItem ->
replyingTo = replyItem
}
)
}
Box(modifier = Modifier.weight(1f)) {
if (uiState.isLoading) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
CircularProgressIndicator()
}
} else if (uiState.error != null) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = uiState.error,
style = typography.copy_r400_s14,
color = colors.Red,
modifier = Modifier.padding(horizontal = 20.dp)
)
}
} else if (uiState.comments.isEmpty()) {
EmptyCommentView()
} else {
CommentLazyList(
commentList = uiState.comments,
isLoadingMore = uiState.isLoadingMore,
isLastPage = uiState.isLast,
onLoadMore = { onEvent(CommentsEvent.LoadMoreComments) },
onReplyClick = { commentId, nickname ->
replyingToCommentId = commentId
replyingToNickname = nickname
},
onEvent = onEvent
)
}
}
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/ui/group/note/component/CommentBottomSheet.kt
around lines 69-93, the UI ignores CommentsUiState.error so users never see
failures; add an explicit check for uiState.error inside the Box and render an
error UI (e.g., a small ErrorView/Text with the error message and a retry
button) before other branches (so error supersedes loading/empty/list), and wire
the retry button to call back into onEvent with an appropriate retry/refresh
event (or expose a retry lambda) so users can retry the load.

Comment on lines +165 to +167
val commentsViewModel: CommentsViewModel = hiltViewModel()
val commentsUiState by commentsViewModel.uiState.collectAsStateWithLifecycle()

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Preview 크래시 및 과도한 ViewModel 생명주기 범위: hiltViewModel()를 BottomSheet 조건 블록 안으로 이동하세요

현재 GroupNoteContent 내부에서 무조건 hiltViewModel()를 호출하고 있어 Compose Preview에서 Hilt 컨텍스트가 없어 크래시가 발생할 수 있고, 코멘트 시트가 열리지 않았는데도 ViewModel이 생성됩니다. 시트가 열릴 때만 ViewModel을 생성하도록 if 블록 안으로 이동하는 편이 안전합니다.

아래처럼 이동해 주세요.

@@
-    val commentsViewModel: CommentsViewModel = hiltViewModel()
-    val commentsUiState by commentsViewModel.uiState.collectAsStateWithLifecycle()
@@
-    if (isCommentBottomSheetVisible && selectedPostForComment != null) {
+    if (isCommentBottomSheetVisible && selectedPostForComment != null) {
+        val commentsViewModel: CommentsViewModel = hiltViewModel()
+        val commentsUiState by commentsViewModel.uiState.collectAsStateWithLifecycle()
         LaunchedEffect(selectedPostForComment) {
             commentsViewModel.initialize(
                 postId = selectedPostForComment!!.postId.toLong(),
                 postType = selectedPostForComment!!.postType
             )
         }
@@
         CommentBottomSheet(
             uiState = commentsUiState,
             onEvent = commentsViewModel::onEvent,

Comment on lines +42 to +46
if (currentPostId == postId) return
this.currentPostId = postId
this.currentPostType = postType
fetchComments(isRefresh = true)
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

동일한 postId 체크 시 postType 변경도 고려 필요

현재 로직은 postId만 체크하여 중복 초기화를 방지하고 있습니다. 하지만 동일한 postId라도 postType이 변경될 수 있는 경우를 고려해야 합니다.

 fun initialize(postId: Long, postType: String) {
-    if (currentPostId == postId) return
+    if (currentPostId == postId && currentPostType == postType) return
     this.currentPostId = postId
     this.currentPostType = postType
     fetchComments(isRefresh = 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
if (currentPostId == postId) return
this.currentPostId = postId
this.currentPostType = postType
fetchComments(isRefresh = true)
}
fun initialize(postId: Long, postType: String) {
if (currentPostId == postId && currentPostType == postType) return
this.currentPostId = postId
this.currentPostType = postType
fetchComments(isRefresh = true)
}
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/CommentsViewModel.kt
around lines 42 to 46, the early-return only checks currentPostId and ignores
postType so updates are skipped when postType changes; change the guard to
return only when both currentPostId == postId and currentPostType == postType,
and otherwise assign both this.currentPostId and this.currentPostType and call
fetchComments(isRefresh = true) so postType changes trigger reinitialization.

Comment on lines +18 to +28
fun Modifier.advancedImePadding() = composed {
var consumePadding by remember { mutableStateOf(0) }
onGloballyPositioned { coordinates ->
val rootCoordinate = coordinates.findRootCoordinates()
val bottom = coordinates.positionInWindow().y + coordinates.size.height

consumePadding = (rootCoordinate.size.height - bottom).toInt().coerceAtLeast(0)
}
.consumeWindowInsets(PaddingValues(bottom = (consumePadding / LocalDensity.current.density).dp))
.imePadding()
} No newline at end of file
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

IME 패딩 계산 로직이 의도대로 동작하지 않음(부호/소비 순서 문제) — 실제로는 패딩 반영되지 않을 가능성 큼

문제점:

  • overlap 계산이 반대로 되어, 키보드에 가려지는 경우에도 0으로 클램프되어 추가 패딩이 적용되지 않습니다.
  • consumeWindowInsets(PaddingValues(...))를 imePadding() 전에 호출하는 구성은 imePadding에 영향을 주지 못합니다(특정 WindowInsets를 소비해야 하며, 현재는 수동 PaddingValues만 소비하고 있음).
  • 계산된 값이 실제 패딩으로 적용되지 않습니다(consumeWindowInsets는 패딩 적용이 아니라 전파 차단 역할).

수정안: 겹침(overlap) 픽셀을 Dp로 변환해 실제 bottom padding으로 적용하고, 그 값을 소비한 뒤 imePadding을 추가하세요.

아래 diff를 적용하세요(해당 범위 내 변경). 추가로 파일 상단에 padding import가 필요합니다.

 fun Modifier.advancedImePadding() = composed {
-    var consumePadding by remember { mutableStateOf(0) }
-    onGloballyPositioned { coordinates ->
-        val rootCoordinate = coordinates.findRootCoordinates()
-        val bottom = coordinates.positionInWindow().y + coordinates.size.height
-
-        consumePadding = (rootCoordinate.size.height - bottom).toInt().coerceAtLeast(0)
-    }
-        .consumeWindowInsets(PaddingValues(bottom = (consumePadding / LocalDensity.current.density).dp))
-        .imePadding()
+    val density = LocalDensity.current
+    var bottomPaddingDp by remember { mutableStateOf(0.dp) }
+    onGloballyPositioned { coordinates ->
+        val root = coordinates.findRootCoordinates()
+        val elementBottom = coordinates.positionInWindow().y + coordinates.size.height
+        val rootBottom = root.positionInWindow().y + root.size.height
+        val overlapPx = (elementBottom - rootBottom).coerceAtLeast(0f)
+        bottomPaddingDp = with(density) { overlapPx.toDp() }
+    }
+        .padding(bottom = bottomPaddingDp)
+        .consumeWindowInsets(PaddingValues(bottom = bottomPaddingDp))
+        .imePadding()
 }

추가 import(선택사항: 적용 시 필요):

import androidx.compose.foundation.layout.padding

대안: 특별한 커스텀 계산이 필요 없다면 단순히 Modifier.windowInsetsPadding(WindowInsets.ime) 또는 현행 imePadding()만으로도 대부분의 케이스를 커버할 수 있습니다.

🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/utils/rooms/AdvancedImePadding.kt around
lines 18 to 28, the overlap calculation and consumption order are incorrect so
padding isn't applied; change logic to compute the overlap (root height -
bottom), convert that pixel value to Dp, apply it as bottom padding via
.padding(bottom = overlapDp), then consume the corresponding Insets (so they
won’t be double-handled) before applying .imePadding(); also add import
androidx.compose.foundation.layout.padding at the top. Ensure you clamp overlap
to >= 0, convert using LocalDensity.current.density, apply .padding(bottom =
overlapDp) and then call .consumeWindowInsets(...) appropriately followed by
.imePadding().

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

🔭 Outside diff range comments (4)
app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomScreen.kt (1)

60-62: LaunchedEffect 키가 Unit 고정이라 roomId 변경 시 재조회되지 않습니다

roomId가 바뀌어도 효과가 재실행되지 않아 이전 방 데이터가 그대로 노출될 수 있습니다. 키를 roomId로 변경하세요.

- LaunchedEffect(key1 = Unit) {
+ LaunchedEffect(key1 = roomId) {
     viewModel.fetchRoomsPlaying(roomId)
 }
app/src/main/java/com/texthip/thip/ui/group/note/component/CommentItem.kt (1)

37-81: 삭제된 댓글(isDeleted) UI 미처리: 내용/액션 숨김 처리 필요

서버 모델에 isDeleted가 있는데, 현재 UI는 삭제 상태를 고려하지 않고 본문/답글/좋아요 UI를 그대로 노출합니다. 기본적으로 “삭제된 댓글입니다.” 같은 플레이스홀더를 표시하고 액션(답글/좋아요)을 비활성화하는 것이 안전합니다.

예시 수정안:

         ProfileBarFeed(
             profileImage = data.creatorProfileImageUrl,
             nickname = data.creatorNickname,
             genreName = data.aliasName,
             genreColor = hexToColor(data.aliasColor),
             date = data.postDate
         )
 
-        Row(
-            horizontalArrangement = Arrangement.spacedBy(20.dp)
-        ) {
-            Column(
-                modifier = Modifier
-                    .weight(1f),
-                verticalArrangement = Arrangement.spacedBy(12.dp)
-            ) {
-                Text(
-                    text = data.content,
-                    color = colors.Grey,
-                    style = typography.feedcopy_r400_s14_h20,
-                )
-                Text(
-                    modifier = Modifier.clickable(onClick = { onReplyClick(data.creatorNickname) }),
-                    text = stringResource(R.string.write_reply),
-                    style = typography.menu_sb600_s12,
-                    color = colors.Grey02,
-                )
-            }
-
-            Column(
-                modifier = Modifier.clickable(onClick = onLikeClick),
-                horizontalAlignment = Alignment.CenterHorizontally,
-                verticalArrangement = Arrangement.spacedBy(2.dp),
-            ) {
-                Icon(
-                    painter = painterResource(if (data.isLike) R.drawable.ic_heart_center_filled else R.drawable.ic_heart_center),
-                    contentDescription = null,
-                    tint = Color.Unspecified
-                )
-                Text(
-                    text = data.likeCount.toString(),
-                    style = typography.navi_m500_s10,
-                    color = colors.White,
-                )
-            }
-        }
+        if (data.isDeleted) {
+            Text(
+                text = stringResource(R.string.comment_deleted_placeholder), // 새 리소스 필요
+                color = colors.Grey01,
+                style = typography.feedcopy_r400_s14_h20,
+            )
+        } else {
+            Row(
+                horizontalArrangement = Arrangement.spacedBy(20.dp)
+            ) {
+                Column(
+                    modifier = Modifier.weight(1f),
+                    verticalArrangement = Arrangement.spacedBy(12.dp)
+                ) {
+                    Text(
+                        text = data.content,
+                        color = colors.Grey,
+                        style = typography.feedcopy_r400_s14_h20,
+                    )
+                    Text(
+                        modifier = Modifier.clickable { onReplyClick(data.creatorNickname) },
+                        text = stringResource(R.string.write_reply),
+                        style = typography.menu_sb600_s12,
+                        color = colors.Grey02,
+                    )
+                }
+                Column(
+                    modifier = Modifier.clickable(onClick = onLikeClick),
+                    horizontalAlignment = Alignment.CenterHorizontally,
+                    verticalArrangement = Arrangement.spacedBy(2.dp),
+                ) {
+                    Icon(
+                        painter = painterResource(if (data.isLike) R.drawable.ic_heart_center_filled else R.drawable.ic_heart_center),
+                        contentDescription = null,
+                        tint = Color.Unspecified
+                    )
+                    Text(
+                        text = data.likeCount.toString(),
+                        style = typography.navi_m500_s10,
+                        color = colors.White,
+                    )
+                }
+            }
+        }

참고: stringResource(R.string.comment_deleted_placeholder) 신규 추가가 필요합니다.

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

139-149: 참여자 아이콘 버튼이 동작하지 않습니다: onNavigateToMates에 연결하세요

우측 IconButton onClick이 TODO로 남아 있습니다. 상단 Row 전체가 클릭 가능한 것과 UX가 불일치합니다. onNavigateToMates로 통일하세요.

-                    IconButton(
-                        onClick = { /* TODO: Navigate to participant list */ },
+                    IconButton(
+                        onClick = onNavigateToMates,
                         modifier = Modifier.size(24.dp)
                     ) {
app/src/main/java/com/texthip/thip/ui/group/note/component/CommentSection.kt (1)

26-28: LazyColumn 아이템 내 fillMaxHeight 사용은 과도한 높이 측정 이슈를 유발할 수 있습니다.

  • CommentSectionLazyColumn의 아이템으로 쓰이는 맥락에서 fillMaxHeight()는 각 아이템이 화면 높이를 점유하도록 유도, 스크롤/측정 비효율과 잘림을 야기할 수 있습니다. 제거를 권장합니다.
-        Column(
-            modifier = Modifier
-                .fillMaxHeight()
-                .padding(20.dp),
+        Column(
+            modifier = Modifier
+                .padding(20.dp),
             verticalArrangement = Arrangement.spacedBy(20.dp),
             horizontalAlignment = Alignment.Start
         ) {
🧹 Nitpick comments (21)
app/src/main/java/com/texthip/thip/ui/common/header/ProfileBar.kt (1)

47-53: 접근성/현지화: contentDescription 하드코딩 문자열 제거 제안

"프로필 이미지" 문자열을 string 리소스로 이전해 현지화/일관성을 확보하는 것을 권장합니다.

적용 예시(해당 라인만 변경):

-            contentDescription = "프로필 이미지",
+            contentDescription = stringResource(R.string.profile_image_content_description),

strings.xml에 다음 키 추가가 필요합니다:

<string name="profile_image_content_description">프로필 이미지</string>
app/src/main/java/com/texthip/thip/data/model/comments/request/CommentsLikesRequest.kt (1)

5-8: 요청 필드 명확화: Boolean 의미가 드러나도록 이름 개선 제안

Boolean 필드명이 type은 의미가 모호합니다. Kotlin 측 필드명을 isLike로 명확히 하고, 서버가 "type" 키를 요구한다면 @SerialName으로 매핑하는 방식을 권장합니다. 서버 스키마 변경 없이 가독성만 개선됩니다.

 import kotlinx.serialization.Serializable
+import kotlinx.serialization.SerialName
 
 @Serializable
 data class CommentsLikesRequest(
-    val type: Boolean
+    @SerialName("type")
+    val isLike: Boolean
 )
app/src/main/java/com/texthip/thip/ui/common/header/ProfileBarFeed.kt (1)

41-50: 빈 문자열 처리 및 로딩 품질 개선 제안 (crossfade, Crop, isNullOrBlank)

  • URL이 빈 문자열("")인 경우를 함께 방지하는 편이 안전합니다.
  • 원형 아바타는 보통 중앙 크롭(Crop)이 자연스럽습니다.
  • Coil crossfade로 전환 시 품질을 개선할 수 있습니다.
-        Row {
-            if (profileImage != null) {
-                AsyncImage(
-                    model = profileImage,
-                    contentDescription = "프로필 이미지",
-                    modifier = Modifier
-                        .size(24.dp)
-                        .clip(CircleShape)
-                )
+        Row {
+            if (!profileImage.isNullOrBlank()) {
+                AsyncImage(
+                    model = ImageRequest.Builder(LocalContext.current)
+                        .data(profileImage)
+                        .crossfade(true)
+                        .build(),
+                    contentDescription = "프로필 이미지",
+                    modifier = Modifier
+                        .size(24.dp)
+                        .clip(CircleShape),
+                    contentScale = ContentScale.Crop
+                )

추가 import:

import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import coil.request.ImageRequest
app/src/main/java/com/texthip/thip/ui/group/room/component/GroupRoomMatesList.kt (1)

35-36: 잘못된/빈 색상값 대비용 폴백 색상 보강 제안

hexToColor는 파싱 실패 시 Color.White를 반환하므로, 밝은 배경에서는 가독성 문제가 날 수 있습니다. 빈 문자열 방어와 함께 테마 색상으로 폴백하는 것이 안전합니다.

-                bottomTextColor = hexToColor(member.aliasColor),
+                bottomTextColor = member.aliasColor
+                    .takeIf { it.isNotBlank() }
+                    ?.let { hexToColor(it) }
+                    ?: colors.ScienceIt,
app/src/main/java/com/texthip/thip/data/model/comments/request/CommentsCreateRequest.kt (1)

6-11: 서버 스펙과 필드명/케이싱 일치 여부 확인 (@SerialName 고려)

서버가 snake_case 또는 다른 키명을 사용한다면, 현재 프로퍼티명(camelCase)이 그대로 직렬화되어 API와 불일치할 수 있습니다. 백엔드 스펙에 맞춰 @SerialName("...")를 명시적으로 부여하는 것을 권장합니다. 특히 isReplyRequest, postType, parentId의 키명을 백엔드 계약과 재확인해 주세요.

원하시면 서버 스펙을 기준으로 @SerialName 주석을 일괄 반영한 패치를 준비해 드리겠습니다.

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

23-27: 파라미터 명칭 혼동: Body는 request로 명명 권장

Body 파라미터 이름이 response로 되어 있어 혼동을 유발합니다. request로 변경해 가독성과 일관성을 높이세요.

-    suspend fun likeComment(
-        @Path("commentId") commentId: Long,
-        @Body response: CommentsLikesRequest
-    ): BaseResponse<CommentsLikesResponse>
+    suspend fun likeComment(
+        @Path("commentId") commentId: Long,
+        @Body request: CommentsLikesRequest
+    ): BaseResponse<CommentsLikesResponse>

17-21: postType String 남용 → 타입 세이프 enum으로 교체 제안

Query 파라미터를 String 리터럴로 유지하면 오타/스펙 변경에 취약합니다. enum(예: PostType)으로 치환해 컴파일 타임 안전성을 확보하는 것을 권장합니다. 레포지토리/모델과 함께 일괄 반영되어야 합니다.

적용 방향:

  • enum 정의(예시)
enum class PostType { RECORD, DISCUSSION /* ... */ }
  • 서비스 시그니처
- @Query("postType") postType: String = "RECORD",
+ @Query("postType") postType: PostType = PostType.RECORD,
  • Retrofit은 기본적으로 enum을 name 문자열로 직렬화합니다. 서버가 다른 표기(예: "record")를 요구하면 enum에 @SerialName 또는 커스텀 toString을 적용하세요. 서버 표기를 확인해 주세요.
app/src/main/java/com/texthip/thip/ui/group/note/component/CommentItem.kt (1)

71-75: 아이콘 접근성: contentDescription 제공 권장

하트 아이콘의 contentDescription이 null입니다. 스크린리더 사용자 접근성을 위해 상태에 따라 “좋아요”/“좋아요 취소” 등 설명을 제공해주세요. 새로운 문자열 리소스가 필요합니다.

-                Icon(
-                    painter = painterResource(if (data.isLike) R.drawable.ic_heart_center_filled else R.drawable.ic_heart_center),
-                    contentDescription = null,
-                    tint = Color.Unspecified
-                )
+                Icon(
+                    painter = painterResource(if (data.isLike) R.drawable.ic_heart_center_filled else R.drawable.ic_heart_center),
+                    contentDescription = if (data.isLike) stringResource(R.string.cd_unlike) else stringResource(R.string.cd_like),
+                    tint = Color.Unspecified
+                )
app/src/main/java/com/texthip/thip/data/model/comments/response/CommentsResponse.kt (3)

12-26: 타입명 혼동: CommentList/ReplyList는 “단일 아이템”을 의미합니다.

  • 서버 응답의 “리스트”와 혼동될 수 있습니다. 유지보수성과 가독성을 위해 CommentListComment, ReplyListReply로의 리네임을 권장합니다. 이 변경은 전역 참조 수정이 필요하지만, 장기적으로 이득이 큽니다.

적용 예시:

-@Serializable
-data class CommentList(
+@Serializable
+data class Comment(
     val commentId: Int,
     val creatorId: Int,
     val creatorProfileImageUrl: String,
     val creatorNickname: String,
     val aliasName: String,
     val aliasColor: String,
     val postDate: String,
     val content: String,
     val likeCount: Int,
     val isDeleted: Boolean,
     val isLike: Boolean,
-    val replyList: List<ReplyList>,
+    val replyList: List<Reply>,
 )
 
 @Serializable
-data class ReplyList(
+data class Reply(
     val commentId: Int,
     val parentCommentCreatorNickname: String,
     val creatorId: Int,
     val creatorProfileImageUrl: String,
     val creatorNickname: String,
     val aliasName: String,
     val aliasColor: String,
     val postDate: String,
     val content: String,
     val likeCount: Int,
     val isLike: Boolean,
 )

리팩터 외부 영향:

  • UI 컴포저블(ReplyItem, CommentSection, CommentBottomSheet), VM/리포지토리에서 타입명 교체 필요.
  • import 경로는 동일, 식별자만 변경.

Also applies to: 28-41


14-16: ID 타입 일관성 검토 필요 (Int → Long 고려).

  • 현재 모든 ID가 Int입니다. 상위 도메인(예: GroupNoteScreen.initialize)에서 postIdLong으로 다뤄지고 있습니다. API가 long 범위의 ID를 반환할 가능성이 있다면, 댓글/답글의 commentId, creatorIdLong으로 승격하는 것을 권장합니다. 불일치 시 오버플로/캐스팅 문제 또는 혼선을 유발할 수 있습니다.

API 스펙 확인 후 필요 시 아래와 같이 변경:

-    val commentId: Int,
-    val creatorId: Int,
+    val commentId: Long,
+    val creatorId: Long,

Reply에도 동일 적용:

-    val commentId: Int,
-    val creatorId: Int,
+    val commentId: Long,
+    val creatorId: Long,

Also applies to: 30-33


5-10: JSON 키 네이밍(스네이크 vs 카멜) 확인 및 @SerialName 매핑 여부 검토.

  • 서버가 snake_case를 사용한다면 현재 프로퍼티명(카멜)과 자동 매핑이 실패할 수 있습니다. @SerialName으로 명시 매핑을 권장합니다. 확인 후 필요 시 반영해 주세요.

예시:

@Serializable
data class Comment(
    @SerialName("comment_id") val commentId: Int,
    @SerialName("creator_id") val creatorId: Int,
    @SerialName("creator_profile_image_url") val creatorProfileImageUrl: String?,
    @SerialName("creator_nickname") val creatorNickname: String,
    ...
)

참고: creatorProfileImageUrl은 서버가 null을 줄 수 있으니 String? 고려도 추천.

Also applies to: 12-26, 28-41

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

20-21: 미사용 파라미터 제거: onSendReply는 이 컴포저블에서 사용되지 않습니다.

  • 실제 사용은 BottomSheet에서 일어나고, 여기서는 콜백을 전달만 하지 않습니다. 파라미터 제거로 API를 간결하게 유지하세요. 프리뷰도 정리 필요.
 fun CommentSection(
-    commentItem: CommentList,
-    onSendReply: (String, Int?, String?) -> Unit = { _, _, _ -> },
+    commentItem: CommentList,
     onReplyClick: (commentId: Int, nickname: String) -> Unit,
     onEvent: (CommentsEvent) -> Unit = { _ -> }
 )

@@
-            ),
-            onSendReply = { _, _, _ -> },
+            ),
             onReplyClick = { commentId, nickname ->
                 // Handle reply click
             }

Also applies to: 79-83

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

90-99: 접근성(보이스오버) 개선 제안.

  • 좋아요 아이콘/버튼에 대한 contentDescription 또는 semantics { role = Role.Button } 등의 보강을 권장합니다. 현재 contentDescription = null이라 스크린리더 노출이 되지 않습니다. 상태(좋아요 여부, 개수) 파악 가능하도록 간단한 설명을 추가해 주세요.

예시:

  • Icon의 contentDescription에 “좋아요”, “좋아요 취소” 등 상태 기반 라벨 제공
  • 클릭 영역의 Column에 semantics { role = Role.Button } 부여
app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt (2)

473-479: LaunchedEffect 키를 postId로 좁혀 불필요한 재초기화 방지.

  • 객체 전체를 키로 사용하면 얕은 변경에도 재실행됩니다. postId/postType만 키로 쓰거나 selectedPostForComment?.let{} 블록으로 감싸는 편이 안전합니다.
-    LaunchedEffect(selectedPostForComment) {
-        commentsViewModel.initialize(
-            postId = selectedPostForComment!!.postId.toLong(),
-            postType = selectedPostForComment!!.postType
-        )
-    }
+    LaunchedEffect(selectedPostForComment?.postId, selectedPostForComment?.postType) {
+        selectedPostForComment?.let {
+            initializeComments(
+                postId = it.postId.toLong(),
+                postType = it.postType
+            )
+        }
+    }

481-497: 댓글 전송 이벤트 라우팅 OK, 단 중복 방지 가드 유지 권장.

  • text.isNotBlank() 체크 후 CommentsEvent.CreateComment 디스패치 흐름이 명확합니다. 상위/하위에서 이중 체크가 생기지 않도록 일관된 레이어 하나에만 검증을 두는 것도 고려하세요.
app/src/main/java/com/texthip/thip/ui/group/note/component/CommentBottomSheet.kt (1)

132-145: 무한 스크롤 트리거 임계값 도입으로 초기 과호출 방지.

  • 화면에 아이템이 1~2개인 초기 상태에도 “마지막 아이템이 보인다”로 간주되어 즉시 LoadMore가 발생할 수 있습니다. 1개 이상 프리페치 여유를 두어 과호출을 줄이세요.
-            val lastVisibleItemIndex = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
-            lastVisibleItemIndex >= layoutInfo.totalItemsCount - 1
+            val lastVisibleItemIndex = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1
+            // 끝에서 2개 이내로 접근 시 로드 (프리페치)
+            lastVisibleItemIndex >= layoutInfo.totalItemsCount - 2
app/src/main/java/com/texthip/thip/data/repository/CommentsRepository.kt (2)

14-52: 중복된 runCatching + handleBaseResponse 패턴 공통화 제안

세 메서드 모두 동일한 예외 처리/응답 래핑 패턴을 사용합니다. 재사용 가능한 safeApiCall 헬퍼로 추출하면 중복이 줄고 유지보수가 쉬워집니다.

예시 요약:

  • private suspend fun safeApiCall(block: suspend () -> BaseResponse): Result = runCatching { block().handleBaseResponse().getOrThrow() }
  • 각 메서드에서 safeApiCall { commentsService.xxx(...) } 형태로 단순화.

14-24: 반환 타입 명시로 가독성 향상

현재 반환 타입이 타입 추론에 의존합니다. Repository의 퍼블릭 API는 Result<...>를 명시하면 호출부 가독성과 유지보수성이 좋아집니다.

예: suspend fun getComments(...): Result<CommentsResponse> 형태로 명시.

Also applies to: 26-34, 36-52

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

60-78: 댓글 작성: 성공 시 전체 새로고침 대신 부분 갱신 준비

현재 서버 응답이 준비되면 단일 항목만 추가하도록 계획이라고 명시되어 있습니다. 응답을 받는 즉시 목록 최상단 prepend(또는 parentId 매칭 시 해당 쓰레드 아래에 append)하는 로직을 바로 수용할 수 있게 구조를 잡아두면 리렌더 비용을 줄일 수 있습니다. 또한 전송 중 상태(isCreating)도 UI에 노출하는 편이 UX에 도움이 됩니다.

원하시면 서버 응답 DTO에 맞춰 단일 삽입/스레드 삽입 로직과 isCreating/에러 초기화 처리까지 포함한 패치 제안을 드리겠습니다.


142-147: 불필요한 네트워크 호출 방지: 다음 커서(null) 시 LoadMore 차단

추가 로딩 시 nextCursor == null이면 첫 페이지 재호출이 일어날 수 있습니다. 방어적으로 막아주세요.

변경 제안:

-        if (currentState.isLoading || currentState.isLoadingMore || (currentState.isLast && !isRefresh)) {
+        if (currentState.isLoading || currentState.isLoadingMore ||
+            (currentState.isLast && !isRefresh) ||
+            (!isRefresh && nextCursor == null)
+        ) {
             return
         }

60-78: 동일 항목 연속 토글과 응답 순서 역전(race) 대응

낙관적 업데이트는 UX에 좋지만, 같은 항목에 대해 빠르게 여러 번 토글하면 이전 요청의 늦은 응답이 최신 상태를 덮어쓸 수 있습니다. 항목별 pending-like 세트를 두고, 요청 중에는 추가 토글을 막거나, 응답에 correlation id를 부여해 최신 상태만 반영하도록 개선을 권장합니다.

원하시면 pendingLikeIds 기반의 간단한 차단 로직 또는 응답 시퀀스 번호를 활용한 최신성 보장 패치를 제안드리겠습니다.

Also applies to: 96-106, 130-140, 148-176

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 667bca2 and e8cc4e4.

📒 Files selected for processing (27)
  • app/src/main/AndroidManifest.xml (1 hunks)
  • app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt (2 hunks)
  • app/src/main/java/com/texthip/thip/data/model/comments/request/CommentsCreateRequest.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/model/comments/request/CommentsLikesRequest.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/model/comments/response/CommentsCreateResponse.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/model/comments/response/CommentsLikesResponse.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/model/comments/response/CommentsResponse.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsPlayingResponse.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsUsersResponse.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/repository/CommentsRepository.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/service/CommentsService.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/ui/common/header/ProfileBar.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/ui/common/header/ProfileBarFeed.kt (2 hunks)
  • app/src/main/java/com/texthip/thip/ui/feed/screen/FeedCommentScreen.kt (2 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/note/component/CommentBottomSheet.kt (4 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/note/component/CommentItem.kt (3 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/note/component/CommentSection.kt (3 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/screen/GroupNoteScreen.kt (5 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/CommentsViewModel.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/room/component/GroupRoomHeader.kt (3 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/room/component/GroupRoomMatesList.kt (3 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomMatesScreen.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomScreen.kt (2 hunks)
  • app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt (5 hunks)
  • app/src/main/java/com/texthip/thip/utils/color/HexToColor.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/utils/rooms/AdvancedImePadding.kt (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (6)
app/src/main/java/com/texthip/thip/ui/group/room/component/GroupRoomMatesList.kt (1)
app/src/main/java/com/texthip/thip/utils/color/HexToColor.kt (1)
  • hexToColor (6-13)
app/src/main/java/com/texthip/thip/ui/group/note/component/CommentBottomSheet.kt (4)
app/src/main/java/com/texthip/thip/ui/common/bottomsheet/CustomBottomSheet.kt (1)
  • CustomBottomSheet (44-129)
app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/CommentsViewModel.kt (1)
  • onEvent (48-58)
app/src/main/java/com/texthip/thip/ui/group/note/component/CommentSection.kt (1)
  • CommentSection (17-55)
app/src/main/java/com/texthip/thip/ui/theme/Theme.kt (1)
  • ThipTheme (41-69)
app/src/main/java/com/texthip/thip/ui/group/room/component/GroupRoomHeader.kt (1)
app/src/main/java/com/texthip/thip/utils/color/HexToColor.kt (1)
  • hexToColor (6-13)
app/src/main/java/com/texthip/thip/ui/group/note/component/CommentSection.kt (2)
app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/CommentsViewModel.kt (1)
  • onEvent (48-58)
app/src/main/java/com/texthip/thip/ui/group/note/component/ReplyItem.kt (1)
  • ReplyItem (28-109)
app/src/main/java/com/texthip/thip/ui/group/note/component/CommentItem.kt (2)
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/group/note/component/ReplyItem.kt (1)
app/src/main/java/com/texthip/thip/utils/color/HexToColor.kt (1)
  • hexToColor (6-13)
🔇 Additional comments (25)
app/src/main/java/com/texthip/thip/ui/common/header/ProfileBar.kt (1)

35-35: 기능 변화 없음 확인 — 기본값 유지로 OK

Line 35의 TODO 제거만 이뤄져 동작 변화는 없습니다. 기본값으로 colors.NeonGreen 유지되는 점 문제 없습니다.

app/src/main/AndroidManifest.xml (1)

20-20: IME 대응을 위한 설정 추가 적절

adjustResize 추가로 키보드 표시 시 레이아웃 리사이즈가 보장되어 댓글 입력 플로우와 잘 맞습니다. AdvancedImePadding과 함께 충돌 소지도 없어 보입니다.

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

5-8: 서버 응답 키/타입 일치 여부 확인 필요

  • 백엔드가 snake_case(예: comment_id)를 사용한다면 @SerialName이 필요합니다.
  • commentId 범위가 Int를 넘을 가능성이 있다면 Long으로의 상향도 검토해주세요.
app/src/main/java/com/texthip/thip/ui/common/header/ProfileBarFeed.kt (1)

29-34: ProfileBarFeed 호출부에서 Painter 인자 잔여 없음

프로파일 이미지 파라미터를 모두 String? 타입으로 전달하고 있어, 기존에 Painter를 넘기던 호출부는 발견되지 않았습니다.
추가 수정이나 검토가 필요하지 않습니다.

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

103-118: 프리뷰 데이터에 aliasColor 추가 변경사항 LGTM

프리뷰가 실제 데이터 모델과 일치하도록 보강된 점 좋습니다. 런타임에서는 빈 문자열/형식 오류 시의 대체 색상만 일관되게 처리되면 충분합니다.

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

61-61: 프리뷰의 aliasColor 예시값 추가 LGTM

프리뷰가 실제 렌더링 상태를 더 가깝게 보여주도록 개선되었습니다.

Also applies to: 69-69

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

15-15: 새로운 aliasColor 필드 추가가 잘 구현되었습니다.

사용자 별명에 색상을 적용하기 위한 필드가 적절히 추가되었으며, 관련 UI 컴포넌트들과 잘 연동될 것으로 보입니다.

app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt (5)

209-209: 동적 roomId 사용으로 변경된 것이 좋습니다.

하드코딩된 roomId = 1 대신 route에서 받은 동적 roomId를 사용하도록 개선되어 실제 룸 컨텍스트에 맞는 화면 초기화가 가능해졌습니다.


228-228: 일관된 동적 roomId 전달이 잘 구현되었습니다.

GroupRoomMatesScreen에도 동적 roomId가 올바르게 전달되어 룸별 멤버 정보를 정확히 표시할 수 있게 되었습니다.


251-251: GroupNoteScreen의 동적 roomId 연동이 적절합니다.

댓글 기능과 연관된 GroupNoteScreen에서 올바른 roomId를 받아 댓글 시스템 초기화에 필요한 룸 컨텍스트를 제공하고 있습니다.


286-286: 노트 생성 화면의 roomId 전달이 정확합니다.

GroupNoteCreateScreen에 동적 roomId가 전달되어 특정 룸에 대한 노트 생성이 가능해졌습니다.


307-307: 투표 생성 화면의 roomId 연동이 완료되었습니다.

GroupVoteCreateScreen에도 동적 roomId가 올바르게 전달되어 룸별 투표 생성 기능이 정상 작동할 것입니다.

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

15-15: 카테고리 색상 필드 추가가 적절합니다.

UI에서 카테고리별 색상 구분을 위한 categoryColor 필드가 잘 추가되었으며, 관련 컴포넌트들에서 활용될 준비가 되었습니다.

app/src/main/java/com/texthip/thip/utils/color/HexToColor.kt (2)

4-4: androidx.core.graphics.toColorInt 사용으로 개선되었습니다.

Android의 Color.parseColor 대신 androidx.core.graphics.toColorInt를 사용하여 더 안정적이고 현대적인 방식으로 헥스 색상을 변환하도록 개선되었습니다.


8-8: 색상 변환 로직이 올바르게 수정되었습니다.

hex.toColorInt()를 사용하여 Color 객체를 생성하는 방식으로 변경되어 Compose UI와 더 잘 호환됩니다.

app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt (2)

4-4: CommentsService import가 추가되었습니다.

새로운 댓글 서비스에 대한 import가 올바르게 추가되어 DI 바인딩 준비가 완료되었습니다.


41-44: CommentsService DI 바인딩이 적절히 구현되었습니다.

다른 서비스들과 일관된 패턴으로 CommentsService가 바인딩되어 댓글 관련 API 호출을 위한 의존성 주입이 준비되었습니다.

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

165-167: 카테고리 컬러 전달 연동 OK

roomDetails.category와 categoryColor를 Header에 정확히 전달하고 있어 변경된 시그니처와 일치합니다. 미리보기에서도 categoryColor를 제공하고 있어 안정적입니다.

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

39-41: hex 컬러 적용 경로 합리적입니다

categoryColor를 hex로 받아 hexToColor로 바로 적용하는 간결한 흐름으로 정리되어 좋습니다. 서버가 “#” 접두어를 항상 보장하는지만 백엔드와 합의되어 있으면 안정적입니다. (hexToColor는 예외 시 White로 폴백)

Also applies to: 176-176

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

34-36: 좋아요/답글 이벤트 연결 LGTM

  • VM 이벤트(LikeComment, LikeReply)로의 위임이 명확하고, parent/child 식별자 전달도 정확합니다.

Also applies to: 41-49

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

49-54: 데이터 바인딩 이행 정상 (ReplyList → UI).

  • 프로필/닉네임/칭호/색상/작성일 매핑이 모델 필드에 정확히 대응합니다. hexToColor 예외 처리도 적절.
app/src/main/java/com/texthip/thip/ui/group/note/component/CommentBottomSheet.kt (1)

81-92: 상태 기반 렌더링/이벤트 위임 구조 전반 LGTM

  • 로딩/빈 상태/리스트 상태 분기, LoadMoreComments 디스패치, replyTo 상태 관리가 깔끔합니다.
app/src/main/java/com/texthip/thip/data/repository/CommentsRepository.kt (2)

10-13: Hilt/싱글턴 구성은 적절합니다

DI 구성(@singleton, @Inject)과 의존성 주입 구조가 명확합니다.


26-34: 인자 이름 response가 서비스 시그니처와 일치함 — 수정 불필요

확인 결과 CommentsService.likeComment 메서드의 두 번째 파라미터가

@Body response: CommentsLikesRequest

로 선언되어 있어, 레포지토리 쪽의

response = CommentsLikesRequest(type)

사용이 올바릅니다. 제안된 request 네임드 인자로의 변경은 불필요하므로 현재 코드 그대로 유지해 주세요.

  • app/src/main/java/com/texthip/thip/data/service/CommentsService.kt
    @Body response: CommentsLikesRequest
  • app/src/main/java/com/texthip/thip/data/repository/CommentsRepository.kt
    • response = CommentsLikesRequest(type)

Likely an incorrect or invalid review comment.

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

22-27: Int → Long ID 타입 일관성: 전체 레이어 반영 검토 필요

현재 CommentsEventInt → Long으로 변경할 경우, 다음을 비롯해 앱 전반에서 ID 타입 불일치가 발생합니다.
백엔드가 Long 기반 ID를 사용 중이라면, 아래 리스트에 있는 모든 위치를 함께 리팩터링해야 하며, API 계약 변경에 따른 호환성 검증이 필수입니다.

· ViewModel

  • CommentsViewModel 내 이벤트 파라미터 (LikeComment, LikeReply, CreateComment)
  • toggleCommentLike, toggleReplyLike, createComment 메서드 시그니처

· UI 호출부

  • GroupNoteScreen.kt (CommentsEvent.CreateComment(parentId) 호출)
  • CommentBottomSheet.kt (onReplyClick: (commentId: Int, …) 등)
  • CommentSection.kt (onReplyClick, onSendReply, onEvent(CommentsEvent) 등 콜백 시그니처)

· 네트워크 모델

  • CommentsCreateRequest.parentId: Int?
  • CommentsCreateResponse.commentId: Int
  • CommentsLikesResponse.commentId: Int
  • CommentsResponse.CommentList.commentId, ReplyList.commentId

· Repository

  • CommentsRepository 메서드 파라미터 (parentId: Int?)

· Mock / 데이터 모델

  • ReplyData.ktCommentItem.commentId: Int, userId: Int

위 전부를 Long으로 통일할 때, 각 레이어별로 다음 작업이 필요합니다:

  1. 백엔드 API 스펙(ID 타입)을 최종 확인
  2. DTO(request/response) → Repository → ViewModel → UI 콜백 순으로 파라미터 타입 변경
  3. 구버전 클라이언트와의 호환성(마이그레이션) 전략 수립

먼저 백엔드 ID 타입을 확정한 뒤, 범위가 큰 리팩터링인 만큼 단계별로 진행해주세요.

Comment on lines +6 to +11
data class CommentsCreateRequest(
val content: String,
val isReplyRequest: Boolean,
val parentId: Int? = null,
val postType: String,
)
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

ID 타입 불일치: parentId를 Long?으로 통일하세요

네트워크 계층(CommentsService)의 Path 파라미터들은 Long을 사용하고 있습니다. 여기에 매핑되는 parentId만 Int?인 것은 타입 불일치로, 장기적으로 오버플로 및 일관성 저하의 원인이 됩니다. 댓글/답글의 식별자는 전반적으로 Long을 사용하는 쪽으로 통일하는 것을 권장합니다.

적용 예시:

 data class CommentsCreateRequest(
     val content: String,
     val isReplyRequest: Boolean,
-    val parentId: Int? = null,
+    val parentId: Long? = null,
     val postType: String,
 )
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/data/model/comments/request/CommentsCreateRequest.kt
around lines 6 to 11, parentId is declared as Int? while network layer
(CommentsService) uses Long IDs; change parentId to Long? to align types, update
any constructors/usages and serialization/mapping code that create or consume
CommentsCreateRequest to accept/produce Long values, and run/build tests to
ensure no further Int usages remain.

Comment on lines +5 to +9
@Serializable
data class CommentsLikesResponse(
val commentId: Int,
val isLiked: Boolean,
)
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

ID 타입 일관성 검토 필요 — Int → Long로 통일 제안

Response의 commentId가 Int인데, Service 계층에서 commentId 매개변수를 Long으로 사용하는 경우가 많습니다. 도메인 전반에서 ID는 Long으로 통일하는 것을 권장합니다. 또한 서버 필드명이 snake_case(comment_id)라면 @SerialName 지정이 필요합니다.

제안 변경:

-import kotlinx.serialization.Serializable
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.SerialName
 
 @Serializable
 data class CommentsLikesResponse(
-    val commentId: Int,
+    /* 서버가 snake_case를 사용한다면 다음과 같이 지정:
+    @SerialName("comment_id")
+    */
+    val commentId: Long,
     val isLiked: Boolean,
 )

검증 스크립트(서비스 시그니처와 다른 모델의 commentId 타입을 조사):


🏁 Script executed:

#!/bin/bash
# CommentsService의 likeComment 시그니처와 commentId 타입을 확인
echo "=== CommentsService likeComment signatures ==="
rg -n -A 3 -B 2 $'interface\\s+CommentsService|likeComment\\s*\\('

echo
echo "=== Response/Model 내 commentId 타입 현황 ==="
rg -n -A 1 -B 1 $'commentId\\s*:\\s*(Int|Long)'

echo
echo "=== Repository 계층에서 likeComment 사용부 ==="
rg -n -A 3 -B 2 $'likeComment\\s*\\('

Length of output: 11283


🏁 Script executed:

#!/bin/bash
# Search for JSON serializer configuration and naming strategies
echo "=== kotlinx.serialization.json.Json instantiations ==="
rg -n "kotlinx.serialization.json.Json" -A 3 -B 3

echo
echo "=== Any custom Json builders or module installs ==="
rg -n "Json\\s*\\{" -A 3 -B 3

echo
echo "=== SerialName usages across models ==="
rg -n "SerialName"

echo
echo "=== References to naming strategy or snake_case ==="
rg -n "snake" -A 2 -B 2

Length of output: 22080


CommentsLikesResponse.commentId → Long 및 @SerialName("comment_id") 적용 제안

CommentsService와 CommentsRepository 모두 commentId를 Long으로 사용하고, 프로젝트 전반에서 ID를 Long으로 통일하고 있습니다. 또한 NetWorkModule에서 snake_case 자동 변환 설정을 사용하지 않으므로, 서버가 comment_id를 반환할 경우 @SerialName을 붙여야 합니다.

• 대상 파일
• app/src/main/java/com/texthip/thip/data/model/comments/response/CommentsLikesResponse.kt

• 제안 변경사항

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

 @Serializable
 data class CommentsLikesResponse(
-    val commentId: Int,
+    @SerialName("comment_id")
+    val commentId: Long,
     val isLiked: Boolean,
 )
📝 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
@Serializable
data class CommentsLikesResponse(
val commentId: Int,
val isLiked: Boolean,
)
// app/src/main/java/com/texthip/thip/data/model/comments/response/CommentsLikesResponse.kt
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerialName
@Serializable
data class CommentsLikesResponse(
@SerialName("comment_id")
val commentId: Long,
val isLiked: Boolean,
)
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/data/model/comments/response/CommentsLikesResponse.kt
around lines 5 to 9, change the commentId property from Int to Long and annotate
it with @SerialName("comment_id") so the model matches
CommentsService/CommentsRepository ID types and the server's snake_case key;
also add the kotlinx.serialization.SerialName import if missing.

Comment on lines +14 to +24
suspend fun getComments(
postId: Long,
postType: String = "RECORD",
cursor: String? = null,
) = runCatching {
commentsService.getComments(
postId = postId,
postType = postType,
cursor = cursor
).handleBaseResponse().getOrThrow()
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

postType을 String 상수로 노출하지 말고 강타입으로 관리하세요

postType: String = "RECORD"는 오타/변경에 취약합니다. enum class(또는 sealed class)로 강타입화하고 직렬화 시 @SerializedName/@Json 어댑터로 문자열 매핑하는 방식을 권장합니다.

예시(요약):

  • enum class PostType { RECORD, NOTICE, ... }
  • Repository/Service는 postType: PostType을 받고, Retrofit/직렬화에서 문자열로 변환.
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/data/repository/CommentsRepository.kt
around lines 14–24, the function currently exposes postType as a raw String
defaulting to "RECORD", which is error-prone; change the API to accept a
strong-typed PostType (enum or sealed class) instead of String, update the
repository signature to postType: PostType and convert to the wire string at the
serialization boundary (either annotate enum entries with @SerializedName /
@Json or add a Retrofit/Converter adapter) so Retrofit sends the correct string;
update the CommentsService interface and all callers to use PostType and ensure
serialization mapping is added so no runtime string literals remain.

Comment on lines +36 to +52
suspend fun createComment(
postId: Long,
content: String,
isReplyRequest: Boolean,
parentId: Int? = null,
postType: String = "RECORD",
) = runCatching {
commentsService.createComment(
postId = postId,
request = CommentsCreateRequest(
content = content,
isReplyRequest = isReplyRequest,
parentId = parentId,
postType = postType
)
).handleBaseResponse().getOrThrow()
}
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

ID 타입 일관성 검토: parentId(Int?) vs commentId(Long)

createCommentparentId: Int?likeCommentcommentId: Long이 혼재합니다. 서버가 Long ID를 사용한다면 전체 레이어(UI 모델/이벤트, ViewModel, Repository, Service, DTO)를 Long으로 통일하는 것이 안전합니다. 혼재 시 변환 과정에서 overflow/호환성 문제가 생길 수 있습니다.

원본 모델/서비스 시그니처의 타입을 점검한 뒤 통일을 제안드립니다. 검색 스크립트:


🏁 Script executed:

#!/bin/bash
# commentId/parentId 타입 선언을 전역 검색
rg -n -A 2 -B 2 $'commentId\\s*:\\s*(Int|Long)|parentId\\s*:\\s*(Int|Long)' app/src/main/java

Length of output: 8010


🏁 Script executed:

#!/bin/bash
# Search for createComment signature in CommentsService.kt
rg -n "createComment" app/src/main/java/com/texthip/thip/data/service/CommentsService.kt

Length of output: 122


🏁 Script executed:

#!/bin/bash
# Get the definition of createComment in CommentsService.kt with context
rg -n -A3 -B3 "suspend fun createComment" app/src/main/java/com/texthip/thip/data/service/CommentsService.kt

Length of output: 364


ID 타입 통일: 댓글 관련 ID를 모두 Long으로 변경 필요

Service(CommentsService)는 Long을 사용하지만, Request/Response DTO와 ViewModel/UI 이벤트는 Int를 사용하고 있어 변환 과정에서 오버플로우나 호환성 문제가 발생할 수 있습니다. 아래 파일들의 ID 타입을 IntLong(nullable 포함)으로 변경하세요.

• app/src/main/java/com/texthip/thip/data/model/comments/request/CommentsCreateRequest.kt

  • parentId: Int? → Long?
    • app/src/main/java/com/texthip/thip/data/model/comments/response/CommentsCreateResponse.kt
  • commentId: Int → Long
    • app/src/main/java/com/texthip/thip/data/model/comments/response/CommentsLikesResponse.kt
  • commentId: Int → Long
    • app/src/main/java/com/texthip/thip/data/model/comments/response/CommentsResponse.kt
  • CommentList.commentId, ReplyList.commentId 등 → Long
    • app/src/main/java/com/texthip/thip/data/repository/CommentsRepository.kt
  • createComment(parentId: Int?) → Long?
    • app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/CommentsViewModel.kt
  • data class LikeComment(val commentId: Int) → Long
  • data class CreateComment(..., val parentId: Int?) → Long?
    • 앱 레이어(UI 컴포넌트)의 이벤트 핸들러(onReplyClick, onEvent 등) 파라미터 타입 수정

예시 diff:

--- CommentsCreateRequest.kt
-    val parentId: Int? = null,
+    val parentId: Long? = null,

--- CommentsCreateResponse.kt
-    val commentId: Int,
+    val commentId: Long,

위 파일들을 포함해 댓글 ID를 다루는 모든 레이어(UI 모델/이벤트, ViewModel, Repository, Service, DTO)에서 Long으로 일관되게 변경해주세요.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/data/repository/CommentsRepository.kt
around lines 36-52, the createComment signature and internal use still treat
parentId as Int? while the service uses Long; change the repository method
signature to accept parentId: Long? and return Long? (or propagate the service
Long), update the CommentsCreateRequest construction to pass parentId as Long?
(no Int conversions), and ensure the function's return type matches the
DTO/service (Long). Also update all related DTOs, ViewModel event/data classes,
and UI handlers listed in the review to use Long (nullable where applicable) so
ID types are consistent across repository → service → DTO → ViewModel → UI
layers.

Comment on lines +29 to +33
@POST("comments/{postId}")
suspend fun createComment(
@Path("postId") postId: Long,
@Body request: CommentsCreateRequest
): BaseResponse<CommentsCreateResponse>
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

createComment의 타입 일관성 점검(IDs Long 통일)

createComment의 Path는 Long(postId)인데, 요청 바디의 parentId가 현재 Int?로 설계되어 있습니다. 상위 모델(CommentsCreateRequest)의 parentId를 Long?으로 정렬해야 합니다. 상단 파일 코멘트를 참고해 일괄 통일을 권장합니다.


🏁 Script executed:

#!/bin/bash
# Locate the CommentsCreateRequest definition to verify the type of parentId
rg "class CommentsCreateRequest" -n .

Length of output: 165


🏁 Script executed:

#!/bin/bash
# 확인: CommentsCreateRequest 내 parentId 타입 조회
rg "parentId" -n app/src/main/java/com/texthip/thip/data/model/comments/request/CommentsCreateRequest.kt

Length of output: 137


parentId 타입을 Long? 으로 통일해주세요

CommentsService.createComment API의 postId가 Long으로 정의되어 있으니, 요청 바디의 parentId도 Long? 으로 변경해 타입 일관성을 유지해야 합니다.

수정 대상:

  • 파일: app/src/main/java/com/texthip/thip/data/model/comments/request/CommentsCreateRequest.kt
  • 라인 9

제안된 변경:

-    val parentId: Int? = null,
+    val parentId: Long? = null,
📝 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
@POST("comments/{postId}")
suspend fun createComment(
@Path("postId") postId: Long,
@Body request: CommentsCreateRequest
): BaseResponse<CommentsCreateResponse>
// app/src/main/java/com/texthip/thip/data/model/comments/request/CommentsCreateRequest.kt
data class CommentsCreateRequest(
val postId: Long,
val content: String,
val parentId: Long? = null
)
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/data/model/comments/request/CommentsCreateRequest.kt
around line 9, the parentId property is not nullable while the API's postId is
defined as Long; update the parentId type to Long? (make it nullable) so the
request model matches the service expectations; ensure any usages and
constructor/defaults accommodate the nullable type.

Comment on lines +117 to +128
val reply = parentComment.replyList[replyIndex]
val currentIsLiked = reply.isLike
val newLikeCount = if (currentIsLiked) reply.likeCount - 1 else reply.likeCount + 1

// 즉시 UI 업데이트
val updatedReply = reply.copy(isLike = !currentIsLiked, likeCount = newLikeCount)
val newReplyList =
parentComment.replyList.toMutableList().apply { set(replyIndex, updatedReply) }
val updatedParentComment = parentComment.copy(replyList = newReplyList)
val newComments =
comments.toMutableList().apply { set(parentCommentIndex, updatedParentComment) }
_uiState.update { it.copy(comments = newComments) }
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

대댓글 좋아요 수 음수 방지

대댓글도 동일하게 클램프가 필요합니다.

변경 제안:

-        val currentIsLiked = reply.isLike
-        val newLikeCount = if (currentIsLiked) reply.likeCount - 1 else reply.likeCount + 1
+        val currentIsLiked = reply.isLike
+        val newLikeCount = (if (currentIsLiked) reply.likeCount - 1 else reply.likeCount + 1).coerceAtLeast(0)
📝 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 reply = parentComment.replyList[replyIndex]
val currentIsLiked = reply.isLike
val newLikeCount = if (currentIsLiked) reply.likeCount - 1 else reply.likeCount + 1
// 즉시 UI 업데이트
val updatedReply = reply.copy(isLike = !currentIsLiked, likeCount = newLikeCount)
val newReplyList =
parentComment.replyList.toMutableList().apply { set(replyIndex, updatedReply) }
val updatedParentComment = parentComment.copy(replyList = newReplyList)
val newComments =
comments.toMutableList().apply { set(parentCommentIndex, updatedParentComment) }
_uiState.update { it.copy(comments = newComments) }
val reply = parentComment.replyList[replyIndex]
val currentIsLiked = reply.isLike
val newLikeCount = (if (currentIsLiked)
reply.likeCount - 1
else
reply.likeCount + 1
).coerceAtLeast(0)
// 즉시 UI 업데이트
val updatedReply = reply.copy(isLike = !currentIsLiked, likeCount = newLikeCount)
val newReplyList =
parentComment.replyList.toMutableList().apply { set(replyIndex, updatedReply) }
val updatedParentComment = parentComment.copy(replyList = newReplyList)
val newComments =
comments.toMutableList().apply { set(parentCommentIndex, updatedParentComment) }
_uiState.update { it.copy(comments = newComments) }
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/CommentsViewModel.kt
around lines 117 to 128, the reply like count can go negative when unliking;
clamp the computed newLikeCount to never drop below zero. Change the like count
calculation to compute the tentative value (reply.likeCount - 1 or +1) and then
apply a max(0, tentativeValue) before creating updatedReply, so the UI state
update uses the non-negative likeCount.

Comment on lines +133 to +139
_uiState.update {
val originalComments = it.comments.toMutableList()
originalComments[parentCommentIndex] = parentComment
it.copy(comments = originalComments)
}
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

대댓글 롤백도 id 재탐색으로 안전하게 되돌리기

부모/자식 인덱스가 변했을 수 있으므로 id로 재탐색 후 반영하세요.

변경 제안:

-                    _uiState.update {
-                        val originalComments = it.comments.toMutableList()
-                        originalComments[parentCommentIndex] = parentComment
-                        it.copy(comments = originalComments)
-                    }
+                    _uiState.update {
+                        val list = it.comments.toMutableList()
+                        val pIdx = list.indexOfFirst { c -> c.commentId == parentComment.commentId }
+                        if (pIdx != -1) {
+                            list[pIdx] = parentComment
+                        }
+                        it.copy(comments = list)
+                    }
📝 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
_uiState.update {
val originalComments = it.comments.toMutableList()
originalComments[parentCommentIndex] = parentComment
it.copy(comments = originalComments)
}
}
}
_uiState.update {
val list = it.comments.toMutableList()
val pIdx = list.indexOfFirst { c -> c.commentId == parentComment.commentId }
if (pIdx != -1) {
list[pIdx] = parentComment
}
it.copy(comments = list)
}
}
}
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/CommentsViewModel.kt
around lines 133-139, the code updates parent/child comments by using stored
indexes which may be stale; instead, locate the parent and child by their ids
before applying the rollback. Find the parent index with comments.indexOfFirst {
it.id == parentComment.id } and if found, replace that item; if a child rollback
is needed, locate the child within the parent's replies by id (e.g.,
parent.replies.indexOfFirst { it.id == childComment.id }) and replace it
immutably, then update the top-level comments list with the modified parent
object; handle not-found cases gracefully (no-op or log) to avoid
IndexOutOfBoundsExceptions.

Comment on lines +148 to +156
viewModelScope.launch {
_uiState.update {
if (isRefresh) it.copy(isLoading = true, comments = emptyList(), isLast = false)
else it.copy(isLoadingMore = true)
}

val cursorToFetch = if (isRefresh) null else nextCursor

commentsRepository.getComments(postId = currentPostId, cursor = cursorToFetch)
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

리프레시 시작 시 nextCursor와 에러 상태 초기화

리프레시 시 커서를 리셋하고, 이전 에러 메시지를 지워야 UI가 깔끔합니다.

변경 제안:

-            _uiState.update {
-                if (isRefresh) it.copy(isLoading = true, comments = emptyList(), isLast = false)
-                else it.copy(isLoadingMore = true)
-            }
+            _uiState.update {
+                if (isRefresh) it.copy(isLoading = true, comments = emptyList(), isLast = false, error = null)
+                else it.copy(isLoadingMore = true, error = null)
+            }
+            if (isRefresh) nextCursor = null
📝 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
viewModelScope.launch {
_uiState.update {
if (isRefresh) it.copy(isLoading = true, comments = emptyList(), isLast = false)
else it.copy(isLoadingMore = true)
}
val cursorToFetch = if (isRefresh) null else nextCursor
commentsRepository.getComments(postId = currentPostId, cursor = cursorToFetch)
viewModelScope.launch {
_uiState.update {
if (isRefresh) it.copy(isLoading = true, comments = emptyList(), isLast = false, error = null)
else it.copy(isLoadingMore = true, error = null)
}
if (isRefresh) nextCursor = null
val cursorToFetch = if (isRefresh) null else nextCursor
commentsRepository.getComments(postId = currentPostId, cursor = cursorToFetch)

Comment on lines +156 to +171
commentsRepository.getComments(postId = currentPostId, cursor = cursorToFetch)
.onSuccess { response ->
if (response != null) {
nextCursor = response.nextCursor
_uiState.update {
it.copy(
isLoading = false,
isLoadingMore = false,
// isRefresh일 경우 새 목록으로, 아닐 경우 기존 목록에 추가
comments = if (isRefresh) response.commentList else it.comments + response.commentList,
isLast = response.isLast
)
}
}
}
.onFailure { throwable ->
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

postType 미전달 버그 + null 응답 시 로딩 플래그 해제 누락

  • Repository의 getCommentspostType을 전달하지 않아 항상 "RECORD"로 호출됩니다.
  • onSuccess에서 response == null이면 로딩 플래그가 풀리지 않아 UI가 영구 로딩 상태가 됩니다.

변경 제안:

-            commentsRepository.getComments(postId = currentPostId, cursor = cursorToFetch)
+            commentsRepository.getComments(
+                postId = currentPostId,
+                postType = currentPostType,
+                cursor = cursorToFetch
+            )
                 .onSuccess { response ->
-                    if (response != null) {
-                        nextCursor = response.nextCursor
-                        _uiState.update {
-                            it.copy(
-                                isLoading = false,
-                                isLoadingMore = false,
-                                // isRefresh일 경우 새 목록으로, 아닐 경우 기존 목록에 추가
-                                comments = if (isRefresh) response.commentList else it.comments + response.commentList,
-                                isLast = response.isLast
-                            )
-                        }
-                    }
+                    if (response != null) {
+                        nextCursor = response.nextCursor
+                        _uiState.update {
+                            it.copy(
+                                isLoading = false,
+                                isLoadingMore = false,
+                                comments = if (isRefresh) response.commentList else it.comments + response.commentList,
+                                isLast = response.isLast
+                            )
+                        }
+                    } else {
+                        // null 응답이라도 로딩 상태는 반드시 해제
+                        _uiState.update { it.copy(isLoading = false, isLoadingMore = false) }
+                    }
                 }
                 .onFailure { throwable ->
                     _uiState.update {
                         it.copy(isLoading = false, isLoadingMore = false, error = throwable.message)
                     }
                 }
📝 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
commentsRepository.getComments(postId = currentPostId, cursor = cursorToFetch)
.onSuccess { response ->
if (response != null) {
nextCursor = response.nextCursor
_uiState.update {
it.copy(
isLoading = false,
isLoadingMore = false,
// isRefresh일 경우 새 목록으로, 아닐 경우 기존 목록에 추가
comments = if (isRefresh) response.commentList else it.comments + response.commentList,
isLast = response.isLast
)
}
}
}
.onFailure { throwable ->
commentsRepository.getComments(
postId = currentPostId,
postType = currentPostType,
cursor = cursorToFetch
)
.onSuccess { response ->
if (response != null) {
nextCursor = response.nextCursor
_uiState.update {
it.copy(
isLoading = false,
isLoadingMore = false,
comments = if (isRefresh) response.commentList else it.comments + response.commentList,
isLast = response.isLast
)
}
} else {
// null 응답이라도 로딩 상태는 반드시 해제
_uiState.update { it.copy(isLoading = false, isLoadingMore = false) }
}
}
.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/note/viewmodel/CommentsViewModel.kt
around lines 156 to 171, the call to commentsRepository.getComments omits the
postType parameter (causing a default "RECORD" call) and the onSuccess branch
fails to clear loading flags when response == null; update the getComments
invocation to pass the current postType (e.g., postType = currentPostType) and
modify the onSuccess null branch to update _uiState to set isLoading = false and
isLoadingMore = false (and keep other state unchanged) so the UI exits loading
state when the response is null.

Comment on lines +18 to +28
fun Modifier.advancedImePadding() = composed {
var consumePadding by remember { mutableStateOf(0) }
onGloballyPositioned { coordinates ->
val rootCoordinate = coordinates.findRootCoordinates()
val bottom = coordinates.positionInWindow().y + coordinates.size.height

consumePadding = (rootCoordinate.size.height - bottom).toInt().coerceAtLeast(0)
}
.consumeWindowInsets(PaddingValues(bottom = (consumePadding / LocalDensity.current.density).dp))
.imePadding()
} No newline at end of file
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

consumeWindowInsets는 실제 패딩을 추가하지 않습니다 → padding으로 변경 필요

지금 구현은 onGloballyPositioned로 계산한 consumePadding을 레이아웃에 반영하지 않고, imePadding만 적용되고 있습니다. consumeWindowInsets(PaddingValues(...))는 하위로 전달되는 인셋을 “소비”할 뿐, 실제 패딩을 추가하진 않습니다. 의도대로라면 계산된 값을 padding(bottom = ...)으로 적용해야 합니다.

아래와 같이 수정하면 계산된 여백이 실제로 반영됩니다.

-fun Modifier.advancedImePadding() = composed {
-    var consumePadding by remember { mutableStateOf(0) }
-    onGloballyPositioned { coordinates ->
-        val rootCoordinate = coordinates.findRootCoordinates()
-        val bottom = coordinates.positionInWindow().y + coordinates.size.height
-
-        consumePadding = (rootCoordinate.size.height - bottom).toInt().coerceAtLeast(0)
-    }
-        .consumeWindowInsets(PaddingValues(bottom = (consumePadding / LocalDensity.current.density).dp))
-        .imePadding()
-}
+fun Modifier.advancedImePadding() = composed {
+    val density = LocalDensity.current
+    var bottomPaddingDp by remember { mutableStateOf(0.dp) }
+    onGloballyPositioned { coordinates ->
+        val rootCoordinate = coordinates.findRootCoordinates()
+        val bottom = coordinates.positionInWindow().y + coordinates.size.height
+        val px = (rootCoordinate.size.height - bottom).toInt().coerceAtLeast(0)
+        bottomPaddingDp = with(density) { px.toDp() }
+    }
+        .padding(bottom = bottomPaddingDp)
+        .imePadding()
+}

추가 import (파일 상단에 필요):

import androidx.compose.foundation.layout.padding
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/utils/rooms/AdvancedImePadding.kt around
lines 18-28, the code currently calls consumeWindowInsets(...) which only
consumes insets and does not apply visual padding; replace the
.consumeWindowInsets(PaddingValues(bottom = (consumePadding /
LocalDensity.current.density).dp)) call with .padding(bottom = (consumePadding /
LocalDensity.current.density).dp) so the computed bottom spacing is actually
applied to the layout, keep .imePadding() as needed, and add the import
androidx.compose.foundation.layout.padding at the top of the file.

Copy link
Collaborator

@rbqks529 rbqks529 left a comment

Choose a reason for hiding this comment

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

댓글 로직 확인했습니다!

Copy link
Collaborator

Choose a reason for hiding this comment

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

혹시 이거는 어떤 기능인가요?

}
}

private fun toggleReplyLike(parentCommentId: Int, replyId: Int) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

위와 동일하네요 확실히 서버에서 오는 응답이 느릴 수 도 있으니 ui를 업데이트 먼저하고 요청을 보내고 실패시 되돌리는 로직이 좋네요

import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp

Copy link
Collaborator

Choose a reason for hiding this comment

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

키보드가 올라왔을때 패딩이 추가되는거라는데 실제로 잘 동작하나요?

@rbqks529 rbqks529 merged commit bac1dc2 into THIP-TextHip:develop Aug 15, 2025
1 check passed
This was referenced Aug 18, 2025
@coderabbitai coderabbitai bot mentioned this pull request Aug 30, 2025
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-256)]

2 participants