Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ data class RecruitingRoomItem(
@SerialName("roomName") val roomName: String = "",
@SerialName("memberCount") val memberCount: Int = 0,
@SerialName("recruitCount") val recruitCount: Int = 0,
@SerialName("deadlineEndDate") val deadlineEndDate: String = ""
@SerialName("deadlineEndDate") val deadlineEndDate: String = "",
@SerialName("isPublic") val isPublic: Boolean = true
)
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,7 @@ data class MyFeedItem(
@SerialName("likeCount") val likeCount: Int,
@SerialName("commentCount") val commentCount: Int,
@SerialName("isPublic") val isPublic: Boolean,
@SerialName("isSaved") val isSaved: Boolean,
@SerialName("isLiked") val isLiked: Boolean,
Comment on lines +26 to +27
Copy link
Copy Markdown

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.

@SerialName("isWriter") val isWriter: Boolean
)
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ data class MyRoomResponse(
@SerialName("recruitCount") val recruitCount: Int,
@SerialName("memberCount") val memberCount: Int,
@SerialName("endDate") val endDate: String,
@SerialName("type") val type: String
@SerialName("type") val type: String,
@SerialName("isPublic") val isPublic: Boolean
)
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,8 @@ class FeedRepository @Inject constructor(
feedId = feedId,
isLiked = it.isLiked,
likeCount = newLikeCount,
isSaved = currentIsSaved // isSaved 상태는 그대로 유지
isSaved = currentIsSaved, // isSaved 상태는 그대로 유지
commentCount = 0 // 좋아요 함수에서는 댓글 수 정보 없음
)
Comment on lines +266 to 268
Copy link
Copy Markdown

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 // 댓글 수는 변경하지 않음
)

_feedStateUpdateResult.emit(update)
}
Expand All @@ -287,7 +288,8 @@ class FeedRepository @Inject constructor(
feedId = feedId,
isLiked = currentIsLiked, // isLiked 상태는 그대로 유지
likeCount = currentLikeCount,
isSaved = it.isSaved
isSaved = it.isSaved,
commentCount = 0 // 저장 함수에서는 댓글 수 정보 없음
)
_feedStateUpdateResult.emit(update)
}
Expand Down
27 changes: 21 additions & 6 deletions app/src/main/java/com/texthip/thip/ui/common/cards/CardItemRoom.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package com.texthip.thip.ui.common.cards

import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
Expand Down Expand Up @@ -41,6 +44,7 @@ fun CardItemRoom(
endDate: String? = null,
imageUrl: String? = null,
hasBorder: Boolean = false,
isSecret: Boolean? = null,
onClick: () -> Unit = {}
) {
Card(
Expand Down Expand Up @@ -71,12 +75,23 @@ fun CardItemRoom(
modifier = Modifier.fillMaxWidth()
) {
// 이미지
AsyncImage(
model = imageUrl ?: R.drawable.img_book_cover_sample,
contentDescription = "책 이미지",
modifier = Modifier.size(width = 80.dp, height = 107.dp),
contentScale = ContentScale.Crop
)
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
)
if (isSecret == true) {
Image(
painter = painterResource(id = R.drawable.ic_secret_cover),
contentDescription = "비밀방",
modifier = Modifier.fillMaxSize()
)
}
}

Spacer(modifier = Modifier.width(12.dp))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ data class FeedStateUpdateResult(
val feedId: Long,
val isLiked: Boolean,
val likeCount: Int,
val isSaved: Boolean
val isSaved: Boolean,
val commentCount: Int
)
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ fun FeedCommentScreen(
handle.set("updated_feed_isLiked", detail.isLiked)
handle.set("updated_feed_likeCount", detail.likeCount)
handle.set("updated_feed_isSaved", detail.isSaved)
handle.set("updated_feed_commentCount", detail.commentCount)
}
}
}
Expand All @@ -114,6 +115,14 @@ fun FeedCommentScreen(
commentsViewModel.initialize(postId = feedId.toLong(), postType = "FEED")
}

// 댓글이 생성되면 피드 상세 정보를 다시 로드
LaunchedEffect(commentsUiState.isCommentCreated) {
if (commentsUiState.isCommentCreated) {
feedDetailViewModel.loadFeedDetail(feedId)
commentsViewModel.resetCommentCreatedState()
}
}

