Skip to content

Conversation

@Nico1eKim
Copy link
Member

@Nico1eKim Nico1eKim commented Aug 16, 2025

➕ 이슈 링크


🔎 작업 내용

  • 특정 유저의 공개 피드 조회 api 연결
  • 기록 핀하기 api 연결
  • 모임방 -> 책 상세 이동 로직 구현
  • 오늘의 한마디 작성 api 연결
  • 댓글 삭제 api 연결
  • 기록 총평으로 보이게 수정
  • 투표 삭제 api 연결 + 투표한 항목만 바뀌도록 수정

📸 스크린샷

특정 유저의 공개 피드 조회

KakaoTalk_Video_2025-08-16-22-40-01.mp4

기록 핀하기

KakaoTalk_Video_2025-08-16-22-40-06.mp4

오늘의 한마디 작성

KakaoTalk_Video_2025-08-16-22-40-10.mp4

댓글 + 답글 삭제

KakaoTalk_Video_2025-08-16-22-40-13.mp4

투표 삭제 + 투표한 항목만 바뀌도록 수정

KakaoTalk_Video_2025-08-16-22-40-17.mp4

😢 해결하지 못한 과제

  • 피드의 댓글 쪽은 규빈이 코드 머지돼야 작업 가능할듯요?


📢 리뷰어들에게

  • 피드 쪽에서 많은 코드가 수정돼서 꽤 많은 컨플릭트가 발생할거같은데 .. 해결하기 두려우면 같이 디코 켜서 수정하는거도 찬성입니다

Summary by CodeRabbit

  • 신기능

    • 타인 피드 화면 추가: 프로필/팔로워 정보와 게시글 목록 조회, 새 카드(OthersFeedCard)로 표시.
    • 그룹룸 채팅 화면 도입: 오늘의 한마디 등록 지원.
    • 그룹노트에서 기록 고정 후 피드 작성으로 바로 이동(도서·내용 자동 채움).
  • 개선

    • 댓글/대댓글 길게 누르기 메뉴(작성자 삭제/신고), 삭제된 댓글 표시, 확인 다이얼로그 및 문구 추가.
    • 하단시트 배경 딤 처리, 구분선/이미지 미리보기 등 피드/노트 UI 개선.
    • 구독 바 데이터 갱신 및 아이콘 교체.
  • 내비게이션

    • 그룹룸에서 책 상세/채팅으로 이동.
    • 구독 목록에서 사용자 프로필로 이동.
    • 피드 작성이 도서/내용 파라미터를 받아 사전 입력.

@coderabbitai
Copy link

coderabbitai bot commented Aug 16, 2025

Walkthrough

여러 데이터 모델 추가/변경, 서비스/레포지토리 API 확장(댓글 삭제, 특정 사용자 피드, 방 투표 삭제/핀/데일리그리팅), 피드 타 사용자 화면 MVVM 도입, 그룹 노트/룸 화면 내 내비게이션 및 상호작용 흐름 추가, 내비게이션 라우트/익스텐션 확장, UI 컴포넌트 소폭 수정과 리소스/테마 보강을 포함합니다.

Changes

Cohort / File(s) Summary
Comments API + ViewModel/UI
data/model/comments/response/CommentsDeleteResponse.kt, .../CommentsResponse.kt, data/service/CommentsService.kt, data/repository/CommentsRepository.kt, ui/group/note/viewmodel/CommentsViewModel.kt, ui/group/note/component/CommentBottomSheet.kt, ui/group/note/component/CommentSection.kt, ui/group/note/component/CommentItem.kt, ui/group/note/component/ReplyItem.kt
댓글 삭제 응답 모델 추가, 댓글/대댓글 모델에 isWriter 및 다수 필드 nullable, DELETE API/레포 추가. ViewModel에 DeleteComment 이벤트와 LikeReply payload 변경, 낙관적 삭제/롤백 로직 추가. UI에 롱프레스 메뉴(삭제/신고 분기), 키 생성 보강, 삭제 표시/빈값 처리 및 콜백 시그니처 업데이트.
Feed: 특정 사용자 피드
data/model/feed/response/FeedUsersInfoResponse.kt, .../FeedUsersResponse.kt, data/service/FeedService.kt, data/repository/FeedRepository.kt, ui/feed/viewmodel/FeedOthersViewModel.kt, ui/feed/screen/FeedOthersScreen.kt, ui/feed/component/OthersFeedCard.kt
사용자 프로필/피드 응답 모델 추가, GET 엔드포인트 2개와 레포 메서드 2개 추가. Others ViewModel/화면 신설(병렬 로드, 상태 구독). 카드 컴포넌트 추가.
Users: 최근 팔로잉 피드
data/model/users/response/UsersMyFollowingsRecentFeedsResponse.kt, .../users/response/MyFollowingsResponse.kt, data/service/UserService.kt, data/repository/UserRepository.kt, ui/feed/viewmodel/FeedViewModel.kt, ui/feed/screen/FeedScreen.kt, ui/feed/component/MySubscribelistBar.kt
최근 작성자 타입 제거/대체, 신규 응답/서비스/레포 메서드로 교체. FeedViewModel 갱신/리프레시 추가. FeedScreen에서 뷰모델 단일화 및 구독바 데이터 소스 교체. 구독바 컴포넌트가 RecentWriterList 사용.
Rooms: 투표 삭제/핀/데일리그리팅
data/model/rooms/request/RoomsDailyGreetingRequest.kt, .../response/RoomsDailyGreetingResponse.kt, .../RoomsDeleteVoteResponse.kt, .../RoomsRecordsPinResponse.kt, .../RoomsVoteResponse.kt, .../RoomsPostsResponse.kt, data/service/RoomsService.kt, data/repository/RoomsRepository.kt
방 API: 투표 삭제/핀 조회/데일리그리팅 요청 추가, 관련 모델 추가. RoomsVoteResponse 구조 변경(voteItems 리스트). PostList에 isOverview 추가. 레포에 대응 메서드 3개 추가.
Group: 노트/룸/채팅 흐름
ui/group/note/viewmodel/GroupNoteViewModel.kt, ui/group/note/screen/GroupNoteScreen.kt, ui/group/note/component/TextCommentCard.kt, ui/group/note/component/VoteCommentCard.kt, ui/group/note/screen/GroupNoteCreateScreen.kt, ui/group/note/screen/GroupVoteCreateScreen.kt, ui/group/room/viewmodel/GroupRoomChatViewModel.kt, ui/group/room/screen/GroupRoomChatScreen.kt, ui/group/room/component/GroupRoomBody.kt, ui/group/room/screen/GroupRoomScreen.kt
그룹 노트: 사이드이펙트(NavigateToFeedWrite), 삭제/핀/새 이벤트, 오버레이/확인 다이얼로그. 카드들에 isOverview 반영. 툴팁 오프셋 조정. 그룹 채팅 ViewModel/화면 신설(데일리그리팅 전송). 룸 바디에 isbn 및 내비 콜백 추가. 룸 스크린에서 새 콜백 전달.
Navigation 확장
ui/navigator/routes/FeedRoutes.kt, .../GroupRoutes.kt, ui/navigator/extensions/FeedNavigationExtensions.kt, .../GroupNavigationExtensions.kt, ui/navigator/navigations/FeedNavigation.kt, .../GroupNavigation.kt, MainNavHost.kt
FeedRoutes.Write에 파라미터 추가, Others 경로 추가, MySubscription 제거. GroupRoutes.RoomChat 추가. 피드/그룹 내비 빌더가 navigateBack 콜백 사용. Feed Write 라우트 데이터 프리필, Others 스크린 연결, 사용자 프로필/그룹 채팅으로 이동 익스텐션 추가. MainNavHost에서 navigateBack 주입.
UI/Theme/리소스 보강
ui/common/bottomsheet/CustomBottomSheet.kt, ui/common/cards/CardNote.kt, ui/common/header/AuthorHeader.kt, ui/feed/screen/FeedCommentScreen.kt, app/src/main/res/drawable/ic_search_character.xml, app/src/main/res/values/strings.xml, ui/theme/Color.kt
바텀시트 딤 추가(Black30). CardNote 퍼센트 Int로 변경. 헤더/디바이더 색상 등 경미한 스타일 수정. 아이콘/문자열 리소스 추가(삭제 문구 등). 테마 컬러 Black30 및 팔레트 확장.
Mock/정리
ui/group/note/mock/CommentData.kt
그룹 노트 목 데이터 파일 삭제.

Sequence Diagram(s)

sequenceDiagram
  actor U as User
  participant S as FeedOthersScreen
  participant VM as FeedOthersViewModel
  participant R as FeedRepository
  participant FS as FeedService

  U->>S: 화면 진입
  S->>VM: collectAsState()
  VM->>R: getFeedUsersInfo(userId) + getFeedUsers(userId)
  R->>FS: GET /feeds/users/{userId}/info
  R->>FS: GET /feeds/users/{userId}
  FS-->>R: BaseResponse(UserInfo)
  FS-->>R: BaseResponse(FeedUsersResponse)
  R-->>VM: Result<UserInfo>, Result<Feeds>
  VM-->>S: uiState 업데이트
Loading
sequenceDiagram
  actor U as User
  participant CB as CommentBottomSheet
  participant VM as CommentsViewModel
  participant Repo as CommentsRepository
  participant Svc as CommentsService

  U->>CB: 댓글 롱프레스 → 삭제 선택
  CB->>VM: CommentsEvent.DeleteComment(commentId)
  VM->>VM: UI 낙관적 업데이트(제거/마킹)
  VM->>Repo: deleteComment(commentId)
  Repo->>Svc: DELETE /comments/{commentId}
  Svc-->>Repo: BaseResponse(CommentsDeleteResponse)
  Repo-->>VM: Result<Unit>
  VM-->>CB: 성공 유지 또는 실패 롤백
Loading
sequenceDiagram
  actor U as User
  participant G as GroupNoteScreen
  participant VM as GroupNoteViewModel
  participant RR as RoomsRepository
  participant RS as RoomsService
  participant Nav as NavController

  U->>G: RECORD 핀 선택 → 확인
  G->>VM: GroupNoteEvent.OnPinRecord(recordId, content)
  VM->>RR: getRoomsRecordsPin(roomId, recordId)
  RR->>RS: GET /rooms/{roomId}/records/{recordId}/pin
  RS-->>RR: BaseResponse(RoomsRecordsPinResponse)
  RR-->>VM: Result<PinInfo>
  VM-->>G: SideEffect.NavigateToFeedWrite(pinInfo, content)
  G->>Nav: navigateToFeedWrite(...payload)
Loading
sequenceDiagram
  actor U as User
  participant C as GroupRoomChatScreen
  participant VM as GroupRoomChatViewModel
  participant RR as RoomsRepository
  participant RS as RoomsService

  U->>C: 메시지 입력 후 전송
  C->>VM: postDailyGreeting(content)
  VM->>RR: postRoomsDailyGreeting(roomId, content)
  RR->>RS: POST /rooms/{roomId}/daily-greeting
  RS-->>RR: BaseResponse(RoomsDailyGreetingResponse)
  RR-->>VM: Result<Response>
  VM-->>C: Event.ShowToast / SubmissionSuccess
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

Assessment against linked issues

