Skip to content

[REFACTOR] QA 반영 3차#120

Merged
Nico1eKim merged 7 commits intoTHIP-TextHip:developfrom
rbqks529:refactor/#102_QA_3
Aug 21, 2025
Merged

[REFACTOR] QA 반영 3차#120
Nico1eKim merged 7 commits intoTHIP-TextHip:developfrom
rbqks529:refactor/#102_QA_3

Conversation

@rbqks529
Copy link
Collaborator

@rbqks529 rbqks529 commented Aug 20, 2025

➕ 이슈 링크

  • closed #이슈넘버

🔎 작업 내용

  • 피드 스크롤 상태 수정(유지)
  • 기타 패딩
  • 비밀방 책 표지
  • 책검색 버그 수정
  • 내피드 좋아요 수정

📸 스크린샷


😢 해결하지 못한 과제

  • [] TASK


📢 리뷰어들에게

  • 리뷰 봐주실거라 믿고 여기 남겨두겠습니다 먼저 자동로그인은 구현하지 못했는데 10시에 수강신청 후 다시 해보겠습니다. 다음으로 FeedCommentScreen에서 댓글 삭제시 토스트 메세지가 뜨지 않습니다. 또 머지하고 실행해보니까 내 프로필로는 이동이 안되던데 어떤 문제가 있나요? (댓글에서 내 프로필을 누르면 잘 이동합니다)

Summary by CodeRabbit

  • 신기능

    • 방 카드에 비공개 표시(시크릿 배지) 추가.
    • 피드 업데이트에 댓글 수 반영 및 전달.
  • 개선

    • 내 피드가 항목별 저장/좋아요/작성자 상태를 정확히 표시.
    • 탭 전환 시 스크롤 상태가 유지되고, 사용자 전환에만 상단 스크롤 적용.
    • 좋아요/저장 시 전체/내 피드가 동기화되어 즉시 반영.
    • 댓글 작성 후 피드 상세 자동 새로고침.
    • 도서 카드가 직접 탭(onClick)으로 동작.
  • 스타일

    • 인기 도서 목록 하단 여백 추가, 작성자 배지 색상 개선.
    • 닉네임 안내 문구를 “영문 소문자”로 명확화.

@rbqks529 rbqks529 self-assigned this Aug 20, 2025
@coderabbitai
Copy link

coderabbitai bot commented Aug 20, 2025

Walkthrough

모델에 isPublic/isSaved/isLiked 필드가 추가되고, FeedStateUpdateResult에 commentCount가 도입되었습니다. 피드 좋아요/저장 갱신이 양 리스트(all/my)에 동기화되며, SavedStateHandle을 통한 업데이트 전파가 추가되었습니다. CardItemRoom에 isSecret 파라미터가 생겨 비공개 뱃지를 오버레이합니다. CardBookList는 onClick 콜백을 받도록 변경되었습니다.

Changes

Cohort / File(s) Summary
Feed 상태/흐름 업데이트
app/src/main/java/com/texthip/thip/ui/feed/mock/FeedStateUpdateResult.kt, app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt, app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt, app/src/main/java/com/texthip/thip/ui/feed/screen/FeedCommentScreen.kt, app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt
FeedStateUpdateResult에 commentCount 추가 및 전역 적용. SavedStateHandle로 updated_* 값 처리(좋아요/저장/댓글수). 탭 스크롤 상태 rememberSaveable 도입, 사용자 주도 탭 전환 플래그 추가. FeedViewModel는 refreshData 제거, 좋아요/저장 낙관적 업데이트를 allFeeds와 myFeeds 모두에 적용, 실패 시 롤백.
데이터 모델 추가 필드
app/src/main/java/com/texthip/thip/data/model/book/response/RecruitingRoomsResponse.kt, app/src/main/java/com/texthip/thip/data/model/feed/response/MyFeedResponse.kt, app/src/main/java/com/texthip/thip/data/model/rooms/response/MyRoomListResponse.kt
RecruitingRoomItem에 isPublic 추가(기본값 true). MyFeedItem에 isSaved, isLiked 추가. MyRoomResponse에 isPublic 추가.
CardItemRoom 및 사용처
app/src/main/java/com/texthip/thip/ui/common/cards/CardItemRoom.kt, app/src/main/java/com/texthip/thip/ui/group/done/screen/GroupDoneScreen.kt, app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt, app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookGroupScreen.kt
CardItemRoomisSecret 파라미터 추가 및 커버 위 뱃지 오버레이 렌더링. 각 화면에서 isSecret = !isPublic로 전달. RecruitingRoomItem 모의 데이터에 isPublic 반영.
CardBookList 클릭 API 변경
app/src/main/java/com/texthip/thip/ui/common/cards/CardBookList.kt, app/src/main/java/com/texthip/thip/ui/search/component/SearchActiveField.kt, app/src/main/java/com/texthip/thip/ui/search/component/SearchBookFilteredResult.kt
CardBookListonClick: () -> Unit을 공개 파라미터로 수용. 기존 clickable Modifier 기반 호출을 onClick 콜백 전달로 변경.
댓글 뷰모델 상태
app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/CommentsViewModel.kt
CommentsUiStateisCommentCreated 추가. 댓글/대댓글 생성 성공 시 true로 설정, 리셋 메서드 resetCommentCreatedState() 추가.
피드 나의 목록 매핑 갱신
app/src/main/java/com/texthip/thip/ui/feed/screen/FeedMyScreen.kt
MyFeedItem에서 isSaved, isLiked, isWriter를 실제 값으로 매핑하도록 수정. 프리뷰 데이터 갱신.
검색 최근 도서 UI
app/src/main/java/com/texthip/thip/ui/search/component/SearchRecentBook.kt
리스트 마지막 항목 뒤 바텀 스페이서 추가(20dp). 포매팅 정리.
문자열 리소스
app/src/main/res/values/strings.xml
nickname_condition 문구를 “영문소문자”로 수정.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as User
  participant C as CommentsViewModel
  participant S as Server
  participant D as FeedDetailViewModel
  participant FCS as FeedCommentScreen
  participant SSH as SavedStateHandle

  U->>C: 댓글 작성 요청
  C->>S: createComment(payload)
  S-->>C: 성공 응답(댓글)
  C->>C: state.comments 업데이트<br/>isCommentCreated = true
  C-->>FCS: state 변화
  FCS->>D: (LaunchedEffect) loadFeedDetail(feedId)
  D-->>FCS: detail(commentCount 등)
  FCS->>SSH: updated_feed_commentCount = detail.commentCount
  FCS->>C: resetCommentCreatedState()