// 로딩 상태 처리
if (feedDetailUiState.isLoading) {
Box(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,9 @@ fun FeedMyContent(
likeCount = this.likeCount,
commentCount = this.commentCount,
isPublic = this.isPublic,
isSaved = false,
isLiked = false,
isWriter = true
isSaved = this.isSaved,
isLiked = this.isLiked,
isWriter = this.isWriter
)
}

Expand Down Expand Up @@ -203,8 +203,8 @@ private fun FeedMyScreenPreview() {
bookTitle = "나의 책 제목 ${it + 1}", bookAuthor = "나",
contentBody = "내가 작성한 피드 내용입니다. 내용은 여기에 표시됩니다.",
contentUrls = emptyList(), likeCount = 15, commentCount = 3,
isPublic = true,
isWriter = false
isPublic = true, isSaved = false, isLiked = it % 2 == 0,
isWriter = true
)
}

Expand Down
82 changes: 60 additions & 22 deletions app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
Expand All @@ -32,6 +32,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
Expand Down Expand Up @@ -85,20 +86,28 @@ fun FeedScreen(
val scope = rememberCoroutineScope()
var showProgressBar by remember { mutableStateOf(false) }
val progress = remember { Animatable(0f) }

val feedTabTitles = listOf(stringResource(R.string.feed), stringResource(R.string.my_feed))

// 탭별로 별도의 스크롤 상태 관리
val allFeedListState = rememberLazyListState()
val myFeedListState = rememberLazyListState()
val allFeedListState = rememberSaveable(saver = LazyListState.Saver) {
LazyListState()
}
val myFeedListState = rememberSaveable(saver = LazyListState.Saver) {
LazyListState()
}
val currentListState = when (feedUiState.selectedTabIndex) {
0 -> allFeedListState
1 -> myFeedListState
else -> allFeedListState
}

// 무한 스크롤 로직
val shouldLoadMore by remember(feedUiState.canLoadMoreCurrentTab, feedUiState.isLoadingMore, feedUiState.selectedTabIndex) {
val shouldLoadMore by remember(
feedUiState.canLoadMoreCurrentTab,
feedUiState.isLoadingMore,
feedUiState.selectedTabIndex
) {
derivedStateOf {
val layoutInfo = currentListState.layoutInfo
val totalItems = layoutInfo.totalItemsCount
Expand Down Expand Up @@ -127,14 +136,27 @@ fun FeedScreen(
feedViewModel.loadMoreFeeds()
}
}


var isUserTabChange by remember { mutableStateOf(false) }

LaunchedEffect(Unit) {
feedViewModel.resetToInitialState()
// 최초 진입시에만 데이터 로딩
if (feedUiState.allFeeds.isEmpty() && feedUiState.myFeeds.isEmpty()) {
feedViewModel.refreshData()
}
val hasUpdatedFeedData =
navController.currentBackStackEntry?.savedStateHandle?.get<Long>("updated_feed_id") != null

if (!hasUpdatedFeedData) {
allFeedListState.scrollToItem(0)
}
}

// 탭 변경 시 해당 탭의 스크롤을 최상단으로 부드럽게 이동
LaunchedEffect(feedUiState.selectedTabIndex) {
currentListState.scrollToItem(0)
if (isUserTabChange) {
currentListState.scrollToItem(0)
isUserTabChange = false
}
}

LaunchedEffect(resultFeedId) {
Expand All @@ -156,12 +178,13 @@ fun FeedScreen(
}
}
}

LaunchedEffect(refreshFeed) {
if (refreshFeed == true) {
onRefreshConsumed()
if (resultFeedId == null) {
feedViewModel.refreshData()
currentListState.scrollToItem(0)
}
}
}
Expand All @@ -172,12 +195,14 @@ fun FeedScreen(
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
isSaved = isSaved,
commentCount = commentCount
)

feedViewModel.updateFeedStateFromResult(result)
Expand All @@ -186,6 +211,7 @@ fun FeedScreen(
handle.remove<Boolean>("updated_feed_isLiked")
handle.remove<Int>("updated_feed_likeCount")
handle.remove<Boolean>("updated_feed_isSaved")
handle.remove<Int>("updated_feed_commentCount")
}
}
}
Expand Down Expand Up @@ -222,7 +248,10 @@ fun FeedScreen(
HeaderMenuBarTab(
titles = feedTabTitles,
selectedTabIndex = feedUiState.selectedTabIndex,
onTabSelected = feedViewModel::onTabSelected
onTabSelected = { index ->
isUserTabChange = true
feedViewModel.onTabSelected(index)
}
)

// 스크롤 영역 전체
Expand Down Expand Up @@ -269,7 +298,7 @@ fun FeedScreen(
}
}
}

if (feedUiState.selectedTabIndex == 1) {
// 내 피드
item {
Expand All @@ -280,15 +309,17 @@ fun FeedScreen(
profileImage = myFeedInfo?.profileImageUrl,
nickname = myFeedInfo?.nickname ?: "",
badgeText = myFeedInfo?.aliasName ?: "",
badgeTextColor = myFeedInfo?.aliasColor?.let { hexToColor(it) } ?: colors.NeonGreen,
badgeTextColor = myFeedInfo?.aliasColor?.let { hexToColor(it) }
?: colors.NeonGreen,
buttonText = "",
buttonWidth = 60.dp,
showButton = false
)
Spacer(modifier = Modifier.height(16.dp))
FeedSubscribeBarlist(
modifier = Modifier.padding(horizontal = 20.dp),
followerProfileImageUrls = myFeedInfo?.latestFollowerProfileImageUrls ?: emptyList(),
followerProfileImageUrls = myFeedInfo?.latestFollowerProfileImageUrls
?: emptyList(),
onClick = {
myFeedInfo?.creatorId?.let { creatorId ->
onNavigateToOthersSubscription(creatorId)
Expand All @@ -297,7 +328,10 @@ fun FeedScreen(
)
Spacer(modifier = Modifier.height(40.dp))
Text(
text = stringResource(R.string.whole_num, myFeedInfo?.totalFeedCount ?: 0),
text = stringResource(
R.string.whole_num,
myFeedInfo?.totalFeedCount ?: 0
),
style = typography.menu_m500_s14_h24,
color = colors.Grey,
modifier = Modifier
Expand Down Expand Up @@ -327,7 +361,9 @@ fun FeedScreen(
}
}
} else {
itemsIndexed(feedUiState.myFeeds, key = { _, item -> item.feedId }) { index, myFeed ->
itemsIndexed(
feedUiState.myFeeds,
key = { _, item -> item.feedId }) { index, myFeed ->
Spacer(modifier = Modifier.height(if (index == 0) 20.dp else 40.dp))

// MyFeedItem을 FeedItem으로 변환
Expand All @@ -342,16 +378,16 @@ fun FeedScreen(
content = myFeed.contentBody,
likeCount = myFeed.likeCount,
commentCount = myFeed.commentCount,
isLiked = false, // 내 피드는 좋아요 개념 없음
isSaved = false, // 내 피드는 저장 개념 없음
isLiked = myFeed.isLiked,
isSaved = myFeed.isSaved,
isLocked = !myFeed.isPublic, // isPublic의 반대값
tags = emptyList(),
imageUrls = myFeed.contentUrls
)

MyFeedCard(
feedItem = feedItem,
onLikeClick = {},
onLikeClick = { feedViewModel.changeFeedLike(feedItem.id) },
onContentClick = {
onNavigateToFeedComment(feedItem.id)
},
Expand All @@ -378,7 +414,9 @@ fun FeedScreen(
onClick = onNavigateToMySubscription
)
}
itemsIndexed(feedUiState.allFeeds, key = { _, item -> item.feedId }) { index, allFeed ->
itemsIndexed(
feedUiState.allFeeds,
key = { _, item -> item.feedId }) { index, allFeed ->
// AllFeedItem을 FeedItem으로 변환
val feedItem = FeedItem(
id = allFeed.feedId.toLong(),
Expand Down Expand Up @@ -454,7 +492,7 @@ fun FeedScreen(
icon = painterResource(id = R.drawable.ic_write),
onClick = onNavigateToFeedWrite
)

// 탭 전환 시 화면 가운데 로딩 인디케이터
if (feedUiState.isRefreshing) {
Box(
Expand Down
Loading