Objective Addressed Explanation
특정 사용자 피드 상단 화면, 특정 사용자 피드 조회 (#90)
댓글 삭제 (#90)
투표 삭제 (#90)

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
그룹 채팅 기능 추가 및 데일리그리팅 전송 흐름 (ui/group/room/viewmodel/GroupRoomChatViewModel.kt, ui/group/room/screen/GroupRoomChatScreen.kt) 이 PR 이슈의 범위(특정 사용자 피드/댓글삭제/투표삭제)와 직접 관련 없음.
내비게이션 전반 리팩터링과 back 콜백 주입 (ui/navigator/navigations/*, MainNavHost.kt) 요구사항과 무관한 내비 구조/콜백 변경.
CardNote 퍼센트 타입 변경(Int) (ui/common/cards/CardNote.kt) 피드/댓글/투표 요구사항과 무관한 UI API 변경.
테마 컬러 Black30 추가 및 바텀시트 딤 적용 (ui/theme/Color.kt, ui/common/bottomsheet/CustomBottomSheet.kt) 기능 요구사항과 직접적인 연관 없음.
그룹 노트/생성 화면 툴팁 위치 오프셋 조정 (ui/group/note/screen/GroupNoteCreateScreen.kt, GroupVoteCreateScreen.kt) 이슈 목표와 무관한 UI 위치 조정.

Possibly related issues

Possibly related PRs

Suggested reviewers

  • JJUYAAA
  • rbqks529

Poem

토끼는 딤을 켜고 살짝 숨었지,
다른 이의 피드길 살금살금 뛰지!
댓글은 길게 눌러 “삭제” 한 번에,
투표도 쓱쓱 정리, 핀은 글에 얹네.
채팅에 안부 전해, 귀는 쫑긋 반짝!
오늘도 코드 들판에 당근 씨앗 착! 🥕✨

Tip

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

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

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

🪧 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: 12

🔭 Outside diff range comments (3)
app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt (1)

98-134: 동일 타임스탬프에 의한 임시 파일명 충돌 가능 — 업로드 이미지가 덮어써질 리스크

루프 내에서 System.currentTimeMillis()만으로 파일명을 생성하면 동일 ms 내 여러 이미지가 같은 파일명을 가질 수 있습니다. 이 경우 마지막 이미지로 덮어써져 멀티파트에 동일 파일이 첨부되는 심각한 오류가 발생할 수 있습니다. 고유 식별자(예: nanoTime/UUID) 추가를 권장합니다. 또한 최신 이미지 MIME(webp/heic/heif) 대응도 함께 보강하세요.

-            val mimeType = context.contentResolver.getType(uri) ?: "image/jpeg"
+            val mimeType = context.contentResolver.getType(uri) ?: "image/jpeg"
             val extension = when (mimeType) {
                 "image/png" -> "png"
                 "image/gif" -> "gif"
-                "image/jpeg", "image/jpg" -> "jpg"
+                "image/jpeg", "image/jpg" -> "jpg"
+                "image/webp" -> "webp"
+                "image/heic" -> "heic"
+                "image/heif" -> "heif"
                 else -> "jpg" // 기본값
             }
 
             // 파일명 생성
-            val fileName = "feed_image_${System.currentTimeMillis()}.$extension"
+            val uniqueSuffix = System.nanoTime()
+            val fileName = "feed_image_${System.currentTimeMillis()}_${uniqueSuffix}.$extension"
             val tempFile = File(context.cacheDir, fileName)

또한 예외 처리에서 printStackTrace() 대신 프로젝트 로거 사용을 권장합니다.

-        } catch (e: Exception) {
-            e.printStackTrace()
-            null
+        } catch (e: Exception) {
+            // Timber.e(e, "이미지 파트 생성 중 오류: uri=%s", uri)
+            android.util.Log.e("FeedRepository", "이미지 파트 생성 중 오류: uri=$uri", e)
+            null
         }
app/src/main/java/com/texthip/thip/ui/feed/component/MySubscribelistBar.kt (1)

55-59: maxVisibleCount 음수 케이스 가드 필요

아주 작은 화면 너비에서 (maxWidthPx - 36f)가 음수가 될 수 있고, 이 경우 take(maxVisibleCount)에 음수가 전달되면 IllegalArgumentException이 발생합니다. 0 이상으로 클램핑해 주세요.

-        val maxVisibleCount = ((maxWidthPx - 36f) / imageWithSpacingPx).toInt()
+        val maxVisibleCount = maxOf(0, ((maxWidthPx - 36f) / imageWithSpacingPx).toInt())
app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomChatScreen.kt (1)

108-169: mockMessages 의존 제거 및 전달된 chatMessages로 렌더링 통일

현재 UI가 mockMessages를 기반으로 그려지고 있어 실제 데이터 바인딩이 이루어지지 않습니다. 아래와 같이 chatMessages로 일원화해 주세요.

-            if (mockMessages.isEmpty()) {
+            if (chatMessages.isEmpty()) {
                 ...
             } else {
                 LazyColumn(
                     reverseLayout = true,
                     modifier = Modifier.weight(1f),
                     verticalArrangement = Arrangement.spacedBy(20.dp, Alignment.Bottom)
                 ) {
-                    itemsIndexed(mockMessages) { index, message ->
+                    itemsIndexed(chatMessages) { index, message ->
                         val isNewDate = when {
-                            index == mockMessages.lastIndex -> true
-                            mockMessages[index + 1].date != message.date -> true
+                            index == chatMessages.lastIndex -> true
+                            chatMessages[index + 1].date != message.date -> true
                             else -> false
                         }
                         val isBottomItem = index == 0
                         ...
                     }
                 }
             }
🧹 Nitpick comments (78)
app/src/main/java/com/texthip/thip/ui/common/header/AuthorHeader.kt (2)

92-93: 버튼 크기 지정 방식 소폭 정리 제안

동작상 문제는 없지만, width/height 체이닝 대신 size(width, height) 한 번으로 표현하면 간결합니다. 텍스트 길이가 변동될 여지가 있다면 높이는 고정, 너비는 최소값 보장 형태(height + widthIn)도 고려해볼 수 있어요.

권장 예시:

-    modifier = Modifier
-        .width(buttonWidth)
-        .height(33.dp),
+    modifier = Modifier.size(buttonWidth, 33.dp),

105-108: 문자열 연결 대신 단일 리소스 포맷으로 국제화(i18n) 개선 제안

두 개의 stringResource를 +로 연결하면 언어별 어순/공백 처리에 취약합니다. 하나의 문자열 리소스에 포맷 파라미터를 사용해 조합하는 편이 안전합니다.

국지 변경(diff):

-                    text = stringResource(
-                        R.string.thip_num,
-                        thipNum
-                    ) + stringResource(R.string.thip_ing),
+                    text = stringResource(R.string.thip_num_ing, thipNum),

참고:

  • 위 변경은 R.string.thip_num_ing 리소스(예: "%1$d%s" 혹은 "%1$d %2$s") 추가/수정이 선행되어야 합니다.
  • 또는 stringResource(R.string.thip_num_and_ing, thipNum, stringResource(R.string.thip_ing))처럼 2개의 포맷 인수를 갖는 리소스를 정의하는 방법도 좋습니다.
app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupVoteCreateScreen.kt (2)

129-135: 툴팁이 아이콘과 겹칠 수 있음 — 아이콘 하단 기준 + 여백으로 오프셋 계산 권장

기존 +32.dp 보정값을 제거하면서, 툴팁이 아이콘과 맞닿거나 겹칠 가능성이 있습니다. 아이콘의 실제 높이를 반영해 하단 기준으로 약간의 여백(예: 8.dp)을 더해 배치하면 기기/해상도별 일관성이 좋아집니다. 또한 LayoutCoordinates가 detach된 상황을 대비해 isAttached 가드를 추가해 주세요.

아래와 같이 보완하는 것을 권장합니다:

-            val yOffsetDp = with(density) {
-                iconCoordinates.value!!.positionInRoot().y.toDp()
-            }
+            val yOffsetDp = with(density) {
+                val coords = iconCoordinates.value!!
+                if (!coords.isAttached) 0.dp else {
+                    val iconBottomY = coords.positionInRoot().y + coords.size.height
+                    iconBottomY.toDp() + 8.dp
+                }
+            }
  • 추가 확인 포인트:
    • 다양한 크기의 폰/태블릿과 글꼴 배율에서 아이콘과 툴팁이 시각적으로 자연스러운지.
    • 스크롤/키보드 열림 등 레이아웃 변화 후에도 위치가 안정적으로 유지되는지.

127-145: 중복된 툴팁 배치 로직 공통화 고려

GroupNoteCreateScreen과 동일한 패턴의 툴팁 배치 코드가 반복됩니다. Anchor LayoutCoordinates를 받아 오프셋을 계산해주는 작은 유틸/컴포저블(예: AnchorPopup 또는 computeAnchorYOffset)을 추출하면 유지보수성이 올라갑니다.

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

116-122: 32.dp 보정 제거에 따른 정렬 변화 — 아이콘 하단 기준 + 여백으로 더 안정적인 배치 제안

y 오프셋에서 고정 보정값을 제거한 변경 방향 자체는 합리적이지만, 아이콘과 툴팁 간 미세한 겹침/충돌이 발생할 여지가 있습니다. 아이콘의 실제 높이를 고려해 하단에 맞춘 뒤 소량 여백을 두면 안전합니다. 또한 좌표가 detach된 경우를 대비해 가드도 추가해 주세요.

-            val yOffsetDp = with(density) {
-                iconCoordinates.value!!.positionInRoot().y.toDp()
-            }
+            val yOffsetDp = with(density) {
+                val coords = iconCoordinates.value!!
+                if (!coords.isAttached) 0.dp else {
+                    val iconBottomY = coords.positionInRoot().y + coords.size.height
+                    iconBottomY.toDp() + 8.dp
+                }
+            }
  • QA 권장: 다양한 DPI/글자 크기 및 다크/라이트 테마에서 아이콘과 툴팁 간 간격이 자연스러운지 확인해주세요.

114-132: 툴팁 배치 로직 재사용성 개선

동일한 툴팁 배치 로직이 다른 화면과 반복됩니다. 배치 계산을 공통 함수로 추출하거나 PopupModal에 anchor 좌표를 직접 전달해 내부에서 오프셋 계산을 맡기는 API 확장을 고려해 보세요.

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

7-8: 서버 응답 누락 대비 기본값과 명시적 필드 매핑(@SerialName) 제안

서버가 구버전 스키마를 반환하거나 필드를 누락하는 상황에 대비해 기본값을 두는 편이 안전합니다. 또한 백엔드가 snake_case(vote_items)를 쓸 가능성이 있다면 @SerialName으로 명시하는 것이 안전합니다.

아래처럼 기본값과 @SerialName을 추가하는 것을 제안합니다.

-    val voteItems: List<VoteItems>,
+    @SerialName("voteItems") // 서버에서 snake_case를 쓴다면 "vote_items"로 변경
+    val voteItems: List<VoteItems> = emptyList(),

추가로 필요한 import:

import kotlinx.serialization.SerialName

7-8: 클래스명 복수형(VoteItems) 사용으로 인한 의미 중복/혼동 가능성

프로퍼티가 voteItems: List<VoteItems> 형태로 “복수형의 리스트”가 되어 가독성이 떨어집니다. 관례적으로 아이템 타입은 단수형(VoteItem), 컬렉션 프로퍼티명은 복수형(voteItems)을 권장합니다.

가능하다면 타입을 단수형으로 리네이밍해 일관성을 높여주세요.

-    val voteItems: List<VoteItems>,
+    val voteItems: List<VoteItem>,

참고: 이 변경은 타입 정의 및 전역 참조 리네이밍이 수반되므로 별도 커밋에서 일괄 적용을 권장합니다.

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

42-62: 프리필(setPinnedRecord) 로직은 적절합니다. 다만 콘텐츠 길이 제한(2000자) 보강과 파라미터 구조 개선을 권장합니다.

  • 현재 setPinnedRecordupdateFeedContent의 2000자 제한 로직을 우회해 바로 상태에 반영합니다. 네비게이션에서 전달되는 recordContent가 2000자를 초과해도 그대로 들어갈 수 있습니다. 동일한 제약을 적용해 일관성을 유지하는 편이 안전합니다.
  • 네비게이션 익스텐션이 모두 nullable 파라미터를 받는 반면, 본 메서드는 non-null 파라미터를 강제합니다. 호출 측에서 null 여부를 선행 검증하는 구조라면 괜찮지만, 방어적으로 처리하거나 오버로드(또는 BookData 한 개 파라미터)로 단순화하는 방안을 고려해 주세요.
  • BookDataui.group.makeroom.mock 경로에 위치하는 것은 프로덕션 VM에서 “mock” 타입 의존으로 보이는 코드 스멜입니다. 공용(shareable) 모델 위치로 이동하거나 도메인/feature 모델로 승격하는 것을 제안드립니다.

아래처럼 길이 제한 및 트림 반영을 권장합니다.

 fun setPinnedRecord(
     isbn: String,
     bookTitle: String,
     bookAuthor: String,
     bookImageUrl: String,
     recordContent: String
 ) {
     val pinnedBook = BookData(
         title = bookTitle,
         imageUrl = bookImageUrl,
         author = bookAuthor,
         isbn = isbn
     )
     updateState {
         it.copy(
             selectedBook = pinnedBook,
             isBookPreselected = true,
-            feedContent = recordContent
+            feedContent = recordContent.trim().take(2000)
         )
     }
 }

추가 확인 요청:

  • 네비게이션에서 일부 값이 null일 수 있는 시나리오에서 본 메서드는 어떻게 호출되는지(호출 조건/가드) 확인 부탁드립니다.
app/src/main/java/com/texthip/thip/ui/common/bottomsheet/CustomBottomSheet.kt (2)

66-66: 스크림(배경) 추가는 👍. 애니메이션/뒤로가기 연동을 소폭 보완해보면 좋아요

시각적으로는 OK입니다. 사용성 측면에서 스크림 알파를 시트 애니메이션 진행도와 연동하고, 시스템 뒤로가기(Back)로도 닫히게 하면 UX가 좋아집니다.

아래처럼 스크림 알파를 진행도와 연동:

-            .background(color = colors.Black30)
+            .background(color = colors.Black30.copy(alpha = scrimAlpha))

그리고 컴포저블 상단 어딘가에서 scrimAlpha 계산을 추가:

// 추가: 스크림 알파 애니메이션 (파일 상단 import 필요: androidx.compose.animation.core.animateFloatAsState, androidx.compose.runtime.getValue)
val progress by remember {
    derivedStateOf {
        // 0f(숨김) ~ 1f(완전 노출)
        (1f - (animatableOffset.value / BOTTOM_SHEET_HIDDEN_OFFSET)).coerceIn(0f, 1f)
    }
}
val scrimAlpha by animateFloatAsState(
    targetValue = if (isDismissing) 0f else progress,
    label = "scrimAlpha"
)

뒤로가기 지원도 권장:

// import androidx.activity.compose.BackHandler
BackHandler(enabled = true) {
    if (!isDismissing) {
        isDismissing = true
        scope.launch {
            animatableOffset.animateTo(BOTTOM_SHEET_HIDDEN_OFFSET, tween(ANIMATION_DURATION))
            onDismiss()
        }
    }
}

123-124: 내부 클릭 흡수는 필요하지만, 불필요한 립플 제거를 권장합니다

빈 clickable로 클릭을 흡수하면 립플이 보일 수 있습니다. indication을 끄고 interactionSource를 지정해 립플을 없애는 것이 깔끔합니다.

-                .clickable(enabled = true) {} // 내부 클릭 무시되지 않도록
+                .clickable(
+                    indication = null,
+                    interactionSource = remember { MutableInteractionSource() }
+                ) {} // 내부 클릭 무시되지 않도록(립플 제거)
app/src/main/res/values/strings.xml (2)

234-235: 삭제 모달 카피 추가 👍. 키 네이밍 패턴만 살짝 정렬하면 더 깔끔합니다

문구는 자연스럽습니다. 다만 기존 feed 삭제 키는 ..._dialog_title/description 패턴을, 여기 기록 삭제 키는 ..._title/content 패턴을 사용하고 있어 혼재됩니다. 장기적으로 키 네이밍을 통일하면 리소스 검색과 유지보수에 유리합니다.

  • 후보:
    • delete_post_contentdelete_post_description (feed와 동일한 접미사)
    • 또는 일관된 도메인 네이밍(예: post vs record)을 확정

변경 시 참조 코드 일괄 치환이 필요하니, 이번 PR에서는 유지하고 다음 정리 PR에서 일괄 적용하는 것도 방법입니다. 원하시면 리소스 키 정리 표를 만들어 드릴게요.


239-241: 댓글 삭제 카피 OK. 확인 문구(paragraph)도 제공하면 UX가 명확해집니다

댓글 삭제도 feed/기록 삭제처럼 확인 문구(설명) 쌍을 두면 재사용성이 올라갑니다.

아래처럼 설명 키를 추가하는 방안을 고려해 주세요:

     <string name="delete_comment_title">이 댓글을 삭제하시겠어요?</string>
     <string name="comment_deleted">삭제된 댓글이에요.</string>
+    <string name="delete_comment_content">삭제 후에는 되돌릴 수 없어요.</string>
app/src/main/res/drawable/ic_search_character.xml (1)

6-21: 벡터 색상 하드코딩 → 다크 모드/테마 대응 방안 고려

현재 경로별 색상이 하드코딩(#6868FF, #ffffff, #FEFEFE)입니다. 디자인 의도라면 OK지만, 다크 모드 또는 테마 변경 대응이 필요하면 아래 중 하나를 추천합니다.

  • drawable-night에 동일 벡터를 두고 색상만 조정하는 나이트 전용 리소스 제공
  • 색상을 @color 리소스로 분리하고 테마에서 값을 바인딩
  • 사용처(Compose Image 등)에서 tint를 적용하는 단색 아이콘으로 단순화(여기처럼 다중 색이면 권장 X)

원하시면 색상 리소스 분리안 또는 night 리소스 초안을 만들어 드리겠습니다.

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

7-16: FeedUsersInfoResponse: 서버 스키마 타입·널러빌리티 재확인 필요

아래 항목들에 대해 서버 계약(API 스펙)과 일치하는지 다시 확인해주세요.
간단한 예시 diff도 첨부했습니다.

– File: app/src/main/java/com/texthip/thip/data/model/feed/response/FeedUsersInfoResponse.kt (Lines 7–16)

• ID 타입 통일
API 호출 파라미터가 userId: Long인데, 모델이 creatorId: Int이면 범위 오버플로우나 불일치 가능성이 있습니다.
Long으로 변경하거나, API 쪽을 Int로 내리겠다는 확약이 필요합니다.

• 문자열 필드 널러빌리티/디폴트
서버가 항상 profileImageUrl, aliasName, aliasColor를 반환하는지 확인하세요.
누락 가능성이 있다면 String?으로 만들거나, 기본값(= "")을 지정하는 것이 안전합니다.

• 팔로워 이미지 리스트 보장
latestFollowerProfileImageUrls가 “빈 배열”을 보장하는지 확인하세요.
안 할 경우 List<String>? 또는 = emptyList() 디폴트가 필요합니다.

예시 diff:

 data class FeedUsersInfoResponse(
-    val creatorId: Int,
+    val creatorId: Long,

-    val profileImageUrl: String,
+    val profileImageUrl: String? = null,

-    val aliasName: String,
+    val aliasName: String? = null,

-    val aliasColor: String,
+    val aliasColor: String? = null,

-    val latestFollowerProfileImageUrls: List<String>
+    val latestFollowerProfileImageUrls: List<String> = emptyList()
 )

위 변경 중 어떤 방향이든, 서버 계약과 일치하도록 확정한 후 적용하세요.

app/src/main/java/com/texthip/thip/ui/common/cards/CardNote.kt (3)

88-88: Progress fraction 안전화(0..1로 클램핑)

percentage가 0~100 범위를 벗어나면 Compose가 기대하지 않는 fraction이 전달될 수 있습니다. coerceIn으로 안전하게 보정하는 편이 좋습니다.

아래 한 줄 교체 제안:

-                    .fillMaxWidth(fraction = (percentage / 100f).toFloat())
+                    .fillMaxWidth(fraction = percentage.coerceIn(0, 100) / 100f)

69-77: 국제화(i18n) 및 접근성 측면의 문자열 결합 방식 개선 제안

숫자와 기호(%)를 분리 렌더링하면 언어권에 따라 어색할 수 있습니다. "30%" 같은 포맷 문자열 리소스를 하나로 사용하는 방식을 고려해 주세요(예: %1$d%%).

가능하다면 strings.xml에 "percent_format"을 추가하고:

-            Text(
-                text = percentage.toString(),
-                style = typography.smalltitle_sb600_s16_h20,
-                color = colors.Purple,
-            )
-            Text(
-                text = stringResource(R.string.percent),
-                style = typography.menu_sb600_s12,
-                color = colors.Purple,
-            )
+            Text(
+                text = stringResource(R.string.percent_format, percentage), // 예: "%1$d%%"
+                style = typography.smalltitle_sb600_s16_h20,
+                color = colors.Purple,
+            )

53-56: 장식 아이콘의 contentDescription 정리

체브론은 장식적 요소일 확률이 높습니다. 스크린리더 중복 낭독을 피하려면 contentDescription = null로 두거나(완전 장식인 경우), stringResource로 현지화된 설명을 제공해 주세요.

-            Icon(
-                painter = painterResource(R.drawable.ic_chevron),
-                contentDescription = "Chevron Icon",
-                tint = colors.Grey02,
-            )
+            Icon(
+                painter = painterResource(R.drawable.ic_chevron),
+                contentDescription = null, // 장식용 아이콘이면 null 권장
+                tint = colors.Grey02,
+            )
app/src/main/java/com/texthip/thip/data/model/users/response/UsersMyFollowingsRecentFeedsResponse.kt (1)

10-15: 단일 엔티티명에 ‘List’ 접미사는 혼동 유발 — 이름 정리 제안

RecentWriterList는 개별 항목인데 List 접미사가 붙어 혼란을 줍니다. RecentWriter로의 리네이밍을 제안합니다.

리네이밍(파일 내 변경 예시):

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

 @Serializable
-data class RecentWriterList(
+data class RecentWriter(
     val userId: Long,
     val nickname: String,
     val profileImageUrl: String
 )

참고: 이 변경은 서비스/레포지토리/UI에 걸친 참조 일괄 치환이 필요합니다.

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

14-21: UI 널 처리 누락 다수 확인 — Presentation 계층 매핑 점검 필요

스크립트(휴리스틱) 결과, CommentsResponse의 nullable 필드를 직접 사용하면서 안전 호출·기본값 대체 없이 넘기는 UI 코드가 다수 확인되었습니다.
하지만 전역 rg 탐색이라 댓글용 Response 이외의 모델 호출도 섞여 있을 수 있어, 아래 매핑 지점을 중심으로 Presentation 계층에서의 널 처리(Elvis, 기본값 매핑 등) 유무를 반드시 검증해야 합니다.

점검 포인트:

  • Response → Domain → UI 모델 매핑 함수
    • CommentsResponseCommentUiModel 등으로 변환하는 곳에서
      • commentId, creatorNickname, content, postDate 등 nullable 필드를 기본값("", "-" 등) 또는 적절 placeholder로 매핑했는지
  • 삭제된 댓글 처리 분기
    • isDeleted=truecontent=null"삭제된 댓글입니다" 등으로 대체하는지
  • 주요 UI 컴포넌트 호출부
    • CommentSection, TextCommentCard, VoteCommentCard 등에 직접 data.content, data.creatorNickname를 넘길 때
      • 내부에서 null-safe하게 처리하는지(컴포저블 파라미터 타입이 nullable인지, 기본 값을 할당하는지)

예시:

// before (nullable 직전 전달)
Text(text = comment.content)

// after (매핑단 처리 예시)
Text(text = comment.content ?: "")

리뷰 코멘트에서 제안한 스크립트는 범용 탐색용이므로, 실제 매핑 계층(Repository/Mapper/UseCase/ViewModel)에서 내려오는 UiModel을 확인하시고 누락된 널 대체 로직을 추가해주세요.

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

155-165: 항목 전체 클릭 처리와 내부 버튼의 겹침 — UX 미스탭 방지 소소한 제안

Row 전체를 clickable로 감싸면 팔로우 버튼 주변에서 실수 탭이 발생할 수 있습니다. AuthorHeader 내부 구조에 따라, 아바타/닉네임 영역에만 onUserClick을 적용하는 방식도 고려해 보세요.

예: AuthorHeader에 onProfileClick 콜백을 추가해 해당 영역에만 클릭을 연결(컴포넌트 분리/역할 명확화).

app/src/main/java/com/texthip/thip/ui/navigator/extensions/GroupNavigationExtensions.kt (1)

83-86: 주석 오타: ‘회면’ → ‘화면’으로 수정

의미 전달을 위해 주석 오타만 정정하면 좋겠습니다.

-// 오늘의 한마디 회면으로 이동
+// 오늘의 한마디 화면으로 이동
 fun NavHostController.navigateToGroupRoomChat(roomId: Int) {
     navigate(GroupRoutes.RoomChat(roomId))
 }
app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomsDailyGreetingRequest.kt (1)

5-8: 요청 DTO 정의 적절 — 후행 콤마 스타일만 정리 제안

동작에는 문제 없으나, 프로젝트 내 다른 데이터 클래스들과의 스타일 일관성을 위해 마지막 파라미터의 후행 콤마 제거를 제안합니다.

 @Serializable
 data class RoomsDailyGreetingRequest(
-    val content: String,
+    val content: String
 )
app/src/main/java/com/texthip/thip/data/service/CommentsService.kt (1)

37-40: 명확성 향상: KDoc로 파라미터 의미를 명시

동일 인터페이스에 GET("comments/{postId}")도 존재하여, 경로는 메서드로 구분되지만 식별자 의미가 혼동될 수 있습니다. KDoc로 commentId가 “댓글의 식별자”임을 명시하면 가독성이 좋아집니다.

 interface CommentsService {
+    /**
+     * 댓글 단건 삭제
+     * @param commentId 댓글 식별자
+     */
     @DELETE("comments/{commentId}")
     suspend fun deleteComment(
         @Path("commentId") commentId: Long
     ): BaseResponse<CommentsDeleteResponse>
 }
app/src/main/java/com/texthip/thip/ui/group/note/component/TextCommentCard.kt (2)

34-38: 페이지 라벨은 문자열 포맷 리소스로 처리하는 것이 현지화에 유리합니다

단순 연결(data.page.toString() + stringResource(R.string.page)) 대신 문자열 포맷 리소스를 사용하면 언어별 어순 차이와 공백 처리 문제를 피할 수 있습니다.

가능하다면 아래처럼 치환(예: " %1$d페이지")을 권장합니다:

-    val pageText = if (data.isOverview) {
-        stringResource(id = R.string.general_review)
-    } else {
-        data.page.toString() + stringResource(R.string.page)
-    }
+    val pageText = if (data.isOverview) {
+        stringResource(R.string.general_review)
+    } else {
+        // strings.xml: <string name="page_with_number">%1$d페이지</string>
+        stringResource(R.string.page_with_number, data.page)
+    }

추가로 미세 최적화로 remember를 사용할 수도 있습니다(선택 사항):

// import androidx.compose.runtime.remember
val pageText = remember(data.isOverview, data.page) {
    if (data.isOverview) stringResource(R.string.general_review)
    else stringResource(R.string.page_with_number, data.page)
}

103-103: 프리뷰 데이터에 isOverview 추가 반영 👍

실제 UI 상태를 가늠하는 데 도움이 됩니다. 필요하면 isOverview=true 케이스도 하나 더 두면 스냅샷 확인이 수월합니다.

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

37-53: 로딩 시작 시 에러 메시지 초기화 권장

이전 실패 메시지가 로딩 중에도 남아있는 상태가 될 수 있습니다. 시작 시 errorMessage를 null로 초기화해 잔상 표시를 방지하세요.

-            _uiState.update { it.copy(isLoading = true) }
+            _uiState.update { it.copy(isLoading = true, errorMessage = null) }

50-52: 예외 메시지 직접 노출은 사용자 경험/보안상 리스크

exception.message는 내부 정보가 포함될 수 있어 UI에 그대로 노출하기엔 부적절할 수 있습니다. 사용자용 메시지 매핑(로컬라이즈된 공통 에러 문구)로 치환을 고려해 주세요.

원하시면 공통 에러 매퍼(예: Network/Server/Unknown으로 구분)와 UiState 매핑 예시를 제안드릴게요.

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

48-50: long-press 제스처 처리: combinedClickable 사용 고려

부모 Column에 pointerInput + detectTapGestures(onLongPress)도 동작은 문제가 없지만, ripple/접근성(세만틱스)까지 포함하는 combinedClickable이 더 일관된 제스처 처리를 제공합니다. 자식의 클릭 가능한 영역과의 충돌도 줄일 수 있습니다.

-        Column(
-            modifier = modifier.pointerInput(Unit) {
-                detectTapGestures(onLongPress = { onLongPress() })
-            },
+        Column(
+            modifier = modifier
+                .combinedClickable(
+                    onClick = {}, // 전체 Row 클릭 액션이 없다면 비워둡니다.
+                    onLongClick = onLongPress
+                ),

참고: combinedClickable 사용 시 import androidx.compose.foundation.combinedClickable 가 필요합니다.


138-138: 프리뷰에 isWriter 필드 반영 확인

모델 변경 사항과 일치합니다. isWriter 사용 UI가 추후 필요하면 해당 상태에 따른 분기(예: 메뉴 노출)를 고려해 주세요.

app/src/main/java/com/texthip/thip/ui/navigator/routes/FeedRoutes.kt (1)

8-15: Write를 data class로 전환 + 페이로드 추가는 확장성 측면에서 타당

기본값(null)로 이전 호출부 부담도 낮습니다. 직렬화 기반 내비게이션에서도 안전합니다. 다만 기존에 object Write 전제의 when 분기/동등성 비교가 있었다면 컴파일 타임에 수정되었는지 확인해 주세요.

필요 시 Write 페이로드가 커질 경우, 딥링크/라우트 길이 제한을 고려해 일부 필드를 저장소/상태로 전달하는 패턴도 대안이 될 수 있습니다.

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

30-38: 특정 사용자 피드 조회: 페이징 파라미터(size, cursor) 지원 추가 제안

사용자 피드 목록 응답(FeedUsersResponse)에 nextCursor/isLast가 존재하므로, 서비스 레벨에서 size, cursor 쿼리 파라미터를 받을 수 있도록 열어두는 것이 확장성에 유리합니다. 기본값을 제공하면 기존 호출부 영향 없이 페이징을 도입할 수 있습니다.

아래처럼 메서드 시그니처를 확장하는 것을 권장합니다:

     @GET("feeds/users/{userId}")
-    suspend fun getFeedUsers(
-        @Path("userId") userId: Long
-    ): BaseResponse<FeedUsersResponse>
+    suspend fun getFeedUsers(
+        @Path("userId") userId: Long,
+        @Query("size") size: Int = 10,
+        @Query("cursor") cursor: String? = null
+    ): BaseResponse<FeedUsersResponse>

추가로, 외부 라인(임포트)에는 아래가 필요합니다:

import retrofit2.http.Query

30-33: 메서드 네이밍 개선 제안

getFeedUsersInfo, getFeedUsers는 “피드의 사용자들”로도 읽힐 수 있어 의미가 다소 모호합니다. getUserFeedInfo, getUserFeeds처럼 “특정 사용자” 주어가 먼저 오는 형태가 가독성에 더 좋습니다. 대규모 리팩터링은 아니므로 후속 PR에서 반영 검토 부탁드립니다.

Also applies to: 35-38

app/src/main/java/com/texthip/thip/ui/navigator/extensions/FeedNavigationExtensions.kt (1)

18-32: 대용량 텍스트(recordContent 등)를 라우트 인자로 전달하는 방식은 위험할 수 있음

책 제목/저자/이미지 URL은 무난하지만, recordContent가 길어질 경우 라우트 직렬화/쿼리 길이 제한 문제에 취약합니다. 일시 데이터는 SavedStateHandle 또는 공유 ViewModel로 전달하고, 라우트에는 최소 식별자만 담는 방식을 권장합니다.

예시: 호출 측

navController.currentBackStackEntry?.savedStateHandle?.set("feedWritePayload", FeedWritePayload(...))
navigate(FeedRoutes.Write())

목적지 측

val payload = navController.previousBackStackEntry?.savedStateHandle?.get<FeedWritePayload>("feedWritePayload")
app/src/main/java/com/texthip/thip/ui/navigator/MainNavHost.kt (2)

17-17: navigateBack 시그니처 일치 여부 확인 (Boolean vs Unit)

navController::popBackStack() -> Boolean입니다. 만약 feedNavigation() -> Unit을 기대한다면 메서드 레퍼런스는 타입이 맞지 않습니다. 아래와 같이 람다로 감싸 반환값을 무시하도록 수정이 필요할 수 있습니다.

-        feedNavigation(navController, navigateBack = navController::popBackStack)
+        feedNavigation(navController, navigateBack = { navController.popBackStack() })

동일한 패턴이 다른 네비게이션 빌더에도 존재한다면 일관 적용을 권장합니다.


25-27: commonNavigation에도 동일 이슈 가능성

commonNavigationnavigateBack 파라미터 타입이 () -> Unit이라면, 동일하게 람다로 감싸 반환값을 무시해야 합니다.

-        commonNavigation(
-            navController = navController,
-            navigateBack = navController::popBackStack
-        )
+        commonNavigation(
+            navController = navController,
+            navigateBack = { navController.popBackStack() }
+        )
app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt (3)

75-87: 이미지 파트 변환 중 예외 삼킴: 사용자 피드백/로깅 보강 필요

mapNotNull 내부에서 예외를 null로만 처리하여 업로드 누락 원인 파악이 어렵습니다. 최소 경고 로그를 남기고, 실패 건수에 따라 사용자에게 안내(예: “일부 이미지는 업로드되지 않았습니다”)를 고려해 주세요.

아래처럼 로깅 추가를 제안합니다(프로젝트 로거에 맞게 Log/Timber 중 택1):

-                    imageUris.mapNotNull { uri ->
-                        try {
-                            uriToMultipartBodyPart(uri, "images", tempFiles)
-                        } catch (e: Exception) {
-                            null
-                        }
-                    }
+                    imageUris.mapNotNull { uri ->
+                        try {
+                            uriToMultipartBodyPart(uri, "images", tempFiles)
+                        } catch (e: Exception) {
+                            // Timber.w(e, "이미지 파트 생성 실패: uri=%s", uri)
+                            android.util.Log.w("FeedRepository", "이미지 파트 생성 실패: uri=$uri", e)
+                            null
+                        }
+                    }

149-154: 반환 타입 명시로 공개 API 안정화

레포지토리 공개 메서드는 명시적 반환 타입을 권장합니다. IDE 자동 추론에 의존하면 추후 내부 구현 변경 시 시그니처 의도 파악이 어려워질 수 있습니다.

-    suspend fun getFeedUsersInfo(userId: Long) = runCatching {
+    suspend fun getFeedUsersInfo(userId: Long): Result<com.texthip.thip.data.model.feed.response.FeedUsersInfoResponse?> = runCatching {
         feedService.getFeedUsersInfo(userId)
             .handleBaseResponse()
             .getOrThrow()
     }

패키지 경로는 이미 import되어 있다면 축약형으로 교체 가능합니다.


155-159: 사용자 피드 목록: 페이징 파라미터 전달 지원

서비스 계층에 size/cursor를 추가했다면, 레포지토리도 동일 파라미터를 노출하여 무한 스크롤 등에서 재사용성을 높이는 것이 좋습니다.

-    suspend fun getFeedUsers(userId: Long) = runCatching {
-        feedService.getFeedUsers(userId)
+    suspend fun getFeedUsers(
+        userId: Long,
+        size: Int = 10,
+        cursor: String? = null
+    ): Result<com.texthip.thip.data.model.feed.response.FeedUsersResponse?> = runCatching {
+        feedService.getFeedUsers(userId, size, cursor)
             .handleBaseResponse()
             .getOrThrow()
     }
app/src/main/java/com/texthip/thip/data/repository/UserRepository.kt (1)

30-34: 공개 API 반환 타입을 명시해 가독성과 일관성을 높여주세요.

동일 파일의 다른 메서드들과 달리 반환 타입을 명시하지 않아 추론에 의존하고 있습니다. 공개 API는 반환 타입을 명시하는 편이 유지보수성과 이진 호환성 측면에서 안전합니다. 또한 새로운 응답 모델(UsersMyFollowingsRecentFeedsResponse)을 드러내면 호출부에서 의도를 더 명확히 이해할 수 있습니다.

적용 제안:

-    suspend fun getMyFollowingsRecentFeeds() = runCatching {
+    suspend fun getMyFollowingsRecentFeeds(): Result<UsersMyFollowingsRecentFeedsResponse?> = runCatching {
         userService.getMyFollowingsRecentFeeds()
             .handleBaseResponse()
             .getOrThrow()
     }

(필요 시) import 추가:

  • import com.texthip.thip.data.model.users.response.UsersMyFollowingsRecentFeedsResponse
app/src/main/java/com/texthip/thip/ui/feed/component/OthersFeedCard.kt (2)

38-39: 미사용 상태 변수 제거

isLiked 상태는 어디에서도 사용되지 않습니다. 불필요한 recomposition 원인이 될 수 있으므로 제거하는 것이 좋습니다.

제거 제안:

-    var isLiked by remember { mutableStateOf(false) }

62-80: 이미지 간 간격은 Row의 spacedBy를 사용하고, 개별 이미지의 end padding은 제거하는 것이 깔끔합니다.

마지막 이미지에도 불필요한 우측 패딩이 들어갑니다. Row의 horizontalArrangement로 간격을 제어하세요.

수정 제안:

-        if (hasImages) {
-            Row(
-                modifier = Modifier
-                    .fillMaxWidth()
-                    .padding(bottom = 16.dp),
-                verticalAlignment = Alignment.CenterVertically,
-            ) {
-                images.take(3).forEach { image ->
-                    AsyncImage(
-                        model = image,
-                        contentDescription = null,
-                        modifier = Modifier
-                            .padding(end = 10.dp)
-                            .size(100.dp),
-                        contentScale = ContentScale.Crop
-                    )
-                }
-            }
-        }
+        if (hasImages) {
+            Row(
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .padding(bottom = 16.dp),
+                verticalAlignment = Alignment.CenterVertically,
+                horizontalArrangement = Arrangement.spacedBy(10.dp)
+            ) {
+                images.take(3).forEach { image ->
+                    AsyncImage(
+                        model = image,
+                        contentDescription = null,
+                        modifier = Modifier.size(100.dp),
+                        contentScale = ContentScale.Crop
+                    )
+                }
+            }
+        }

추가로 필요한 import:

  • import androidx.compose.foundation.layout.Arrangement
app/src/main/java/com/texthip/thip/ui/group/note/component/VoteCommentCard.kt (1)

38-43: i18n 관점에서 페이지 문자열은 서식 문자열 사용을 권장합니다.

숫자와 접미사를 단순 연결하면 언어/서식에 따라 어색할 수 있습니다. stringResource 플레이스홀더를 사용해 주세요.

수정 예시:

-    val pageText = if (data.isOverview) {
-        stringResource(id = R.string.general_review)
-    } else {
-        data.page.toString() + stringResource(R.string.page)
-    }
+    val pageText = if (data.isOverview) {
+        stringResource(id = R.string.general_review)
+    } else {
+        stringResource(R.string.page_with_number, data.page)
+    }

strings.xml 추가 예시(참고):

<string name="page_with_number">%1$d페이지</string>
app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt (1)

59-72: Composable 파라미터의 기본값으로 hiltViewModel()을 사용하면 Preview가 깨집니다 — 래퍼 함수를 분리하세요.

Preview 환경에서는 Hilt 컨텍스트가 없기 때문에 hiltViewModel() 호출이 실패합니다. viewModel 기본값을 제거한 내부 Composable과, 외부에서 hiltViewModel()을 생성해 주입하는 래퍼 Composable로 분리하는 패턴을 권장합니다.

수정 제안(diff: 시그니처 수정):

-fun FeedScreen(
+fun FeedScreen(
     onNavigateToMySubscription: () -> Unit = {},
     onNavigateToFeedWrite: () -> Unit = {},
     nickname: String = "",
     userRole: String = "",
     feeds: List<FeedItem> = emptyList(),
     totalFeedCount: Int = 0,
     selectedTabIndex: Int = 0,
     followerProfileImageUrls: List<String> = emptyList(),
     resultFeedId: Int? = null,
     onResultConsumed: () -> Unit = {},
-    feedViewModel: FeedViewModel = hiltViewModel(),
+    feedViewModel: FeedViewModel,
 ) {

추가(선택) 코드: 외부 래퍼 Composable

@Composable
fun FeedRoute(
    onNavigateToMySubscription: () -> Unit = {},
    onNavigateToFeedWrite: () -> Unit = {},
    nickname: String = "",
    userRole: String = "",
    feeds: List<FeedItem> = emptyList(),
    totalFeedCount: Int = 0,
    selectedTabIndex: Int = 0,
    followerProfileImageUrls: List<String> = emptyList(),
    resultFeedId: Int? = null,
    onResultConsumed: () -> Unit = {},
) {
    val feedViewModel: FeedViewModel = hiltViewModel()
    FeedScreen(
        onNavigateToMySubscription = onNavigateToMySubscription,
        onNavigateToFeedWrite = onNavigateToFeedWrite,
        nickname = nickname,
        userRole = userRole,
        feeds = feeds,
        totalFeedCount = totalFeedCount,
        selectedTabIndex = selectedTabIndex,
        followerProfileImageUrls = followerProfileImageUrls,
        resultFeedId = resultFeedId,
        onResultConsumed = onResultConsumed,
        feedViewModel = feedViewModel
    )
}

Preview에서는 더미/가짜 ViewModel을 만들어 넘기거나, uiState를 분리해 주입하는 전략을 권장합니다.

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

163-168: 레코드 핀 API 경로/메서드 확인이 필요합니다.

  • 경로 일관성: 동일 파일에서 record 관련 다른 엔드포인트는 단수(singular) 'record'를 사용합니다(예: POST rooms/{roomId}/record, DELETE rooms/{roomId}/record/{recordId}). 여기서는 'records' 복수형을 사용하고 있어 오타/불일치 가능성이 있습니다.
  • HTTP 메서드 의미: 'pin'은 일반적으로 상태 변경 액션이므로 POST/PATCH가 더 자연스럽습니다. 현재 GET은 조회 성격으로 보이며, 실제 목적이 "핀 토글"이라면 서버와 메서드/경로를 재확인하는 게 안전합니다. 목적이 "핀 정보 조회"라면 메서드/함수 명칭을 더 명확히(getRecordPinInfo 등) 하는 것을 권장합니다.

확인 용 질문:

  • 서버 스펙에서 정확한 경로는 rooms/{roomId}/record/{recordId}/pin 인가요, rooms/{roomId}/records/{recordId}/pin 인가요?
  • 본 호출의 목적이 상태 변경(핀 설정/해제)인가요, 아니면 현재 핀 정보를 조회하는 것인가요?

필요 시 수정 예시:

-@GET("rooms/{roomId}/records/{recordId}/pin")
-suspend fun getRoomsRecordsPin(
+@GET("rooms/{roomId}/record/{recordId}/pin")
+suspend fun getRoomsRecordPin(
     @Path("roomId") roomId: Int,
     @Path("recordId") recordId: Int
 ): BaseResponse<RoomsRecordsPinResponse>
app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt (2)

251-261: 본문 공백 처리 및 유효성 보강 제안

서버로 공백만 포함된 문자열이 전달되는 것을 방지하기 위해 trim() 적용을 권장합니다. 필요 시 빈 문자열에 대해 빠르게 실패하도록 require로 검증하는 것도 고려해 주세요.

적용 예시:

 suspend fun postRoomsDailyGreeting(
     roomId: Int,
     content: String
 ) = runCatching {
+    val normalized = content.trim()
+    // 서버 정책상 빈 문자열을 허용하지 않는다면 다음 라인 활성화 고려
+    // require(normalized.isNotEmpty()) { "데일리 그리팅 내용이 비어 있습니다." }
     roomsService.postRoomsDailyGreeting(
         roomId = roomId,
         request = RoomsDailyGreetingRequest(
-            content = content
+            content = normalized
         )
     ).handleBaseResponse().getOrThrow()
 }

263-271: 네이밍/HTTP 의미 명확화 및 널 처리 일관성 검토

  • getRoomsRecordsPin은 상태 변경(핀 고정/해제) 동작처럼 보이는데, 메서드명이 GET 의미로 읽힙니다. 팀 내 컨벤션에 맞춰 pinRecord, toggleRecordPin, 또는 HTTP 메서드에 맞춘 postRoomsRecordsPin 등의 명칭을 고려해 주세요.
  • 일부 메서드는 널 응답 시 도메인 친화적 예외를 던지고(예: createRoom), 일부는 그대로 반환합니다. 이 메서드의 응답 널 가능성에 대해 서비스 시그니처가 비널 타입이면 그대로 두고, 널 가능성이 있다면 동일 레벨의 일관된 처리(명시적 예외 또는 널 허용)로 맞추는 것을 권장합니다.
app/src/main/java/com/texthip/thip/ui/feed/component/MySubscribelistBar.kt (1)

53-54: 중첩 clickable 제거로 중복 클릭/리플 방지

상단 BoxWithConstraints와 내부 Row 모두 clickable이 걸려 있어 중복 처리/이중 리플이 발생할 수 있습니다. 한 곳으로 통일해 주세요. 상단 컨테이너만 유지하는 편을 권장합니다.

Row 쪽 clickable 제거 예시:

-                Row(
-                    modifier = Modifier
-                        .fillMaxWidth()
-                        .height(58.dp)
-                        .clickable { onClick() },
+                Row(
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .height(58.dp),
                     verticalAlignment = Alignment.CenterVertically
                 ) {

Also applies to: 84-87

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

44-48: 롱프레스 제스처 처리: combinedClickable로 간결화 제안

pointerInput + detectTapGestures 대신 combinedClickable을 사용하면 클릭/롱클릭 제스처 간 중복 처리 및 상호작용을 간결하게 구성할 수 있습니다. 하위의 개별 clickable과의 제스처 충돌도 완화됩니다.

-        Column(
-            modifier = modifier.pointerInput(Unit) {
-                detectTapGestures(onLongPress = { onLongPress() })
-            },
+        Column(
+            modifier = modifier.combinedClickable(
+                onClick = {}, // 상위 전체 클릭 동작이 없다면 no-op
+                onLongClick = { onLongPress() }
+            ),
             verticalArrangement = Arrangement.spacedBy(12.dp)
         ) {

파일 상단 임포트 추가:

import androidx.compose.foundation.combinedClickable

49-56: 불필요한 단독 표현식 제거

data 단독 라인이 남아 있습니다. 의미 없는 표현식으로 Lint 경고 원인이며 제거해 주세요.

-            data
             ProfileBarFeed(
                 profileImage = data.creatorProfileImageUrl,
                 nickname = data.creatorNickname ?: "",
                 genreName = data.aliasName ?: "",
                 genreColor = hexToColor(data.aliasColor ?: "#FFFFFF"),
                 date = data.postDate ?: ""
             )

86-91: 접근성: 좋아요 아이콘 contentDescription 제공 권장

인터렉티브 요소이므로 상태에 따른 설명을 제공하는 것이 바람직합니다. 문자열 리소스가 없다면 추가를 고려해 주세요.

예시:

-                    Icon(
+                    Icon(
                         painter = painterResource(if (data.isLike) R.drawable.ic_heart_center_filled else R.drawable.ic_heart_center),
-                        contentDescription = null,
+                        contentDescription = if (data.isLike) stringResource(R.string.unlike) else stringResource(R.string.like),
                         tint = Color.Unspecified
                     )

원하시면 strings.xml 업데이트까지 함께 제안드릴게요.

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

12-27: 네이밍 및 역직렬화 내구성 개선 제안

  • 타입 네이밍: 컬렉션 항목 타입명이 FeedList라 컬렉션 필드(feedList)와 혼동될 수 있습니다. FeedItem 등으로 변경을 고려해 주세요.
  • 역직렬화 내구성: contentUrls가 서버에서 null로 오면 역직렬화 실패합니다. 기본값을 두어 방어적으로 처리하는 것을 권장합니다(다른 필드도 서버 계약에 따라 기본값 고려).

예시:

 @Serializable
-data class FeedList(
+data class FeedItem(
     val feedId: Long,
     val postDate: String,
     val isbn: String,
     val bookTitle: String,
     val bookAuthor: String,
     val contentBody: String,
-    val contentUrls: List<String>,
+    val contentUrls: List<String> = emptyList(),
     val likeCount: Int,
     val commentCount: Int,
     val isPublic: Boolean,
     val isSaved: Boolean,
     val isLiked: Boolean,
     val isWriter: Boolean,
 )

그리고 상위 타입에서의 참조도 함께 변경:

- data class FeedUsersResponse (
-     val feedList: List<FeedList>,
+ data class FeedUsersResponse (
+     val feedList: List<FeedItem>,
      val nextCursor: String? = null,
      val isLast: Boolean = false,
 )
app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomChatScreen.kt (6)

54-64: 이벤트 수집 처리: SubmissionSuccess에 대한 후속 동작 정의 권장

ShowToast는 처리되지만 SubmissionSuccess에서는 별도 UI 반응이 없습니다. 예: 입력창 포커스 해제, 키보드 숨김, 상단 안내 배너 표시 등 후속 처리 정의를 고려해 주세요.


78-85: List 대신 도메인 타입 사용으로 타입 안정성 확보

chatMessages: List는 타입 안전성과 가독성을 해칩니다. 사용 중인 메시지 모델(GroupRoomChatData)을 명시적으로 반영하는 것이 좋습니다.

- fun GroupRoomChatContent(
-     chatMessages: List<Any>,
+ fun GroupRoomChatContent(
+     chatMessages: List<GroupRoomChatData>,
     inputText: String,
     onInputTextChanged: (String) -> Unit,
     onSendClick: () -> Unit,
     onNavigateBack: () -> Unit
 ) {

49-53: 초기 리스트 타입 명시

타입 변경 시 컴파일러 추론이 애매해질 수 있습니다. 초기 리스트를 명시적으로 GroupRoomChatData 리스트로 선언해 주세요.

-    val chatMessages = emptyList<Any>()
+    val chatMessages = emptyList<GroupRoomChatData>()

172-177: 전송 버튼 처리: 공백 입력 비활성화 고려

viewModel에서 isBlank를 방지하고 있으나, 사용자 경험 측면에서 UI 레벨에서 전송 버튼 비활성화도 고려해 주세요(불필요한 네트워크 트리거 방지).


181-221: BottomSheet 액션 TODO 처리 가이드

수정/삭제/신고 처리가 TODO로 남아있습니다. 이벤트를 ViewModel로 위임해 일관된 단방향 흐름(MVI/MVVM)을 유지하는 것이 좋습니다. 원하시면 이벤트/레포지토리 연결 코드 골격을 생성해 드릴 수 있습니다.


224-235: 미리보기 업데이트 필요(타입 변경 시)

chatMessages 타입을 도메인 타입으로 변경했다면, Preview에서도 해당 타입으로 맞춰 주세요.

-        GroupRoomChatContent(
-            chatMessages = emptyList(),
+        GroupRoomChatContent(
+            chatMessages = emptyList(),
             inputText = inputText,
             onInputTextChanged = { newText -> inputText = newText },
             onSendClick = {},
             onNavigateBack = {}
         )
app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomChatViewModel.kt (2)

34-36: 이벤트 스트림 버퍼링으로 일시 미구독 시 UI 프리징 예방

MutableSharedFlow는 기본 설정에서 구독자가 없으면 emit가 서스펜드됩니다. 버퍼를 1개 정도 두면 일시적인 미구독 상태에서도 안전합니다.

-    private val _eventFlow = MutableSharedFlow<GroupRoomChatEvent>()
+    private val _eventFlow = MutableSharedFlow<GroupRoomChatEvent>(
+        replay = 0,
+        extraBufferCapacity = 1
+    )

37-51: VM에서 하드코딩된 토스트 메시지 → UI로 이관 권장

ViewModel에서 문자열 리소스 접근이 어려워 하드코딩이 늘어납니다. 이벤트를 의미 단위(예: SubmissionSuccess/Failure(reason))로만 내보내고, 실제 문자열은 UI에서 stringResource로 매핑하는 구조가 유지보수에 유리합니다. 또한 content.trim()을 적용해 공백 처리도 강화해 주세요.

가능한 접근:

  • 이벤트: ShowSubmissionSuccess, ShowSubmissionFailure(messageResId?: Int, fallbackMessage: String?)
  • UI: when(event) { ... stringResource(...) ... }
app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt (2)

521-552: 널 단언(!!) 제거로 안정성 향상 권장

selectedPostForMenu!! 대신 let 또는 안전 호출로 변경하면 NPE 리스크가 제거됩니다.

-    val post = selectedPostForMenu!!
-    val menuItems = if (post.isWriter) {
+    val post = selectedPostForMenu
+    val menuItems = if (post?.isWriter == true) {
         ...
-                    onClick = {
-                        postToDelete = post // 삭제할 포스트 정보를 기억
+                    onClick = {
+                        post?.let { postToDelete = it } // 삭제할 포스트 정보를 기억
                         showDeleteDialog = true
                         selectedPostForMenu = null
                     }

182-184: Content 내부 hiltViewModel 사용으로 Preview 깨짐 가능성

GroupNoteContent 내부에서 hiltViewModel()를 호출하면 Preview 실행 시 크래시가 발생할 수 있습니다. ViewModel을 상위(Screen)에서 주입받아 파라미터로 전달하거나, 인터페이스/컨트랙트 기반으로 UI가 필요한 데이터만 주입받도록 리팩터링을 권장합니다.

원하시면 파라미터/호출부까지 포함한 최소 침습 리팩터링 패치를 제안드릴게요.

app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedOthersViewModel.kt (1)

53-55: 디버그 로그 정리 권장

배포 빌드에서 Log.d는 남지 않도록 Timber 또는 BuildConfig.DEBUG 가드로 감싸 주세요.

-            Log.d("FeedOthersViewModel", "User Info Result: ${userInfoResult.getOrNull()}")
-            Log.d("FeedOthersViewModel", "Fetched Feeds Count: ${fetchedFeeds.size}")
+            if (BuildConfig.DEBUG) {
+                Log.d("FeedOthersViewModel", "User Info Result: ${userInfoResult.getOrNull()}")
+                Log.d("FeedOthersViewModel", "Fetched Feeds Count: ${fetchedFeeds.size}")
+            }
app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/CommentsViewModel.kt (2)

63-101: 댓글/답글 삭제 낙관적 업데이트: 엣지 케이스 및 국제화 보완 제안.

  • 부모 댓글 삭제 시 답글 존재 여부로 제거/isDeleted 토글 로직이 명확하고, 실패 시 롤백도 적절합니다.
  • 다만 다음 보완을 고려해 주세요.
    • replyList가 절대 null이 아닐지 데이터 모델을 재확인해 주세요. null 가능성이 있다면 isNullOrEmpty() 사용이 안전합니다.
    • 에러 메시지 "삭제 실패"를 하드코딩하지 말고 string 리소스로 관리하는 것이 좋습니다. ViewModel에서 키만 전달하고, 표시 책임은 UI로 넘기면 재사용성이 좋아집니다.
    • 동시 진행 중인 다른 UI 갱신(좋아요 토글 등)이 있는 상태에서 삭제 실패 롤백이 전체 목록을 원본으로 되돌리기 때문에 최근 변경분을 덮을 수 있습니다. 필요한 경우, 대상 댓글/답글만 되돌리는 부분 롤백 전략(타겟팅된 업데이트)으로 개선을 고려해 주세요.

150-193: 답글 좋아요 탐색 O(N*M): 성능 및 단순화 여지.

현재는 모든 부모 댓글을 순회하여 대상 답글의 부모/인덱스를 찾습니다. 목록이 커질 경우 클릭-반응 지연이 체감될 수 있습니다. 선택지:

  • (간단) UI 이벤트에 parentCommentId를 다시 포함시켜 O(1)에 가깝게 접근.
  • (현 구조 유지) ViewModel 내에 replyId → (parentIndex, replyIndex) 캐시를 유지하고, 댓글 새로고침 시 갱신.

실패 롤백은 인덱스 기반으로 원복되어 일관적입니다. 필요 시 캐시 방식으로도 동일하게 적용 가능합니다.

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

59-67: 이중 BottomSheet + 블러는 성능 비용이 큼: 블러 제거 또는 스크림만 활용 권장.

CustomBottomSheet 자체가 바깥 스크림을 제공하는데, 그 위에 블러까지 적용하면 저사양 기기에서 스크롤/제스처 지연이 발생할 수 있습니다. 블러를 제거하고 스크림만 활용해도 사용자 인지에는 큰 차이가 없습니다.

적용 예시:

-    Box(
-        if (isOverlayVisible) {
-            Modifier
-                .fillMaxSize()
-                .blur(5.dp)
-        } else {
-            Modifier.fillMaxSize()
-        }
-    ) {
+    Box(Modifier.fillMaxSize()) {

151-169: mock 패키지 의존 제거 권장.

MenuBottomSheetItemui.group.room.mock에서 가져옵니다. 공용 컴포넌트 성격이므로 ui.common.bottomsheet 등의 실제 프로덕션 패키지로 이동하는 것이 구조적으로 적합합니다.


172-180: 신고(Report) TODO 처리 도와드릴 수 있어요.

신고 로직이 TODO 상태입니다. API/디자인이 정해져 있다면 이벤트/리포지토리/피드백 토스트까지 연결 코드 초안 제공 가능합니다. 원하시면 이슈를 만들어 드릴게요.

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

41-50: collectAsStateWithLifecycle 사용 권장.

수명주기 인지 수집으로 Configuration change/백그라운드 시 안전성을 높일 수 있습니다.

-import androidx.compose.runtime.collectAsState
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
 ...
-    val uiState by viewModel.uiState.collectAsState()
+    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

69-73: 에러 상태 표시 보완 제안.

현재 isLoading/userInfo != null 외 상태(예: 네트워크 실패)일 경우 화면이 비어 보일 수 있습니다. uiState.errorMessage가 있다면 상단 토스트/인라인 문구로 피드백을 추가해 주세요.


82-92: 팔로우/언팔로우 TODO 연결 도와드릴 수 있어요.

버튼 텍스트는 토글되지만 동작이 비어 있습니다. ViewModel 이벤트/리포지토리 호출/낙관적 UI 업데이트 설계를 함께 정리해 드릴 수 있습니다.

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

76-78: SharedFlow 기본 설정으로 emit가 suspend될 수 있음 — 버퍼/overflow 설정 및 tryEmit 권장

현재 MutableSharedFlow 가 기본값(replay 0, 버퍼 0)이라 수집자가 연결되지 않은 순간에 emit가 호출되면 코루틴이 suspend되어 흐름이 멈출 수 있습니다. 내비게이션 같은 1회성 사이드이펙트는 버퍼를 주고 tryEmit을 사용하는 패턴이 안전합니다.

적용 예시(해당 범위 내 변경):

-    private val _sideEffect = MutableSharedFlow<GroupNoteSideEffect>()
-    val sideEffect = _sideEffect.asSharedFlow()
+    private val _sideEffect = MutableSharedFlow<GroupNoteSideEffect>(
+        replay = 0,
+        extraBufferCapacity = 1,
+        onBufferOverflow = BufferOverflow.DROP_OLDEST
+    )
+    val sideEffect = _sideEffect.asSharedFlow()

추가로, pinRecord 내 발행부도 emit 대신 tryEmit으로 교체하는 것을 권장합니다(아래 별도 코멘트에 diff 포함). 이 변경을 위해 import 추가가 필요합니다(선택 영역 밖):

import kotlinx.coroutines.channels.BufferOverflow

173-189: pinRecord 실패 시 에러를 삼키고 있습니다 — 사용자 알림/상태 반영 필요 + 사이드이펙트 발행은 tryEmit 권장

  • 네트워크 실패 시 .onFailure {} 가 비어 있어 사용자에게 피드백이 없습니다. 최소한 uiState.error 업데이트나 스낵바용 사이드이펙트 발행이 필요합니다.
  • 사이드이펙트 발행은 emit 대신 tryEmit 사용을 권장합니다(상세는 위 코멘트 참고).

적용 예시:

                 .onSuccess { pinInfo ->
                     if (pinInfo != null) {
-                        _sideEffect.emit(
+                        _sideEffect.tryEmit(
                             GroupNoteSideEffect.NavigateToFeedWrite(
                                 pinInfo = pinInfo,
                                 recordContent = content
                             )
                         )
                     }
                 }
-                .onFailure {
-                }
+                .onFailure { throwable ->
+                    _uiState.update { it.copy(error = throwable.message) }
+                    // 필요 시: 실패 스낵바 등의 사이드이펙트도 함께 발행
+                    // _sideEffect.tryEmit(GroupNoteSideEffect.ShowSnackbar(throwable.message ?: "핀 정보 조회 실패"))
+                }

Also applies to: 178-183


100-105: async/awaitAll이 실질적으로 동기화하지 않습니다 — 두 함수 모두 내부에서 launch 사용

loadPosts/loadBookPageInfo가 내부에서 viewModelScope.launch를 사용하므로, 여기서 async { ... }는 즉시 반환되고 awaitAll도 곧바로 끝납니다. 동기화 의도가 없다면 단순 호출로, 동기화/동시 실행이 필요하다면 두 함수에서 내부 launch를 제거하고 suspend로 전환하세요.

간단 정리(내부 launch를 유지하는 최소 변경):

-        viewModelScope.launch {
-            val postsJob = async { loadPosts(isRefresh = true) }
-            val bookPageJob = async { loadBookPageInfo() }
-            awaitAll(postsJob, bookPageJob)
-        }
+        // 두 함수가 내부에서 viewModelScope.launch를 사용하므로 여기의 async/awaitAll은 의미가 없습니다.
+        loadPosts(isRefresh = true)
+        loadBookPageInfo()

필요 시, 제가 suspend fun loadPosts/loadBookPageInfo로 바꾸고 호출부를 정리한 리팩터링 패치를 제안드릴 수 있습니다.


163-171: sealed interface에 대한 when은 ‘else’ 없이 exhaustive로 유지 권장

GroupNoteEvent가 sealed이므로 else를 제거하면 새 이벤트 추가 시 컴파일 타임에 누락을 잡을 수 있습니다. 현재 로깅용 else는 이점을 상쇄합니다.

적용 예시:

-            else -> {
-                Log.w("GroupNoteViewModel", "Unhandled event received: $event")
-            }

222-228: 문자열 기반 postType 분기 — 타입 세이프 전환 권장

"RECORD", "VOTE" 하드코딩 문자열은 오타/변경에 취약합니다. enum class PostType { RECORD, VOTE } 또는 서버 스키마에 맞춘 sealed 타입을 도입하고, OnDeleteRecord/OnLikeRecord 등 이벤트 파라미터도 String 대신 해당 타입을 받도록 바꾸는 것을 권장합니다.


261-283: 서버 응답 병합 시 O(n^2) → O(n)으로 개선 가능

현재 find를 각 항목마다 호출하여 O(n^2)입니다. 서버 응답을 associateBy로 매핑하고 단일 패스에서 병합하면 성능과 가독성이 개선됩니다.

-                if (voteResponse != null) {
-                    val serverVoteItems = voteResponse.voteItems
+                if (voteResponse != null) {
+                    val serverVoteItems = voteResponse.voteItems
+                    val serverById = serverVoteItems.associateBy { it.voteItemId }
@@
-                    val updatedVoteItems = postToUpdate.voteItems.map { originalItem ->
-                        val newItem = serverVoteItems.find { it.voteItemId == originalItem.voteItemId }
-                        newItem ?: originalItem
-                    }
+                    val updatedVoteItems = postToUpdate.voteItems.map { originalItem ->
+                        serverById[originalItem.voteItemId] ?: originalItem
+                    }
app/src/main/java/com/texthip/thip/ui/group/note/component/CommentSection.kt (1)

37-38: 임시 주석 “todo: 수정 가능” 정리 권장

사용자 상호작용 핵심 지점에 모호한 TODO가 남아 있습니다. 이슈 트래킹 번호를 명시하거나 제거해 코드 가독성을 높이는 것을 권장합니다.

예시:

-                    // todo: 수정 가능
+                    // TODO(THIP2025-xxx): 설계 확정 시 null 처리 정책 재검토

Also applies to: 44-46

Comment on lines +24 to 25
val isWriter: Boolean,
val isLike: 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

새 불리언 필드(isWriter) 디코딩 안정성 — 기본값 추가 권장

백엔드 배포 타이밍 차이로 isWriter가 누락된 응답이 올 경우, 현재는 역직렬화 실패합니다. 기본값을 추가해 하위호환성을 확보하세요.

-    val isWriter: Boolean,
+    val isWriter: Boolean = false,
-    val isWriter: Boolean,
+    val isWriter: Boolean = false,

Also applies to: 42-43

🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/data/model/comments/response/CommentsResponse.kt
around lines 24-25 (and likewise for the fields at 42-43), the Boolean fields
can be absent from older/rolled-back backend responses causing deserialization
failures; fix this by giving those properties safe default values (e.g., change
their declarations to include = false) so missing keys decode to the default
instead of throwing, and ensure any JSON annotations remain compatible with the
defaults.

Comment on lines +7 to +10
val attendanceCheckId: Long,
val roomId: Long,
val isFirstWrite: 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 vs Long)

roomId가 Long인데, 서비스/레포지토리에서 roomId: Int로 사용 중인 것으로 요약에 언급되어 있습니다. API 계약과 맞춰 타입을 통일해 주세요. 숫자 폭이 충분하면 Int로, 아니면 호출부/엔드포인트도 Long로 맞추는 것이 안전합니다.

Int로 맞출 경우 예시:

 data class RoomsDailyGreetingResponse(
     val attendanceCheckId: Long,
-    val roomId: Long,
+    val roomId: Int,
     val isFirstWrite: Boolean,
 )

검증 스크립트:


🏁 Script executed:

#!/bin/bash
# RoomsDailyGreeting 관련 타입/시그니처 일관성 확인
rg -n -C2 'postRoomsDailyGreeting\s*\('
rg -n -C2 'RoomsDailyGreetingResponse'
rg -n -C2 'roomId:\s*(Int|Long)'

Length of output: 48247


roomId 타입 일관성 수정 필요 (Long → Int)
서비스/레포지토리에서 roomId를 Int로 사용 중이며, 다른 응답 모델들도 모두 Int를 사용하고 있습니다. API 계약과 코드 일관성을 위해 RoomsDailyGreetingResponseroomId를 Long에서 Int로 변경해주세요.

  • 수정 대상
    • app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsDailyGreetingResponse.kt:8

제안 변경안:

 data class RoomsDailyGreetingResponse(
     val attendanceCheckId: Long,
-    val roomId: Long,
+    val roomId: Int,
     val isFirstWrite: 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 attendanceCheckId: Long,
val roomId: Long,
val isFirstWrite: Boolean,
)
data class RoomsDailyGreetingResponse(
val attendanceCheckId: Long,
val roomId: Int,
val isFirstWrite: Boolean,
)
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsDailyGreetingResponse.kt
around lines 7 to 10, the roomId property is declared as Long but
service/repository and other response models use Int; change the property type
from Long to Int (val roomId: Int), then update any direct usages or tests that
assume Long to use Int and ensure JSON (de)serialization expectations remain
consistent.

Comment on lines +1 to +8
package com.texthip.thip.data.model.rooms.response

import kotlinx.serialization.Serializable

@Serializable
data class RoomsDeleteVoteResponse(
val roomId: Int
)
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

서버 필드명(roomId)과 직렬화 규칙 확인 필요

서버가 snake_case(room_id)를 반환하면 현재 모델은 역직렬화에 실패할 수 있습니다. 서버 응답 키가 roomId인지 room_id인지 확인 후, 필요 시 @SerialName("room_id")를 적용해 주세요. 또한 응답에 voteId 등 추가 필드가 존재한다면 스펙에 맞춰 확장되어야 합니다.

다음 스크립트로 직렬화 관례와 사용처를 빠르게 점검할 수 있습니다.


🏁 Script executed:

#!/bin/bash
# 1) @SerialName 사용 관례 파악
rg -n --type=kt -C2 '@SerialName\('

# 2) RoomsDeleteVoteResponse 사용처와 필드 접근 패턴 확인
rg -n --type=kt -C3 '\bRoomsDeleteVoteResponse\b'
rg -n --type=kt -C3 '\bdeleteRoomsVote\s*\('

Length of output: 217


rg에서 --type=kt를 인식하지 못해 검색이 실패한 것 같습니다. glob 패턴을 사용하여 .kt 파일을 대상으로 다시 점검해 보겠습니다.


🏁 Script executed:

#!/bin/bash
# 1) @SerialName 어노테이션 사용 관례 파악
rg -n --glob '*.kt' -C2 '@SerialName\('

# 2) RoomsDeleteVoteResponse 사용처와 필드 접근 패턴 확인
rg -n --glob '*.kt' -C3 'RoomsDeleteVoteResponse'
rg -n --glob '*.kt' -C3 'deleteRoomsVote\s*\('

Length of output: 37652


@SerialName 어노테이션 추가 및 스펙 확인 필요

다른 응답 모델들과 일관된 직렬화 매핑을 위해, 아래 사항을 반영해 주세요:

• app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsDeleteVoteResponse.kt
roomId 프로퍼티에 @SerialName("roomId") 어노테이션 추가
• 서버가 실제로 반환하는 키가 roomId(camelCase)인지, room_id(snake_case)인지 확인
• 만약 응답에 voteId 등 추가 필드가 있다면 스펙에 맞춰 모델에 확장 반영

🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsDeleteVoteResponse.kt
lines 1-8, the data class lacks explicit serialization mapping and may not match
the server's field naming; add the @SerialName annotation on the roomId property
(import kotlinx.serialization.SerialName) using the exact key the server returns
(either "roomId" or "room_id"), and if the API returns additional fields (e.g.,
voteId) extend the data class with those properties and corresponding
@SerialName annotations to match the spec.

val content: String,
val likeCount: Int,
val commentCount: Int,
val isOverview: 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

신규 필드(isOverview) 역직렬화 하위호환성 — 기본값 권장

서버가 즉시 해당 필드를 내려주지 않는 상황을 대비해 기본값을 넣어 두면 안전합니다.

-    val isOverview: Boolean,
+    val isOverview: Boolean = false,
📝 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 isOverview: Boolean,
val isOverview: Boolean = false,
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsPostsResponse.kt
around line 27, the new Boolean field `isOverview` lacks a default value which
can break deserialization when the server omits it; update the data class to
provide a safe default (e.g., `val isOverview: Boolean = false`) so old
responses without the field deserialize correctly, and if using a JSON library
with annotations, ensure the default is recognized (or add @JvmOverloads /
@JsonProperty default handling) so backward compatibility is preserved.

Comment on lines +6 to +11
data class RoomsRecordsPinResponse(
val bookTitle: String,
val authorName: String,
val bookImageUrl: String,
val isbn: String,
) 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

서버 필드 네이밍과 직렬화 키 일치 여부 확인

kotlinx.serialization에서 글로벌 네이밍 전략을 쓰지 않는다면 서버의 스네이크 케이스 필드와 매칭 문제가 생길 수 있습니다. 서버가 camelCase를 준수하지 않으면 @SerialName이 필요합니다.

다음 스크립트로 프로젝트의 Json 설정(네이밍 전략)과 SerialName 사용 관례를 확인할 수 있습니다:


🏁 Script executed:

#!/bin/bash
# Json 설정에서 namingStrategy 사용 여부 확인
rg -n -C2 'Json\s*\{' -g '!**/build/**' | rg -n -C1 'namingStrategy|JsonNamingStrategy|ignoreUnknownKeys|serializersModule'

# 프로젝트 내 @SerialName 사용 현황 확인
rg -n '@SerialName\(' -g '!**/build/**'

# RoomsRecordsPinResponse 사용처와 응답 파싱 경로 확인
rg -n -C2 'RoomsRecordsPinResponse|getRoomsRecordsPin\(' -g '!**/build/**'

Length of output: 26098


RoomsRecordsPinResponse 프로퍼티에 @SerialName 적용 필요

현재 RoomsRecordsPinResponse 데이터 클래스는 @Serializable만 선언되어 있어, 서버가 snake_case로 응답할 경우 기본 camelCase 매핑과 불일치하여 파싱 오류가 발생할 수 있습니다. 프로젝트 내 다른 Response DTO들은 모두 @SerialName을 명시적으로 사용하고 있으므로, 다음 중 하나를 적용해주세요:

• app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsRecordsPinResponse.kt 수정

 @Serializable
 data class RoomsRecordsPinResponse(
-    val bookTitle: String,
-    val authorName: String,
-    val bookImageUrl: String,
-    val isbn: String,
+    @SerialName("bookTitle")    val bookTitle: String,
+    @SerialName("authorName")   val authorName: String,
+    @SerialName("bookImageUrl") val bookImageUrl: String,
+    @SerialName("isbn")         val isbn: String,
 )

• 또는 Json { namingStrategy = JsonNamingStrategy.SnakeCase } 와 같은 전역 네이밍 전략 설정 검토

서버 스펙과 응답 키 명칭이 정확히 일치하는지 백엔드 문서도 함께 확인해주세요.

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

🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsRecordsPinResponse.kt
around lines 6 to 11, the data class fields currently rely on default camelCase
serialization which will not match a snake_case server response; update the file
to either annotate each property with @SerialName using the expected snake_case
keys (e.g., @SerialName("book_title") for bookTitle, etc.) and ensure the class
is annotated @Serializable, or alternatively configure the project's Json
instance to use a SnakeCase naming strategy globally; pick one approach and
apply it so the serialized names exactly match the backend spec.

Comment on lines +21 to 25
onReplyClick: (commentId: Int, nickname: String?) -> Unit,
onEvent: (CommentsEvent) -> Unit = { _ -> },
onCommentLongPress: (CommentList) -> Unit = { _ -> },
onReplyLongPress: (ReplyList) -> Unit = { _ -> },
) {
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

공개 API 시그니처 변경 확인 필요

onSendReply 제거, onReplyClick 시그니처 변경, long-press 핸들러 2종 추가 등의 API 변경이 있어 모든 호출부 정합성 검증이 필요합니다.

다음 스크립트로 영향을 받는 호출부를 빠르게 점검할 수 있습니다:


🏁 Script executed:

#!/bin/bash
# CommentSection 호출부 및 제거된 onSendReply 사용처 탐색
rg -n -C2 'CommentSection\(' --type=kt
rg -n -C2 '\bonSendReply\b' --type=kt

Length of output: 134


rg가 --type=kt를 인식하지 못해 검색이 실패했습니다. 실제 .kt 파일을 대상으로 직접 검색해 보겠습니다.


🏁 Script executed:

#!/bin/bash
# CommentSection 호출부 및 제거된 onSendReply 사용처 탐색 (.kt 파일 전체 대상)
rg -n -C2 'CommentSection\(' -g '*.kt'
rg -n -C2 '\bonSendReply\b' -g '*.kt'

Length of output: 4167


CommentSection 호출부(Preview) 파라미터 누락 수정 필요

CommentSection 시그니처가 아래와 같이 변경되었습니다:

fun CommentSection(
    commentItem: CommentList,
    onReplyClick: (commentId: Int, nickname: String?) -> Unit,
    onEvent: (CommentsEvent) -> Unit = { _ -> },
    onCommentLongPress: (CommentList) -> Unit = { _ -> },
    onReplyLongPress: (ReplyList) -> Unit = { _ -> },
) { … }

모든 호출부 중, Preview용 호출부에 필수 인자인 onReplyClick과 추가된 롱프레스 핸들러가 빠져 있어 컴파일 오류가 발생합니다. 다음 위치를 수정해주세요:

  • 파일: app/src/main/java/com/texthip/thip/ui/group/note/component/CommentSection.kt
    위치: 약 76번째 줄 (@Preview 내부)

수정 예시:

 @Preview
 @Composable
 fun PreviewCommentSection() {
     ThipTheme {
         Column {
-            CommentSection(
-                commentItem = CommentList(/* … */)
-            )
+            CommentSection(
+                commentItem = CommentList(/* … */),
+                onReplyClick = { _, _ -> },
+                onEvent = { _ -> },
+                onCommentLongPress = { _ -> },
+                onReplyLongPress = { _ -> },
+            )
         }
     }
 }
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/ui/group/note/component/CommentSection.kt
around lines 21-25 (and fix the Preview invocation at ~line 76), the Preview
call is missing the newly required onReplyClick and the long-press handler
parameters; update the @Preview call to pass an onReplyClick lambda and both
onCommentLongPress and onReplyLongPress lambdas (e.g. simple no-op lambdas) so
the call site matches the new CommentSection signature and it compiles.

Comment on lines +62 to +63
onEvent(CommentsEvent.LikeReply(reply.commentId))
},
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

대댓글 좋아요에 parent commentId를 전달하고 있음 — replyId를 전달해야 함

CommentsEvent.LikeReplyreplyId를 받습니다. 현재 reply.commentId(부모 코멘트 ID)를 넘겨 잘못된 대상에 좋아요가 반영될 수 있습니다. 관련 뷰모델에서도 toggleReplyLike(event.replyId)로 사용 중입니다(파일: CommentsViewModel.kt 48-60).

수정안:

-                    onLikeClick = {
-                        onEvent(CommentsEvent.LikeReply(reply.commentId))
-                    },
+                    onLikeClick = {
+                        // replyId가 nullable이라면 null-가드 추가
+                        reply.replyId?.let { onEvent(CommentsEvent.LikeReply(it)) }
+                    },
📝 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
onEvent(CommentsEvent.LikeReply(reply.commentId))
},
onLikeClick = {
// replyId가 nullable이라면 null-가드 추가
reply.replyId?.let { onEvent(CommentsEvent.LikeReply(it)) }
},
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/ui/group/note/component/CommentSection.kt
around lines 62-63, the LikeReply event is being constructed with
reply.commentId (the parent comment's ID) but CommentsEvent.LikeReply expects
the replyId; change the argument to reply.id (or the correct reply identifier
property) so the reply's ID is passed, and verify any nearby event constructions
use the reply's id consistently and that the view/model receives the replyId as
expected.

CardNote(
currentPage = currentPage,
percentage = userPercentage
percentage = userPercentage.roundToInt(),
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

퍼센티지 값 0..100 범위로 클램핑 필요(잠재적 크래시)

CardNote 내부 게이지는 fillMaxWidth(fraction = percentage/100f)를 사용합니다. fraction은 0..1 범위를 요구하므로, 서버/로컬 연산으로 0 미만 또는 100 초과가 전달되면 런타임 예외가 발생할 수 있습니다. 전달 전에 클램핑 해주세요.

-            percentage = userPercentage.roundToInt(),
+            percentage = userPercentage.roundToInt().coerceIn(0, 100),
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/ui/group/room/component/GroupRoomBody.kt
around line 47, the percentage passed to CardNote can be outside 0..100 which
will make CardNote's fillMaxWidth(fraction = percentage/100f) receive an
out-of-range fraction and crash; before passing userPercentage.roundToInt()
clamp it to the 0..100 range (e.g., use Kotlin's coerceIn(0,100) or equivalent)
so the value always yields a fraction between 0f and 1f.

Comment on lines +66 to +76
GroupRoomChatContent(
chatMessages = chatMessages,
inputText = inputText,
onInputTextChanged = { newText -> inputText = newText },
onSendClick = {
viewModel.postDailyGreeting(inputText)
inputText = ""
},
onNavigateBack = onBackClick
)
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

chatMessages 파라미터는 전달되지만 내부에서 전혀 사용되지 않습니다

현재 GroupRoomChatContent로 chatMessages를 넘기지만, Content 내부는 mockMessages로 렌더링합니다. 전달 인자와 실제 사용 데이터가 달라 혼선을 유발합니다. 아래 수정으로 chatMessages를 실제로 사용하도록 반영해 주세요.

-    GroupRoomChatContent(
-        chatMessages = chatMessages,
+    GroupRoomChatContent(
+        chatMessages = chatMessages,
         inputText = inputText,
         onInputTextChanged = { newText -> inputText = newText },
         onSendClick = {
             viewModel.postDailyGreeting(inputText)
             inputText = ""
         },
         onNavigateBack = onBackClick
     )

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

🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomChatScreen.kt
around lines 66 to 76, you pass chatMessages into GroupRoomChatContent but the
composable still renders mockMessages; replace usage of mockMessages inside
GroupRoomChatContent with the passed-in chatMessages parameter (or forward it
into the list rendering), remove or disable the hardcoded/mock data, and ensure
any default or preview values use mockMessages only in preview code so runtime
uses the actual chatMessages prop.

Comment on lines +73 to +83
FeedWriteScreen(
viewModel = viewModel,
onNavigateBack = { navigateBack() },
onFeedCreated = { feedId ->
// 피드 생성 성공 시 결과를 저장하고 피드 목록으로 돌아가기
navController.getBackStackEntry(MainTabRoutes.Feed)
.savedStateHandle
.set("feedId", feedId)
navController.popBackStack()
.savedStateHandle["feedId"] = feedId
navController.popBackStack(MainTabRoutes.Feed, inclusive = false)
}
)
}
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

피드 생성 성공 후 복귀 로직: 백스택 엔트리 존재 여부 확인 권장

MainTabRoutes.Feed가 백스택에 없을 가능성(딥링크로 Write 진입 등)에 대비해 예외 처리/폴백(popBackStack() 또는 navigateUp())을 두는 것이 안전합니다.

가능한 보완 예:

  • getBackStackEntry 실패 시 catch하여 navController.navigate(MainTabRoutes.Feed) 또는 popBackStack(route, inclusive=false)로 대체.

검증 스크립트(Feed 라우트가 항상 그래프에 올라오는지 확인):


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Feed 라우트 및 백스택 진입 지점 확인
rg -n -C3 'composable<MainTabRoutes\.Feed>' app/src/main/java
rg -n -C3 'navigate\(.+MainTabRoutes\.Feed' app/src/main/java || true

Length of output: 981


앱 충돌 방지: Feed 백스택 엔트리 예외 처리 추가 필요

FeedWriteScreen에서
navController.getBackStackEntry(MainTabRoutes.Feed) 호출 시 백스택에 해당 라우트가 없으면 런타임 예외가 발생할 수 있습니다.
(딥링크 등으로 Write 화면만 진입한 경우)

수정 대상
• app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt 73-83라인

보완 예시:

// 피드 생성 성공 시 결과를 저장하고 피드 목록으로 돌아가기
runCatching {
    navController.getBackStackEntry(MainTabRoutes.Feed)
}.onSuccess { backStackEntry ->
    backStackEntry.savedStateHandle["feedId"] = feedId
    navController.popBackStack(MainTabRoutes.Feed, inclusive = false)
}.onFailure {
    // 폴백: Feed 화면으로 네비게이트하거나 Up으로 복귀
    navController.navigate(MainTabRoutes.Feed)
    // 또는 navController.navigateUp()
}

이처럼 예외 처리와 폴백 로직을 추가하면, 백스택 엔트리 미존재 시에도 앱이 안전하게 동작합니다.

🤖 Prompt for AI Agents
app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt
lines 73-83: wrap the call to
navController.getBackStackEntry(MainTabRoutes.Feed) in a safe check/try (e.g.,
runCatching or try/catch) so the app doesn't crash when the Feed entry is
absent; on success set backStackEntry.savedStateHandle["feedId"]=feedId and
popBackStack(MainTabRoutes.Feed, inclusive=false), and on failure provide a
fallback (navigate to MainTabRoutes.Feed or navController.navigateUp()) so the
Write screen handles deep-link entry without a Feed back stack entry.

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.

코드 수정 확인했습니다 기타 api연결인데 기타 api가 뭔가 더 많은거 같네요 ㅋㅋ

Copy link
Collaborator

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.

ActionBarButton 사용 확인했습니다!

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 fb4d7e2 into THIP-TextHip:develop Aug 17, 2025
1 check passed
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] 특정 사용자 피드 조회 [(THIP2025-283)]

2 participants