Loading
sequenceDiagram
  autonumber
  actor U as User
  participant VM as FeedViewModel
  participant Repo as FeedRepository
  participant S as Server

  U->>VM: changeFeedLike(feedId)
  VM->>VM: all/my 리스트 낙관적 토글<br/>likeCount 조정
  VM->>Repo: ChangeFeedLikeUseCase(feedId, current 상태)
  Repo->>S: API 호출
  alt 성공
    S-->>Repo: OK
    Repo-->>VM: FeedStateUpdateResult(isLiked, likeCount, isSaved, commentCount)
    VM->>VM: all/my 리스트에 결과 반영
  else 실패
    S-->>Repo: Error
    Repo-->>VM: Error
    VM->>VM: 이전 상태로 롤백
  end
Loading
sequenceDiagram
  autonumber
  participant FS as FeedScreen
  participant SSH as SavedStateHandle
  participant VM as FeedViewModel

  FS->>SSH: updated_* 값 읽기<br/>(feedId, isLiked, likeCount, isSaved, commentCount)
  alt 값 존재
    FS->>VM: updateFeedStateFromResult(FeedStateUpdateResult)
    FS->>SSH: updated_* 키 삭제
  else 없음
    FS->>VM: 필요 시 데이터 로드/리프레시
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

🍀 refactor, 🎨 UI, 🐻 규빈, ✅ OK merge

Suggested reviewers

  • JJUYAAA
  • Nico1eKim

Poem

깡총깡총, 탭도 저장도 척척척!
비밀 방엔 뱃지 한 장 살짝 첩! 🔒
좋아요 늘고 댓글 수도 착착 척,
책 카드는 톡—onClick에 쏙!
풀밭 위 코드, 바람에 살랑—오늘도 릴리즈 복! 🐰✨

Tip

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

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

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

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

CodeRabbit Commands (Invoked using PR/Issue comments)

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

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt (2)

124-132: observeForever 사용은 메모리 누수 및 중복 수신 위험 — getStateFlow로 교체하세요.

Compose 범위에서 LiveData.observeForever는 해제되지 않아 누수/중복 콜백 위험이 큽니다. SavedStateHandle.getStateFlow를 사용해 수명에 안전하게 수집하도록 바꾸세요.

-    LaunchedEffect(Unit) {
-        navController.currentBackStackEntry?.savedStateHandle?.let { handle ->
-            handle.getLiveData<Long>("deleted_feed_id").observeForever { deletedId ->
-                if (deletedId != null) {
-                    feedViewModel.removeDeletedFeed(deletedId)
-                    handle.remove<Long>("deleted_feed_id")
-                }
-            }
-        }
-    }
+    LaunchedEffect(navController) {
+        navController.currentBackStackEntry?.savedStateHandle?.let { handle ->
+            handle.getStateFlow<Long?>("deleted_feed_id", null).collect { deletedId ->
+                deletedId?.let {
+                    feedViewModel.removeDeletedFeed(it)
+                    handle["deleted_feed_id"] = null
+                }
+            }
+        }
+    }

191-218: updated_ 이벤트에도 observeForever 사용 — 동일하게 StateFlow로 마이그레이션 필요.*

위와 동일한 이유로 누수/중복 위험이 있습니다. getStateFlow를 사용해 수집하고, 처리 후 null로 되돌려 재발행을 제어하세요.

-    LaunchedEffect(Unit) { //커스텀객체 타입 인식오류 -> 직렬화가 아닌 잘게 쪼개어 전달
-        navController.currentBackStackEntry?.savedStateHandle?.let { handle ->
-            handle.getLiveData<Long>("updated_feed_id").observeForever { feedId ->
-                if (feedId != null) {
-                    val isLiked = handle.get<Boolean>("updated_feed_isLiked") ?: false
-                    val likeCount = handle.get<Int>("updated_feed_likeCount") ?: 0
-                    val isSaved = handle.get<Boolean>("updated_feed_isSaved") ?: false
-                    val commentCount = handle.get<Int>("updated_feed_commentCount") ?: 0
-
-                    val result = FeedStateUpdateResult(
-                        feedId = feedId,
-                        isLiked = isLiked,
-                        likeCount = likeCount,
-                        isSaved = isSaved,
-                        commentCount = commentCount
-                    )
-
-                    feedViewModel.updateFeedStateFromResult(result)
-
-                    handle.remove<Long>("updated_feed_id")
-                    handle.remove<Boolean>("updated_feed_isLiked")
-                    handle.remove<Int>("updated_feed_likeCount")
-                    handle.remove<Boolean>("updated_feed_isSaved")
-                    handle.remove<Int>("updated_feed_commentCount")
-                }
-            }
-        }
-    }
+    LaunchedEffect(navController) { // 커스텀 객체 직렬화 이슈 회피: 키 분해 전달 유지
+        navController.currentBackStackEntry?.savedStateHandle?.let { handle ->
+            handle.getStateFlow<Long?>("updated_feed_id", null).collect { feedId ->
+                feedId?.let {
+                    val result = FeedStateUpdateResult(
+                        feedId = it,
+                        isLiked = handle.get<Boolean>("updated_feed_isLiked") ?: false,
+                        likeCount = handle.get<Int>("updated_feed_likeCount") ?: 0,
+                        isSaved = handle.get<Boolean>("updated_feed_isSaved") ?: false,
+                        commentCount = handle.get<Int>("updated_feed_commentCount") ?: 0
+                    )
+                    feedViewModel.updateFeedStateFromResult(result)
+                    handle["updated_feed_id"] = null
+                    handle["updated_feed_isLiked"] = null
+                    handle["updated_feed_likeCount"] = null
+                    handle["updated_feed_isSaved"] = null
+                    handle["updated_feed_commentCount"] = null
+                }
+            }
+        }
+    }
🧹 Nitpick comments (21)
app/src/main/java/com/texthip/thip/ui/search/component/SearchRecentBook.kt (1)

133-137: 마지막 아이템 여백은 LazyColumn의 contentPadding으로 일원화 권장

마지막 아이템에서만 Spacer(padding = 20.dp)를 두는 분기는 동작상 문제는 없으나, 아이템 분기 로직이 복잡해지고 불필요한 측정이 추가됩니다. LazyColumncontentPadding을 주면 동일 효과를 더 간결하게 얻을 수 있습니다. 또한 리스트 길이가 변해도 바닥 여백이 일관되게 유지되어 스크롤 유지 측면에서도 안전합니다.

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

-                LazyColumn(
-                    modifier = Modifier.fillMaxSize()
-                ) {
+                LazyColumn(
+                    modifier = Modifier.fillMaxSize(),
+                    contentPadding = PaddingValues(bottom = 20.dp)
+                ) {
                     items(popularBooks.size) { index ->
                         val book = popularBooks[index]
                         CardBookSearch(
                             number = index + 1,
                             title = book.title,
                             imageUrl = book.imageUrl,
                             onClick = { onBookClick(book) }
                         )
                         if (index < popularBooks.size - 1) {
                             Spacer(
                                 modifier = Modifier
                                     .padding(top = 12.dp, bottom = 12.dp)
                                     .fillMaxWidth()
                                     .height(1.dp)
                                     .background(colors.DarkGrey02)
                             )
-                        } else {
-                            Spacer(
-                                modifier = Modifier
-                                    .padding(bottom = 20.dp)
-                            )
                         }
                     }
                 }

추가로 필요한 import:

+import androidx.compose.foundation.layout.PaddingValues
app/src/main/java/com/texthip/thip/ui/feed/mock/FeedStateUpdateResult.kt (1)

10-11: 기본값 추가하여 옵셔널 필드로 전환 권장

검증 결과 FeedStateUpdateResult 생성자 호출부는 모두 명시적으로 isSavedcommentCount 값을 전달하고 있어, 아래와 같이 기본값을 추가해도 기존 동작에 전혀 영향이 없습니다.
또한, @Serializable 클래스에 기본값을 두면 SavedStateHandle/JSON 역직렬화 시 해당 키가 누락되더라도 안전하게 디폴트 값이 적용되어 직렬화 호환성을 높일 수 있습니다.

수정 대상 위치:

  • app/src/main/java/com/texthip/thip/ui/feed/mock/FeedStateUpdateResult.kt:6

권장하는 변경(diff):

 data class FeedStateUpdateResult(
     val feedId: Long,
-    val isSaved: Boolean,
-    val commentCount: Int
+    val isSaved: Boolean = false,
+    val commentCount: Int = 0
 )
app/src/main/java/com/texthip/thip/ui/search/component/SearchActiveField.kt (1)

40-48: 무한 스크롤 트리거의 안정성 개선 제안

remember(hasMore, isLoading)만 키로 쓰면 listState 교체 시 파생 상태 인스턴스가 재생성되지 않아 미묘한 타이밍 이슈가 날 수 있습니다. 키에 listState를 포함하는 것을 권장합니다. 또한 중복 호출 방지에 snapshotFlow { shouldLoadMore }.distinctUntilChanged() 패턴도 고려 가능.

-    val shouldLoadMore by remember(hasMore, isLoading) {
+    val shouldLoadMore by remember(listState, hasMore, isLoading) {
         derivedStateOf {
             val layoutInfo = listState.layoutInfo
             val totalItemsCount = layoutInfo.totalItemsCount
             val lastVisibleItemIndex = (layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0) + 1
             hasMore && !isLoading && totalItemsCount > 0 && lastVisibleItemIndex >= totalItemsCount - 3
         }
     }

추가로, 현재 파일에서는 import androidx.compose.foundation.clickable가 사용되지 않는 것으로 보입니다. 린트 경고가 있다면 제거해 주세요.

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

47-49: isSecret 파라미터는 nullable 대신 기본값 false가 단순합니다

모든 호출부가 isPublic 기반으로 명확히 결정되고 있어 Boolean? 보다는 Boolean = false가 사용성과 가독성에 유리합니다.

-fun CardItemRoom(
+fun CardItemRoom(
     ...
-    hasBorder: Boolean = false,
-    isSecret: Boolean? = null,
+    hasBorder: Boolean = false,
+    isSecret: Boolean = false,
     onClick: () -> Unit = {}
 )

78-87: 커버 이미지 모서리 클리핑 및 Placeholders 추가 제안

현재 Box는 둥근 모서리 없이 이미지를 꽉 채워 렌더링합니다. 카드의 라운드와 시각적으로 일치하도록 이미지와 오버레이에 동일한 클립을 적용하고, 빈 문자열 대비도 처리하면 UX가 좋아집니다.

적용 diff:

-                Box(
-                    modifier = Modifier.size(width = 80.dp, height = 107.dp)
-                ) {
-                    AsyncImage(
-                        model = imageUrl ?: R.drawable.img_book_cover_sample,
-                        contentDescription = "책 이미지",
-                        modifier = Modifier.fillMaxSize(),
-                        contentScale = ContentScale.Crop
-                    )
+                Box(
+                    modifier = Modifier.size(width = 80.dp, height = 107.dp)
+                ) {
+                    AsyncImage(
+                        model = (imageUrl?.takeIf { it.isNotBlank() } ?: R.drawable.img_book_cover_sample),
+                        contentDescription = stringResource(R.string.cd_book_cover), // 문자열 리소스 필요
+                        modifier = Modifier
+                            .fillMaxSize()
+                            .clip(RoundedCornerShape(8.dp)),
+                        contentScale = ContentScale.Crop
+                    )

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

import androidx.compose.ui.draw.clip

88-93: 비밀 배지 접근성/현지화 정리

오버레이 이미지는 장식 목적이라면 contentDescription을 null로 두고, 대체 텍스트(예: "비밀방")는 상위 컨테이너에 semantics로 부여하는 편이 스크린리더 경험에 적합합니다. 하드코딩 문자열도 stringResource로 교체해주세요.

적용 diff:

-                    if (isSecret == true) {
-                        Image(
-                            painter = painterResource(id = R.drawable.ic_secret_cover),
-                            contentDescription = "비밀방",
-                            modifier = Modifier.fillMaxSize()
-                        )
-                    }
+                    if (isSecret == true) {
+                        Image(
+                            painter = painterResource(id = R.drawable.ic_secret_cover),
+                            contentDescription = null,
+                            modifier = Modifier
+                                .fillMaxSize()
+                                .clip(RoundedCornerShape(8.dp))
+                        )
+                    }

추가로 Box 또는 Card에 semantics 부여(범위 외 보조 코드):

import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics

// Box modifier에 예시로 추가
Box(
    modifier = Modifier
        .size(width = 80.dp, height = 107.dp)
        .semantics { if (isSecret == true) contentDescription = stringResource(R.string.cd_secret_room) }
) { ... }
app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt (1)

210-212: Preview 데이터에 isPublic 포함 OK

시나리오(공개/비공개)별 렌더링 점검에 도움이 됩니다. 필요시 비밀 배지 노출 케이스도 추가 스냅샷 해두면 회귀에 유용합니다.

Also applies to: 220-222, 230-232, 240-242, 250-252, 260-262

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

3-5: 사용되지 않는 import 정리

clickable을 더 이상 사용하지 않으므로 제거해주세요. Lint 경고 예방용입니다.

-import androidx.compose.foundation.clickable
app/src/main/java/com/texthip/thip/data/model/book/response/RecruitingRoomsResponse.kt (1)

21-23: isPublic 기본값(true) 결정 근거 및 모델 간 일관성 확인 요청

  • RecruitingRoomsResponse.kt (21–23행)
    현재 @SerialName("isPublic") val isPublic: Boolean = true 로 정의되어 있습니다. 서버에서 이 필드를 누락할 경우 “공개”로 간주되어 의도치 않은 노출이 발생할 수 있습니다.
  • 다른 Response 모델들과 비교해 보면
    • 일부 모델(e.g. CompletedRoomsResponse.kt)에서는 val isPublic: Boolean? = null 처럼 nullable로 선언하거나
    • 기본값 없이 non-nullable val isPublic: Boolean 만 두어 서버 값을 그대로 사용하고 있습니다.
      (검색 결과: 여러 Response 클래스에서 isPublic 정의 방식을 다양하게 사용 중임 확인)

따라서 아래 사항을 확인 부탁드립니다:

  1. 백엔드 API 계약isPublic 필드가 항상 명시적으로 내려오는지 검증
  2. 만약 누락 가능 이라면
    • DTO에서는 Boolean? = null 처럼 nullable로 받고,
    • 도메인 계층에서 true/false 기본값을 결정하는 방식 고려
    • 또는 kotlinx.serialization의 @EncodeDefault 등 기본값 직렬화 전략 활용 검토
  3. 모델 간 isPublic 처리 방식을 통일하여 혼선을 방지하도록 리팩터링 검토
app/src/main/java/com/texthip/thip/ui/feed/screen/FeedCommentScreen.kt (1)

118-124: 댓글 생성 시 재로딩 로직에 finally로 플래그 리셋을 보강하세요

네트워크 예외가 발생하더라도 isCommentCreated 플래그가 반드시 원복되도록 보장하는 편이 안전합니다. 또한 다중 탭으로 인한 중복 호출을 줄이는 효과도 있습니다.

-    LaunchedEffect(commentsUiState.isCommentCreated) {
-        if (commentsUiState.isCommentCreated) {
-            feedDetailViewModel.loadFeedDetail(feedId)
-            commentsViewModel.resetCommentCreatedState()
-        }
-    }
+    LaunchedEffect(commentsUiState.isCommentCreated) {
+        if (commentsUiState.isCommentCreated) {
+            try {
+                feedDetailViewModel.loadFeedDetail(feedId)
+            } finally {
+                // 실패/성공과 무관하게 플래그를 원복해 재호출 루프를 방지
+                commentsViewModel.resetCommentCreatedState()
+            }
+        }
+    }

참고: 생성 직후 댓글 수만 동기화가 필요하다면 전체 상세 재호출 대신, 로컬로 commentCount + 1을 저장 상태로 전파(예: SavedStateHandle → FeedViewModel)하는 경량화도 고려 가능합니다.

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

146-148: MyRoomResponse에 isPublic 필드 반영 확인 완료

app/src/main/java/com/texthip/thip/data/model/rooms/response/MyRoomListResponse.kt에서 MyRoomResponse 데이터 클래스에 다음과 같이 isPublic: Boolean이 정의되어 있습니다. 더 이상의 검증은 필요 없습니다.

  • 22행: @SerialName("isPublic") val isPublic: Boolean

💡 니트픽 제안
완료 리스트에서는 isRecruiting=false가 고정이라면
RoomUtils.isRecruitingByType(room.type) 대신
상수 false를 넘겨 구성요소 간 결합을 줄일 수 있습니다.
(적용 대상: 146–148, 156–158, 166–168, 176–178, 186–188)

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

261-265: 좋아요 수 계산 음수 방지

동시성/상태 불일치로 currentLikeCount가 0일 때 언라이크하면 음수가 될 수 있습니다. 최소 0으로 보정하세요.

-                val newLikeCount = if (it.isLiked) currentLikeCount + 1 else currentLikeCount - 1
+                val newLikeCount = (if (it.isLiked) currentLikeCount + 1 else currentLikeCount - 1)
+                    .coerceAtLeast(0)
app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookGroupScreen.kt (1)

216-217: 모집 카드에 isSecret 전달 적용 👍

!item.isPublic로 비공개 상태를 명확히 반영했습니다. 동일 패턴을 사용하는 다른 화면들과도 일관됩니다.

가독성을 위해 루프 내에서 val isSecret = !item.isPublic로 한 번 계산해 넘기는 방식도 고려 가능합니다.

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

22-24: UiState에 이벤트성 플래그를 두는 패턴은 재입장/프로세스 복원 시 미세 이슈가 있을 수 있습니다

isCommentCreated는 일회성 이벤트에 가깝습니다. 장기적으로는 SharedFlow(또는 Channel 기반)로 “댓글 생성됨” 이벤트를 별도로 발행하고, 화면에서 collect하는 패턴이 더 견고합니다. 현재 구현은 동작상 문제 없으나 전환을 권장드립니다.

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

363-364: StateFlow 업데이트 방식 일관화 권장 (updateState로 통일).

동일 파일 내에서 updateState와 _uiState.update가 혼용되고 있습니다. 동일한 헬퍼를 사용해 가독성과 유지보수성을 높이는 것이 좋습니다.

-            _uiState.update { it.copy(allFeeds = newAllFeeds, myFeeds = newMyFeeds) }
+            updateState { it.copy(allFeeds = newAllFeeds, myFeeds = newMyFeeds) }
-                .onFailure {
-                    _uiState.update { it.copy(allFeeds = currentAllFeeds, myFeeds = currentMyFeeds) }
-                }
+                .onFailure {
+                    updateState { it.copy(allFeeds = currentAllFeeds, myFeeds = currentMyFeeds) }
+                }
-            ).onFailure {
-                _uiState.update { it.copy(allFeeds = currentAllFeeds, myFeeds = currentMyFeeds) }
-            }
+            ).onFailure {
+                updateState { it.copy(allFeeds = currentAllFeeds, myFeeds = currentMyFeeds) }
+            }

Also applies to: 379-380, 429-430


332-339: feedId 변환 중복 호출 정리로 미세 최적화 및 가독성 개선.

동일 블록에서 toLong 변환이 반복됩니다. 한 번만 변환해 지역 변수로 사용하면 안전하고 읽기 쉽습니다.

-            val allFeedToUpdate = currentAllFeeds.find { it.feedId.toLong() == feedId }
-            val myFeedToUpdate = currentMyFeeds.find { it.feedId.toLong() == feedId }
+            val targetId = feedId
+            val allFeedToUpdate = currentAllFeeds.find { it.feedId.toLong() == targetId }
+            val myFeedToUpdate = currentMyFeeds.find { it.feedId.toLong() == targetId }

283-287: refreshData는 현재 직렬 실행입니다 — 네트워크 대기 시간을 줄이려면 병렬화/일관화 고려.

refreshAllFeeds()와 refreshMyFeeds()를 순차 호출하여 총 대기 시간이 길어질 수 있습니다. 두 호출을 병렬화하거나(예: coroutineScope + async) 혹은 ViewModel init/화면 최초 진입 로직 중 하나만 단일 진입점으로 두는 방식으로 단순화하는 것을 권장합니다. 현 PR의 범위를 넘는 변경이므로 선택 사항으로 남깁니다.

해당 변경을 적용할 경우, 도메인 레이어의 API 제약(동시 호출 허용 여부)을 확인해 주세요.


434-462: 양 리스트(all/my) 동기화 및 commentCount 전파 로직은 명확하고 일관적입니다.

서버/상세화면에서 넘어온 FeedStateUpdateResult를 기반으로 두 리스트를 동시에 갱신하는 접근이 합리적입니다. 불필요한 재조합을 줄이려면 실제 변경이 없는 경우 early-return을 추가하는 것도 고려해볼 수 있습니다.


290-311: isLoading을 ‘최근 작성자’ 로딩에도 재사용 — 초기 스피너 제어 상의 미묘한 결합.

최근 작성자 로딩에도 isLoading을 사용하고 있어(라인 292/306), 초기 진입/탭 전환 시 스피너 표시에 간접 영향이 생길 수 있습니다. UI가 명확하다면 그대로도 가능하나, 필요 시 recentWritersLoading 같은 전용 플래그로 분리하면 의도가 더 분명해집니다.

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

106-122: 무한 스크롤 트리거 계산은 동작하나, snapshotFlow로 스크롤 신호만 관찰하면 불필요한 recomposition을 더 줄일 수 있습니다.

현재 derivedStateOf로 계산하고 LaunchedEffect(shouldLoadMore)로 로딩을 트리거합니다. 성능상 문제는 크지 않지만, 리스트 스크롤 신호만 추출해 snapshotFlow { currentListState.layoutInfo.visibleItemsInfo.lastOrNull()?.index }로 수집하면 키 변경에 따른 재계산을 더 구체적으로 제어할 수 있습니다.


143-153: Feed 초기 로딩 중복 가능성 확인 및 단일 진입점으로 정리 제안

현재 FeedViewModel init 블록에서 loadAllFeeds() 를 호출하는 한편, FeedScreen 에서도 진입 시 데이터가 비어 있으면 refreshData() 를 다시 호출하고 있어 초기 진입 시 API가 중복 요청될 수 있습니다.

  • FeedViewModel.kt init 블록 (67–70줄): loadAllFeeds(), fetchRecentWriters() 호출
  • FeedViewModel.kt refreshData 메서드 (282–286줄): refreshAllFeeds(), refreshMyFeeds(), fetchRecentWriters() 순으로 재호출
  • FeedScreen.kt 초기 진입 로직 (143–147줄): allFeeds와 myFeeds가 비었으면 refreshData() 실행

제안:

  • UI(Screen) 진입부에서만 초기 로드를 담당하거나,
  • ViewModel init에서만 초기 로드를 수행하도록 통일
  • 또는 ViewModel에서 “초기 로드 완료” 상태를 노출하여 UI가 중복 호출을 방지하도록 조건을 명확히 분리
📜 Review details

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

💡 Knowledge Base configuration:

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

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a823936 and 5f3b426.

📒 Files selected for processing (18)
  • app/src/main/java/com/texthip/thip/data/model/book/response/RecruitingRoomsResponse.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/model/feed/response/MyFeedResponse.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/model/rooms/response/MyRoomListResponse.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt (2 hunks)
  • app/src/main/java/com/texthip/thip/ui/common/cards/CardItemRoom.kt (3 hunks)
  • app/src/main/java/com/texthip/thip/ui/feed/mock/FeedStateUpdateResult.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/ui/feed/screen/FeedCommentScreen.kt (2 hunks)
  • app/src/main/java/com/texthip/thip/ui/feed/screen/FeedMyScreen.kt (2 hunks)
  • app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt (15 hunks)
  • app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt (3 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/done/screen/GroupDoneScreen.kt (6 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt (7 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/CommentsViewModel.kt (4 hunks)
  • app/src/main/java/com/texthip/thip/ui/search/component/SearchActiveField.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/ui/search/component/SearchBookFilteredResult.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/ui/search/component/SearchRecentBook.kt (2 hunks)
  • app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookGroupScreen.kt (2 hunks)
  • app/src/main/res/values/strings.xml (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt (3)
app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedDetailViewModel.kt (1)
  • updateState (40-42)
app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt (1)
  • updateState (33-35)
app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt (1)
  • updateState (35-37)
app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt (3)
app/src/main/java/com/texthip/thip/utils/color/HexToColor.kt (1)
  • hexToColor (6-13)
app/src/main/java/com/texthip/thip/ui/feed/component/FeedSubscribelistBar.kt (1)
  • FeedSubscribeBarlist (35-101)
app/src/main/java/com/texthip/thip/ui/feed/component/MyFeedCard.kt (1)
  • MyFeedCard (38-187)
🔇 Additional comments (19)
app/src/main/res/values/strings.xml (1)

319-319: 닉네임 검증 로직 위치 및 일관성 확인 필요

코드베이스 내에서 닉네임 입력을 실제로 필터링하거나 정규식으로 검증하는 로직(Pattern/Regex/InputFilter)을 찾을 수 없습니다. 서버 측 검증 규칙(한글·영문 소문자·숫자 허용 여부, 길이 제한 등)과 클라이언트 UI의 안내 문구 및 구현이 일치하는지 직접 점검해 주세요.

– app/src/main/res/values/strings.xml:319 (nickname_condition = “한글/영문소문자/숫자로 구성”)
– app/src/main/java/.../SignupNicknameScreen.kt: WarningTextFieldmaxLength 미설정(길이 제한 없음)
– app/src/main/java/.../MypageEditScreen.kt: WarningTextFieldmaxLength=10 적용

요청사항

  • 서버 API(UserService.checkNickname)의 허용 패턴(대문자·특수문자 포함 여부, 최소/최대 길이)을 확인
  • 위 규칙을 클라이언트 쪽 InputFilter/Regex 또는 TextField maxLength 등에 반영
  • strings.xml 안내 문구에도 “2~12자” 등 구체적인 길이 범위를 추가하여 UX 개선
app/src/main/java/com/texthip/thip/ui/search/component/SearchActiveField.kt (1)

66-68: CardBookList onClick API 전환 적절

카드 자체의 클릭 처리를 컴포저블 파라미터로 넘기는 방향이 명확합니다. 이벤트 추적/테스트에도 유리합니다.

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

21-22: 비-옵셔널 isPublic 필드로 인한 역직렬화 실패 위험
현재 MyRoomResponse 내에서

  • 파일: app/src/main/java/com/texthip/thip/data/model/rooms/response/MyRoomListResponse.kt
  • 위치: MyRoomResponse 클래스, @SerialName("isPublic") val isPublic: Boolean

처럼 non-nullable Boolean으로 선언되어 있습니다. 서버가 해당 필드를 누락하거나 배포가 지연된 경우, kotlinx.serialization 역직렬화 단계에서 예외가 발생합니다. 안전한 기본값을 지정하거나 nullable 처리하는 것을 권장합니다.

제안하는 리팩터링 예시:

 data class MyRoomResponse(
     @SerialName("type") val type: String,
-    @SerialName("isPublic") val isPublic: Boolean
+    @SerialName("isPublic") val isPublic: Boolean = true
 )
  • 기본값으로 true를 지정하면, 기존에 isSecret = !room.isPublic 로 UI를 구성할 때 변경 없이 동일한 UX를 보장합니다.
  • 서버 스키마나 샘플 JSON 응답에서 isPublic 필드가 항상 포함되는지 반드시 확인해 주세요.
app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt (1)

165-167: 검증 완료: Room 응답 모델의 isPublic은 non-nullable로 선언되어 있습니다

  • MyRoomListResponse.kt 에서
    @SerialName("isPublic") val isPublic: Boolean 으로 선언되어 있어 null 또는 누락 가능성이 없습니다.
  • RoomsSearchResponse.kt 에서는
    @SerialName("isPublic") val isPublic: Boolean = true 로 기본값을 지정하고 있어 누락 시에도 true 로 처리됩니다.
  • RoomsPlayingResponse.kt, RoomRecruitingResponse.kt 등 모든 룸 관련 응답 모델에서 isPublic: Boolean 으로 정의되어 있습니다.

따라서 isSecret = !room.isPublic 역치환 로직은 안전하며, 별도 추가 검증 없이 코드 변경을 승인합니다.

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

93-95: CardBookList.onClick API로의 이관 깔끔합니다

클릭 영역을 컴포넌트 내부로 수렴시켜 책임이 명확해졌습니다.

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

86-89: isSaved/isLiked/isWriter를 모델에서 직접 반영 — 상태 일관성 개선

하드코딩 제거로 다른 화면과의 상태 동기화가 수월해집니다. LGTM.


206-208: 프리뷰 데이터도 최신 필드 반영 OK

isLiked 패턴과 isSaved=false로 다양한 케이스 확인 가능. 유지하세요.

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

94-101: 키 소비 및 제거 정상 확인됨

  • FeedScreen.kt에서 handle.getXXX("updated_feed_*")로 값을 읽은 후
    handle.remove<…>("updated_feed_id")부터 handle.remove<Int>("updated_feed_commentCount")까지 모든 키가 명시적으로 제거되고 있습니다.
  • updated_feed_commentCount 역시 소비 후 정상 제거되는 것을 확인했습니다.

이상 없으므로 변경 승인합니다.

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

113-122: isSecret 파생값 전달 방향성 좋습니다

isSecret = !room.isPublic으로 의미가 명확합니다. CardItemRoom의 잠금 뱃지 렌더링과도 자연스럽게 매핑됩니다.

app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookGroupScreen.kt (1)

273-275: 프리뷰 Mock에 isPublic 추가 반영 적절

디자이너/QA 확인용으로 공개/비공개 케이스를 모두 포함한 점 좋습니다.

Also applies to: 282-284, 291-293

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

144-148: 상위 댓글 추가 시 상태 갱신과 이벤트 플래그 설정 타이밍 적절

성공 시 선반영(UI 즉시성) + isCommentCreated=true로 후속 동기화 트리거를 주는 흐름이 명확합니다.


296-298: 리셋 메서드 분리 좋습니다

화면에서 명시적으로 호출해 재호출 루프를 방지하는 구조가 명확합니다.


163-167: replyList 병합 로직: API 응답 계약 확인 필요

현재 CommentsCreateResponse.replyList를 기존 originalParentComment.replyList에 추가(+ res.replyList)하는 방식은
서버가 "신규 답글만" 반환한다는 전제 하에서만 안전합니다.
만약 서버가 “부모 댓글의 전체 최신 답글 목록”을 반환한다면 기존 리스트와 중복이 발생할 수 있습니다.

– 점검 대상
• app/src/main/java/com/texthip/thip/data/model/comments/response/CommentsCreateResponse.kt
• app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/CommentsViewModel.kt (163–167행)

서버 응답이 “신규 답글만”인지, “전체 최신 목록”인지 API 명세 또는 백엔드 담당자에게 확인 후,
필요 시 merge 대신 replace 로직으로 변경해 주세요.

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

93-98: 스크롤 상태 rememberSaveable + LazyListState.Saver 적용 좋습니다.

프로세스 킬/재생성 후에도 탭별 스크롤 위치를 안정적으로 복원할 수 있어 UX가 향상됩니다.


156-160: 사용자 주도 탭 전환 시에만 상단 스크롤 — UX 측면에서 적절합니다.

프로그램적 탭 변경 시 스크롤 이동을 막아 의도치 않은 점프를 방지합니다.


182-190: 새로고침 결과 처리 후 상단 스크롤 처리 적절.

refreshFeed 플래그 소비 후 현재 탭 리스트만 스크롤하여 과도한 이동을 방지합니다.


312-314: UI 안전성 보완(색상/리스트 fallback) 좋습니다.

  • aliasColor 파싱 실패 시 기본색으로 폴백
  • 팔로워 이미지 URL 리스트 null 시 빈 리스트 처리
  • 수량 표기 stringResource 포맷팅 적용

모두 런타임 안정성에 기여합니다.

Also applies to: 321-323, 331-335


365-367: 내 피드 카드 매핑 및 액션 바인딩 적절.

isLiked/isSaved를 MyFeedItem에서 직접 반영하고, 좋아요 액션을 ViewModel로 연결해 상/하위 리스트 동기화를 기대할 수 있습니다.

Also applies to: 381-383, 390-391


417-420: 전체 피드 리스트 키 및 액션 연동 적절.

  • key를 feedId로 지정해 항목 재정렬/갱신 시 안정성 확보
  • 저장/좋아요 클릭 액션이 ViewModel에 위임되어 상태 동기화 경로가 명확

Also applies to: 444-449

Comment on lines +26 to +27
@SerialName("isSaved") val isSaved: Boolean,
@SerialName("isLiked") 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

MyFeedItem에 isSaved/isLiked 추가: 서버 누락 시 크래시 가능

신규 불린 필드를 비-옵셔널로 추가하면, 구버전 서버/캐시된 응답/실험 플래그 미적용 환경에서 역직렬화가 실패할 수 있습니다. 기본값을 지정해 하위 호환성을 확보하세요.

-    @SerialName("isSaved") val isSaved: Boolean,
-    @SerialName("isLiked") val isLiked: Boolean,
+    @SerialName("isSaved") val isSaved: Boolean = false,
+    @SerialName("isLiked") val isLiked: Boolean = false,

또한, 새 필드가 누락되더라도 UI/도메인 매핑이 자연스럽게 동작하는지(예: 기본 false로 표시) 확인 바랍니다.

디코딩 회귀 테스트 추가 제안:

  • 케이스1: 두 필드가 없는 기존 JSON → 정상 디코딩(기본값 적용)
  • 케이스2: 두 필드가 포함된 최신 JSON → 정상 디코딩

기본값 누락 여부 확인을 위해 아래 스크립트를 실행합니다.


🏁 Script executed:

#!/bin/bash
# MyFeedResponse.kt 파일 경로 검색
file=$(fd --strip-cwd-prefix 'MyFeedResponse.kt')
echo "File: $file"

# isSaved 선언부 확인 (기본값 없는지)
rg -n '@SerialName\("isSaved"\)' "$file" -B2 -A2

# isLiked 선언부 확인 (기본값 없는지)
rg -n '@SerialName\("isLiked"\)' "$file" -B2 -A2

Length of output: 1031


MyFeedResponse.kt의 isSaved/isLiked에 기본값(false) 지정 필수

검증 결과, app/src/main/java/com/texthip/thip/data/model/feed/response/MyFeedResponse.kt 파일의 26–27행에 선언된 두 Boolean 필드에 기본값이 없어, 서버에서 해당 필드가 누락될 경우 역직렬화 실패로 앱이 크래시될 수 있습니다.

수정 제안:

  • 파일: app/src/main/java/com/texthip/thip/data/model/feed/response/MyFeedResponse.kt
  • 위치: 26–27행
-    @SerialName("isSaved") val isSaved: Boolean,
-    @SerialName("isLiked") val isLiked: Boolean,
+    @SerialName("isSaved") val isSaved: Boolean = false,
+    @SerialName("isLiked") val isLiked: Boolean = false,

추가 확인 및 작업:

  • 기본값 지정 후, UI/도메인 매핑 시 두 필드가 누락돼도 false로 자연스럽게 처리되는지 확인
  • 디코딩 회귀 테스트 추가
    • 케이스1: isSaved/isLiked 누락 기존 JSON → 기본값 적용 후 정상 디코딩
    • 케이스2: 두 필드 포함 최신 JSON → 정상 디코딩
📝 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
@SerialName("isSaved") val isSaved: Boolean,
@SerialName("isLiked") val isLiked: Boolean,
@SerialName("isSaved") val isSaved: Boolean = false,
@SerialName("isLiked") val isLiked: Boolean = false,
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/data/model/feed/response/MyFeedResponse.kt
around lines 26–27, the Boolean properties isSaved and isLiked lack default
values which causes deserialization to fail when the server omits them; add
default values (false) to both declarations, update any UI/domain mapping to
treat absent fields as false, and add decoding regression tests: one JSON
missing isSaved/isLiked should decode with false defaults, and one JSON
including both should still decode correctly.

Comment on lines +266 to 268
isSaved = currentIsSaved, // isSaved 상태는 그대로 유지
commentCount = 0 // 좋아요 함수에서는 댓글 수 정보 없음
)
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

commentCount=0로 브로드캐스트 시 댓글 수가 0으로 초기화되는 치명적 가능성

좋아요/저장 처리에서 FeedStateUpdateResult(commentCount = 0)를 내보내면, 뷰모델 머지 로직에 따라 목록의 commentCount가 0으로 덮어써질 수 있습니다(실제 값 보존 실패). 이는 데이터 손실/UX 오류로 직결됩니다.

권장 수정안 A(타입 안전):

  • FeedStateUpdateResult.commentCountInt?로 변경하고, null일 때는 기존 값을 유지하도록 머지 로직을 수정하세요.
  • 본 파일에서는 다음처럼 설정합니다.
-                    commentCount = 0 // 좋아요 함수에서는 댓글 수 정보 없음
+                    commentCount = null // 댓글 수는 변경하지 않음
-                    commentCount = 0 // 저장 함수에서는 댓글 수 정보 없음
+                    commentCount = null // 댓글 수는 변경하지 않음

추가로 필요한 외부 변경(예시):

// FeedStateUpdateResult.kt (예시)
data class FeedStateUpdateResult(
    val feedId: Long,
    val isLiked: Boolean,
    val likeCount: Int,
    val isSaved: Boolean,
    val commentCount: Int? = null // null이면 기존 값 유지
)

권장 수정안 B(임시 우회):

  • 타입 변경이 어렵다면, ViewModel의 머지 단계에서 “좋아요/저장에 의해 발생한 업데이트”에는 commentCount를 무시하도록 분기하세요. 단, 실제 0으로 갱신해야 하는 시나리오(댓글 0건)도 존재하므로, 이 접근은 장기적으로 안전하지 않습니다.

대안 C(호출부 전달):

  • 본 함수 시그니처에 currentCommentCount: Int를 추가해 그대로 전달/유지. 호출부 전파가 필요합니다.

다음 스크립트로 머지 로직이 commentCount를 어떻게 취급하는지 확인해 주세요.

Also applies to: 291-293


🏁 Script executed:

#!/bin/bash
# FeedStateUpdateResult 병합 경로 및 commentCount 사용 방식 확인
rg -nP --type=kt -C3 'updateFeedStateFromResult|apply.*FeedStateUpdateResult|commentCount'

Length of output: 119


기존 스크립트가 .kt 파일 타입을 인식하지 못해 결과를 얻지 못했습니다. 아래 스크립트로 Kotlin 소스 전체에서 머지 로직과 commentCount 사용 위치를 다시 확인해주세요.


🏁 Script executed:

#!/bin/bash
# Kotlin 파일 전체에서 FeedStateUpdateResult와 commentCount 사용 위치 검색

# 1. updateFeedStateFromResult 호출 위치 검색
rg -n 'updateFeedStateFromResult' -C3 -g '*.kt'

# 2. FeedStateUpdateResult 생성/사용 위치 검색
rg -n 'FeedStateUpdateResult' -C3 -g '*.kt'

# 3. apply { FeedStateUpdateResult(...) } 패턴 검색
rg -nP 'apply.*FeedStateUpdateResult' -C3 -g '*.kt'

# 4. commentCount 필드 접근 또는 할당 위치 검색
rg -n 'commentCount' -C3 -g '*.kt'

Length of output: 43136


commentCount 초기화 버그 발견 — 긴급 수정 필요

FeedRepository.kt에서 좋아요/저장 처리 시 commentCount = 0으로 설정하고,
FeedViewModel.updateFeedStateFromResult에서 이 값을 무조건 덮어써 실제 댓글 개수가 손실됩니다.

다음 위치를 확인해주세요:

  • app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt (266–268, 291–293)
  • app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt (434–442, 451–454)

권장 수정안 A (타입 안전)

// app/src/main/java/com/texthip/thip/ui/feed/mock/FeedStateUpdateResult.kt
- data class FeedStateUpdateResult(
+ data class FeedStateUpdateResult(
      val feedId: Long,
      val isLiked: Boolean,
      val likeCount: Int,
      val isSaved: Boolean,
-     val commentCount: Int
+     val commentCount: Int? = null // null이면 기존 값 유지
  )
// app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt
-                    commentCount = 0 // 좋아요/저장 함수에서는 댓글 수 정보 없음
+                    commentCount = null // 댓글 수는 변경하지 않음
// app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt
-                    commentCount = result.commentCount
+                    commentCount = result.commentCount ?: feed.commentCount

※ 임시 우회(B) 및 호출부 전달(C) 대안은 앞선 코멘트를 참고하세요.

📝 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
isSaved = currentIsSaved, // isSaved 상태는 그대로 유지
commentCount = 0 // 좋아요 함수에서는 댓글 수 정보 없음
)
isSaved = currentIsSaved, // isSaved 상태는 그대로 유지
commentCount = null // 댓글 수는 변경하지 않음
)

Comment on lines +341 to +347
val newAllFeeds = currentAllFeeds.map {
if (it.feedId.toLong() == feedId) {
it.copy(
isLiked = !it.isLiked,
likeCount = if (it.isLiked) it.likeCount - 1 else it.likeCount + 1
)
} else {
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

좋아요 수 음수로 내려갈 수 있습니다 — 최소 0으로 클램프하세요.

현재 낙관적 업데이트에서 isLiked가 true인 상태에서 해제 시 likeCount를 단순히 -1 합니다. 0에서 해제하면 -1이 되어 UI 일관성과 서버 정합성이 깨질 수 있습니다. 최소값을 0으로 제한하는 것이 안전합니다.

-                        likeCount = if (it.isLiked) it.likeCount - 1 else it.likeCount + 1
+                        likeCount = if (it.isLiked) (it.likeCount - 1).coerceAtLeast(0) else it.likeCount + 1
-                        likeCount = if (it.isLiked) it.likeCount - 1 else it.likeCount + 1
+                        likeCount = if (it.isLiked) (it.likeCount - 1).coerceAtLeast(0) else it.likeCount + 1

Also applies to: 353-357

🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt around
lines 341-347 and 353-357, the optimistic update can decrement likeCount below
zero; change the decrement logic to clamp the resulting likeCount to a minimum
of 0 (e.g., compute the new count and apply max/coerceAtLeast(0)) so unliking
from 0 never produces -1; apply this fix in both places where likeCount is
adjusted.

Comment on lines 378 to 381
.onFailure {
_uiState.update { it.copy(allFeeds = currentFeeds) }
_uiState.update { it.copy(allFeeds = currentAllFeeds, myFeeds = currentMyFeeds) }
}
}
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

낙관적 업데이트 실패 시 “전체 스냅샷 롤백”은 최신 데이터 덮어쓸 위험이 큽니다. 대상 아이템만 롤백하세요.

현재 실패 처리에서 변경 전 전체 리스트 스냅샷으로 되돌립니다. 같은 시점에 백그라운드 새로고침/페이지네이션이 반영되었다면 그 변경까지 소실될 수 있습니다. 실패 시 해당 feedId 아이템만 원래 값으로 복구하세요.

-            ).onFailure {
-                updateState { it.copy(allFeeds = currentAllFeeds, myFeeds = currentMyFeeds) }
-            }
+            ).onFailure {
+                updateState {
+                    it.copy(
+                        allFeeds = it.allFeeds.map { f ->
+                            if (f.feedId.toLong() == feedId) {
+                                f.copy(
+                                    isLiked = (allFeedToUpdate?.isLiked ?: myFeedToUpdate!!.isLiked),
+                                    likeCount = (allFeedToUpdate?.likeCount ?: myFeedToUpdate!!.likeCount)
+                                )
+                            } else f
+                        },
+                        myFeeds = it.myFeeds.map { f ->
+                            if (f.feedId.toLong() == feedId) {
+                                f.copy(
+                                    isLiked = (myFeedToUpdate?.isLiked ?: allFeedToUpdate!!.isLiked),
+                                    likeCount = (myFeedToUpdate?.likeCount ?: allFeedToUpdate!!.likeCount)
+                                )
+                            } else f
+                        }
+                    )
+                }
+            }
-            ).onFailure {
-                updateState { it.copy(allFeeds = currentAllFeeds, myFeeds = currentMyFeeds) }
-            }
+            ).onFailure {
+                updateState {
+                    it.copy(
+                        allFeeds = it.allFeeds.map { f ->
+                            if (f.feedId.toLong() == feedId) {
+                                f.copy(isSaved = (allFeedToUpdate?.isSaved ?: myFeedToUpdate!!.isSaved))
+                            } else f
+                        },
+                        myFeeds = it.myFeeds.map { f ->
+                            if (f.feedId.toLong() == feedId) {
+                                f.copy(isSaved = (myFeedToUpdate?.isSaved ?: allFeedToUpdate!!.isSaved))
+                            } else f
+                        }
+                    )
+                }
+            }

Also applies to: 429-430

🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt around
lines 378-381 (and similarly at 429-430), the optimistic-update failure handler
currently replaces the entire feed lists with a prior full snapshot, which can
overwrite concurrent updates; change the rollback to restore only the single
feed item that was modified: capture the original Feed item (by feedId) before
applying the optimistic change, and onFailure update the UI state by mapping
over current lists to replace the item with that original value (leaving other
items and any background refresh/pagination intact); ensure you handle both
allFeeds and myFeeds lists and null/absent cases gracefully.

Comment on lines +89 to 93
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Modifier.weight import 누락으로 컴파일 실패 가능

Modifier.weight(1f)androidx.compose.foundation.layout.weight 확장 함수입니다. 현재 import 목록에 없어서 컴파일 에러가 발생할 수 있습니다. 아래 import를 추가해 주세요.

+import androidx.compose.foundation.layout.weight

추가 제안: 이 Box가 실질적으로 가중치 적용 용도라면, 빈 상태 UI(Column)와 LazyColumn에 각각 Modifier.weight(1f)를 직접 부여하도록 구조를 단순화할 수 있습니다. 가독성이 좋아지고 중첩이 줄어듭니다.

🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/ui/search/component/SearchRecentBook.kt
around lines 89 to 93, the call to Modifier.weight(1f) uses the extension
androidx.compose.foundation.layout.weight which is not imported and will cause a
compile error; add the import androidx.compose.foundation.layout.weight to the
file, and optionally simplify the layout by removing this Box and applying
Modifier.weight(1f) directly to the empty-state Column and the LazyColumn
respectively so each branch handles its own weight without an extra wrapping
Box.

@Nico1eKim
Copy link
Member

  1. 댓글 삭제 시 toast message 뜨도록 수정했습니다
  2. 앱을 처음 실행시키고 나서를 말씀하시는거면 수정했습니다

@Nico1eKim Nico1eKim merged commit c5bc220 into THIP-TextHip:develop Aug 21, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants