Conversation
|
Caution Review failedThe pull request is closed. Walkthrough사용자 도메인 API 연동을 위해 요청/응답 DTO 추가, UserService/UserRepository 확장, 관련 뷰모델·화면(Feed, MyPage, OthersSubscription, Signup 등) 추가 및 UI 컴포넌트(카드, RoleCard, SavedFeedCard 등) API 변경과 네비게이션 라우트 확장을 적용했습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant UI as FeedScreen
participant VM as FeedViewModel
participant Repo as UserRepository
participant API as UserService
UI->>VM: init()
VM->>Repo: getRecentWriters()
Repo->>API: GET /users/recent-writers
API-->>Repo: BaseResponse<MyRecentFollowingsResponse>
Repo-->>VM: Result<MyRecentFollowingsResponse?>
VM-->>UI: uiState(recentWriters)
sequenceDiagram
participant UI as MySubscriptionList
participant VM as MySubscriptionViewModel
participant Repo as UserRepository
participant API as UserService
UI->>VM: toggleFollow(userId)
VM->>VM: optimistic toggle (isFollowing 반전)
VM->>Repo: toggleFollow(followingUserId, isFollowing)
Repo->>API: POST /follow/{followingUserId} {type: Boolean}
API-->>Repo: BaseResponse<FollowResponse>
Repo-->>VM: Result<FollowResponse?>
VM->>VM: reconcile or rollback on failure
sequenceDiagram
participant UI as SignupNicknameScreen
participant VM as NicknameViewModel
participant Repo as UserRepository
participant API as UserService
UI->>VM: checkNickname()
VM->>Repo: checkNickname(NicknameRequest)
Repo->>API: POST /nickname/check
API-->>Repo: BaseResponse<NicknameResponse>
Repo-->>VM: Result<NicknameResponse?>
VM-->>UI: navigateToNext or show warning
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Possibly related issues
Possibly related PRs
Suggested labels
Suggested reviewers
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. 📜 Recent review detailsConfiguration used: CodeRabbit UI 💡 Knowledge Base configuration:
You can enable these sources in your CodeRabbit configuration. 📒 Files selected for processing (1)
✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 10
🔭 Outside diff range comments (2)
app/src/main/java/com/texthip/thip/ui/mypage/component/SavedFeedCard.kt (1)
51-57: 프로필 이미지 로딩 버그: Int 리소스를 String으로 변환해 전달
ProfileBar는profileImage: String을 받아 CoilAsyncImage(model = profileImage)에 넘깁니다. 현재feedItem.userProfileImage가 정수 리소스 아이디로 보이는데,toString()으로 넘기면 유효한 URL/경로가 아니어서 이미지가 로드되지 않습니다.해결 옵션:
- 옵션 A(권장):
ProfileBar의profileImage타입을Any로 변경하여 URL(String)과 리소스(Int) 모두 허용.ProfileBar.kt 변경 예시:
-fun ProfileBar( - modifier: Modifier = Modifier, - profileImage: String, +fun ProfileBar( + modifier: Modifier = Modifier, + profileImage: Any, topText: String, bottomText: String, ... ) { - AsyncImage(model = profileImage, ...) + AsyncImage(model = profileImage, ...) }SavedFeedCard.kt는 다음처럼 변경됩니다:
- ProfileBar( - profileImage = feedItem.userProfileImage.toString(), + ProfileBar( + profileImage = feedItem.userProfileImage ?: "", topText = feedItem.userName, bottomText = feedItem.userRole, showSubscriberInfo = false, hoursAgo = feedItem.timeAgo )
- 옵션 B: 리소스 URI 문자열로 변환해 넘기기(추가 비용/가독성 떨어짐). 예:
"android.resource://${context.packageName}/${feedItem.userProfileImage}"옵션 A가 코드 간결성과 유연성 측면에서 우수합니다.
app/src/main/java/com/texthip/thip/ui/feed/screen/MySubscriptionListScreen.kt (1)
58-66: 초기 컴포지션에서 무한 스크롤 트리거가 과하게 발생할 수 있음visibleItemsInfo가 비어있는 초기 프레임에서 lastVisibleItemIndex를 0으로 간주하면, totalItemsCount가 1인 경우 곧바로 "끝까지 스크롤"로 판단될 수 있습니다. 안전하게 lastVisibleItem이 존재할 때만 비교하도록 수정하면 불필요한 추가 요청을 방지할 수 있습니다.
다음과 같이 방어적으로 계산하도록 제안드립니다:
- val isScrolledToEnd by remember { - derivedStateOf { - val layoutInfo = lazyListState.layoutInfo - if (layoutInfo.totalItemsCount == 0) return@derivedStateOf false - val lastVisibleItemIndex = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0 - lastVisibleItemIndex >= layoutInfo.totalItemsCount - 1 - } - } + val isScrolledToEnd by remember(lazyListState) { + derivedStateOf { + val layoutInfo = lazyListState.layoutInfo + if (layoutInfo.totalItemsCount == 0) return@derivedStateOf false + val lastVisible = layoutInfo.visibleItemsInfo.lastOrNull() ?: return@derivedStateOf false + lastVisible.index >= layoutInfo.totalItemsCount - 1 + } + }
🧹 Nitpick comments (49)
app/src/main/java/com/texthip/thip/data/model/users/response/NicknameResponse.kt (1)
6-9: 불린 필드 의미 확인 필요: isVerified가 “사용 가능 여부”인지 “형식 검증 여부”인지 명확화닉네임 중복 검증의 응답 필드가 isVerified로 되어 있는데, 보통 도메인에서는 “사용 가능 여부(isAvailable)”로 표현되는 경우가 많습니다. 네이밍이 의미를 충분히 전달하지 못할 수 있어, 백엔드 계약이 “중복 아님(사용 가능) = true”인지, “형식 검증 통과 = true”인지 확인 바랍니다. 의미가 “사용 가능 여부”라면 아래와 같이 Kotlin 내 필드명을 의미 있는 이름으로 유지하면서 JSON 키 매핑만 유지하는 것을 권장합니다.
아래는 선택적 리팩터 제안입니다(동일 JSON 키 유지):
package com.texthip.thip.data.model.users.response import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable data class NicknameResponse( - @SerialName("isVerified") val isVerified: Boolean + @SerialName("isVerified") + val isAvailable: Boolean )app/src/main/java/com/texthip/thip/ui/mypage/mock/RoleItem.kt (1)
6-7: UI 계층에서는 Color 파싱을 한 번만 수행하도록 경계(매핑)에서 변환 권장imageUrl/roleColor를 네트워크 포맷(String)으로 유지하신 의도가 이해됩니다. 다만 이 RoleItem이 실제 Compose UI에서 직접 소비된다면, 매 렌더링마다 문자열→Color 변환 비용/예외 처리가 반복될 수 있습니다. ViewModel 또는 mapper에서 한 번 변환하여 UI 모델은 Color 타입을 갖도록 하거나, 최소한 확장함수로 안전 파싱 유틸을 두고 UI 단에서는 이미 변환된 값을 쓰는 패턴을 권장합니다.
예시(파일 외 유틸):
// ex) ui/common/ColorExt.kt import androidx.compose.ui.graphics.Color fun String.toColorOrNull(): Color? = runCatching { // #RRGGBB / #AARRGGBB 모두 대응 Color(android.graphics.Color.parseColor(this)) }.getOrNull()ViewModel에서:
val uiModel = roleDto?.let { RoleItem( genre = it.genre, role = it.role, imageUrl = it.imageUrl, roleColor = it.roleColor.toColorOrNull() ?: Color.Black ) }또한 서버가 “#” 포함 여부(예: #RRGGBB)를 일관되게 보장하는지 확인 부탁드립니다.
app/src/main/java/com/texthip/thip/data/model/users/request/FollowRequest.kt (1)
6-8: 불린 키 ‘type’의 의미가 모호 — 도메인 의미가 드러나는 이름으로 리네이밍 후 @SerialName 유지 권장현재 Boolean 필드명이 type이라 의미 파악이 어렵습니다. true/false가 각각 “팔로우/언팔로우” 중 무엇을 의미하는지 코드만으로 추론하기 어렵기 때문에, Kotlin 필드명을 isFollowing(또는 shouldFollow) 등으로 바꾸고 JSON 키는 @SerialName("type")으로 유지하는 것을 권장합니다. 이는 가독성과 오용 방지에 도움이 됩니다.
아래는 제안 diff입니다(백엔드 키는 그대로 유지):
package com.texthip.thip.data.model.users.request +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable data class FollowRequest( - val type: Boolean + @SerialName("type") + val isFollowing: Boolean )추가로, true/false의 의미(예: true=팔로우, false=언팔로우)를 코드 주석/문서로 고정해 두면 이후 유지보수 비용을 줄일 수 있습니다.
app/src/main/java/com/texthip/thip/data/model/users/response/MyPageInfoResponse.kt (1)
7-12: 서버 응답 누락 대비 기본값/널 처리 전략 점검 권장nickname/aliasName/aliasColor가 서버 스키마 변경 또는 일부 케이스에서 누락될 경우, kotlinx.serialization은 기본값 없는 비널 필드에서 디코딩 실패를 일으킬 수 있습니다. 클라이언트 안정성을 높이려면 다음 중 하나를 검토해 주세요:
- 기본값을 지정해 디코딩 실패를 회피
- DTO는 서버 원본을 그대로 두고, UI/도메인 모델로 매핑 시 기본값 부여
- 백엔드와 “필수/옵셔널” 계약 명시 및 필수 필드 누락 방지
또한 aliasColor는 UI에서 Color로 변환될 것이므로, 유효한 HEX 포맷(“#RRGGBB”/“#AARRGGBB”)을 서버가 일관되게 내려주는지 확인 부탁드립니다.
app/src/main/java/com/texthip/thip/ui/common/cards/CardBookList.kt (1)
82-95: 아이콘 버튼 접근성 및 리소스 로딩 개선 제안 (48dp 터치 타겟, painterResource 사용 권장)
- IconButton 사이즈를 24.dp로 제한하면 최소 터치 타겟(48.dp) 가이드에 미달합니다.
- ImageVector.vectorResource는 벡터가 아닌 PNG 리소스에선 실패할 수 있습니다. painterResource를 사용하면 리소스 형식에 관계없이 안전합니다.
- 상태 기반 contentDescription(“저장됨”/“저장”)을 제공하면 보조기기 접근성이 향상됩니다.
아래와 같이 수정하는 것을 권장합니다.
- if(showBookmark) { - IconButton( - onClick = onBookmarkClick, - modifier = Modifier.size(24.dp) - ) { - Icon( - imageVector = if (isBookmarked) ImageVector.vectorResource(R.drawable.ic_save_filled) else ImageVector.vectorResource( - R.drawable.ic_save - ), - contentDescription = "북마크", - tint = if (isBookmarked) colors.Purple else colors.Grey01 - ) - } - } + if (showBookmark) { + IconButton( + onClick = onBookmarkClick + ) { + val iconRes = if (isBookmarked) R.drawable.ic_save_filled else R.drawable.ic_save + Icon( + painter = painterResource(iconRes), + contentDescription = if (isBookmarked) "저장됨" else "저장", + tint = if (isBookmarked) colors.Purple else colors.Grey01, + modifier = Modifier.size(24.dp) // 아이콘 자체 크기, 버튼은 기본 48dp 터치 타겟 + ) + } + }파일 상단에 다음 import를 추가해야 합니다:
import androidx.compose.ui.res.painterResource참고: 토글 상태를 표현하는 컴포넌트로 IconToggleButton을 사용하는 것도 선택지입니다(checked = isBookmarked, onCheckedChange = { onBookmarkClick() }).
app/src/main/java/com/texthip/thip/ui/mypage/component/BookContent.kt (2)
36-45: 첫 아이템 상단 여백은 contentPadding으로 단순화 권장index == 0 체크로 Spacer를 넣기보다 LazyColumn의 contentPadding을 쓰면 코드가 간결해지고 유지보수성이 좋아집니다.
- LazyColumn ( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 20.dp), - ){ - itemsIndexed(bookList, key = { _, book -> book.id }) { index, book -> - if (index == 0) { - Spacer(Modifier.height(32.dp)) - } + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(horizontal = 20.dp, top = 32.dp), + ) { + itemsIndexed(bookList, key = { _, book -> book.id }) { index, book -> // ...
56-63: 마지막 아이템 구분선 조건 처리 OK — 미세 최적화 제안index != lastIndex 조건은 올바릅니다. 사소하지만, lastIndex를 바깥 스코프에서 미리 계산해 캡처하면(예: val lastIndex = bookList.lastIndex) 재구성 시 미세한 이득이 있습니다.
app/src/main/java/com/texthip/thip/data/model/users/response/AliasChoiceResponse.kt (1)
3-5: 직렬화 라이브러리 혼용(Gson + kotlinx.serialization) — 한 가지로 정리 권장@SerializedName(Gson)와 @serializable(kotlinx) 혼용은 유지보수 혼선을 유발합니다. 프로젝트 전반 컨버터(예: Retrofit ConverterFactory)에 맞춰 한 가지로 통일하세요. Gson을 사용 중이라면 @serializable 제거를 추천합니다.
-import kotlinx.serialization.Serializable - -@Serializable data class AliasChoiceResponse( @SerializedName("aliasChoices") val aliasChoices: List<AliasChoice> ) -@Serializable data class AliasChoice( @SerializedName("aliasName") val aliasName: String, @SerializedName("categoryName") val categoryName: String, @SerializedName("imageUrl") val imageUrl: String, @SerializedName("aliasColor") val aliasColor: String )추가 확인 요청:
- imageUrl, aliasColor가 항상 비null인지 백엔드 스키마를 확인해 주세요. Optional 가능성이 있으면
String?로 선언하는 편이 안전합니다.Also applies to: 6-17
app/src/main/java/com/texthip/thip/data/model/users/response/MyFollowingsResponse.kt (1)
1-22: 직렬화 어노테이션 혼용 — 모델 전반 통일 제안이 파일도 @SerializedName(Gson)과 @serializable(kotlinx)을 혼용하고 있습니다. 컨버터 전략에 맞춰 단일화해 주세요(예: Gson 유지 시 @serializable 제거).
app/src/main/java/com/texthip/thip/data/model/users/response/FollowResponse.kt (1)
6-9: @SerialName 생략 가능 여부 점검 (API 스펙 확인 필요)JSON 키와 프로퍼티 이름이 동일("isFollowing")하여 @SerialName은 기능상 불필요합니다. API가 camelCase로 반환된다는 전제가 맞다면 제거를 권장합니다. 반대로 서버 응답 키가 다른 형식(snake_case 등)이라면 그대로 두는 게 맞습니다.
적용 예시:
-import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable data class FollowResponse( - @SerialName("isFollowing") val isFollowing: Boolean + val isFollowing: Boolean )app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt (6)
116-116: 중복 수집 제거: uiState를 두 번 collect하고 있습니다Line 59에서 이미
feedUiState로 collect한 상태인데, Line 116에서 동일한viewModel.uiState.collectAsState()를 다시 수행하고 있으며 변수(subscriptionUiState)도 사용되지 않습니다. 제거해주세요.- val subscriptionUiState by viewModel.uiState.collectAsState()
204-210: lastIndex 기준 컬렉션 불일치 가능성 (feeds vs feedStateList)Divider 표시 조건에서
feeds.lastIndex를 사용하고 있으나, 실제로 순회하는 컬렉션은feedStateList입니다. 두 컬렉션의 크기가 diverge될 경우(향후 페이징/갱신 로직 추가 등) 경계 판단이 틀어질 수 있습니다.feedStateList.lastIndex를 사용하는 것으로 통일하세요.- if (index != feeds.lastIndex) { + if (index != feedStateList.lastIndex) { HorizontalDivider( color = colors.DarkGrey02, thickness = 10.dp ) }(위 수정은 두 곳 모두 동일하게 적용)
Also applies to: 251-257
57-60: Preview 안전성: hiltViewModel 기본값 사용으로 Preview 크래시 가능Compose Preview에서 Hilt DI 컨텍스트가 없어
hiltViewModel()기본값이 호출되면 Preview가 크래시할 수 있습니다. 옵션:
- 방법 A: viewModel 파라미터를 nullable로 바꾸고, null일 때는 수집을 생략하거나 기본 uiState를 사용.
- 방법 B: LocalInspectionMode를 활용해 Preview 모드에서는 hiltViewModel 호출을 피함.
- 방법 C: 화면을 두 단계로 분리(Stateless UI): ViewModel 의존하는 래퍼(collect만 수행)와, 실제 UI는
uiState를 파라미터로 받도록 분리.간단 예시(참고용):
@Composable fun FeedScreen( ..., viewModel: FeedViewModel? = null ) { val isPreview = androidx.compose.ui.platform.LocalInspectionMode.current val vm = viewModel ?: if (isPreview) null else hiltViewModel<FeedViewModel>() val feedUiState = vm?.uiState?.collectAsState()?.value ?: /* preview fallback */ FeedUiState() ... }또한 lifecycle-aware 수집으로 전환(
collectAsStateWithLifecycle)을 권장합니다.
216-224: 구독 바 데이터 매핑은 remember로 메모이즈 권장
subscriptionsForBar는 recomposition마다 매번 새 리스트를 생성합니다.recentWriters가 바뀔 때만 재계산하도록remember(feedUiState.recentWriters)로 감싸면 불필요한 객체 생성이 줄어듭니다.예시:
val subscriptionsForBar = remember(feedUiState.recentWriters) { feedUiState.recentWriters.map { user -> MySubscriptionData( profileImageUrl = user.profileImageUrl, nickname = user.nickname, role = "", roleColor = colors.White, subscriberCount = 0, isSubscribed = true ) } }
66-115: 사용되지 않는 더미 데이터 제거
mySubscriptions는 현재 어디에서도 사용되지 않습니다. 실제 API 연동으로 대체된 상태라면 삭제해 코드 가독성과 유지보수성을 높이세요.- val mySubscriptions = listOf( - MySubscriptionData( - profileImageUrl = "https://example.com/image1.jpg", - nickname = "abcabcabcabc", - role = "문학가", - roleColor = colors.SocialScience - ), - ... - )
39-39: mock 패키지 의존 최소화 권장
MySubscriptionData를ui.feed.mock에서 가져와 실제 화면에 사용하고 있습니다. 추후 mock 제거 시 의존성 문제가 생길 수 있어, 실제 UI용 모델로 이동/정의하거나 DTO 매핑 레이어를 두는 것을 권장합니다.app/src/main/java/com/texthip/thip/ui/navigator/extensions/MyPageNavigationExtensions.kt (2)
13-27: navigate 옵션(launchSingleTop 등) 고려같은 화면으로의 반복 클릭 시 백스택이 쌓일 수 있습니다. 필요한 경우
launchSingleTop = true등을 적용해 중복 누적을 방지하세요.예시:
-fun NavHostController.navigateToEditProfile() { - navigate(MyPageRoutes.Edit) -} +fun NavHostController.navigateToEditProfile() { + navigate(MyPageRoutes.Edit) { + launchSingleTop = true + } +}다른 함수에도 동일 패턴 적용 가능.
25-27: 함수 네이밍 일관성
navigateToLeaveThipScreen만Screen접미사를 사용하고, 나머지는 사용하지 않습니다. 일관성을 위해navigateToLeaveThip으로 통일하는 것을 제안합니다. (호출부 영향 검토 필요)-fun NavHostController.navigateToLeaveThipScreen() { +fun NavHostController.navigateToLeaveThip() { navigate(MyPageRoutes.LeaveThip) }app/src/main/java/com/texthip/thip/ui/navigator/routes/MyPageRoutes.kt (1)
9-9: Reaction 경로 사용 여부 확인
Reaction라우트가 선언되어 있으나 본 PR 범위에서 사용처가 보이지 않습니다. 의도된 예약 분(추후 작업)인지, 아니면 불필요 선언인지 확인 부탁드립니다.app/src/main/java/com/texthip/thip/ui/mypage/component/SavedFeedCard.kt (1)
41-45: 중복 리스트 계산 제거
images와imagePainters가 동일한 역할을 합니다. 하나만 유지하고 이를 기준으로hasImages/maxLines를 계산하는 편이 낫습니다.- val images = feedItem.imageUrls.orEmpty().map { painterResource(id = it) } - val imagePainters = feedItem.imageUrls.orEmpty().map { painterResource(it) } - val hasImages = imagePainters.isNotEmpty() - val maxLines = if (hasImages) 3 else 8 + val images = feedItem.imageUrls.orEmpty().map { painterResource(id = it) } + val hasImages = images.isNotEmpty() + val maxLines = if (hasImages) 3 else 8app/src/main/java/com/texthip/thip/ui/mypage/component/FeedContent.kt (1)
32-55: 첫 아이템 Spacer는 contentPadding으로 대체하면 더 간결합니다index == 0 조건으로 Spacer를 두는 대신 LazyColumn의 contentPadding을 쓰면 구성 로직이 단순해지고 가독성이 좋아집니다.
다음과 같이 정리 가능합니다:
-LazyColumn ( - modifier = Modifier - .fillMaxSize() -){ - itemsIndexed(feedList, key = { _,feed -> feed.id }) { index,feed -> - if (index == 0) { - Spacer(Modifier.height(32.dp)) - } +LazyColumn( + modifier = Modifier + .fillMaxSize(), + contentPadding = PaddingValues(top = 32.dp) +) { + itemsIndexed(feedList, key = { _, feed -> feed.id }) { index, feed -> SavedFeedCard( feedItem = feed, onBookmarkClick = { viewModel.toggleBookmark(feed.id) }, onLikeClick = { viewModel.toggleLike(feed.id) } ) if (index != feedList.lastIndex) { Spacer(Modifier.height(40.dp)) HorizontalDivider( color = colors.DarkGrey03, thickness = 6.dp ) Spacer(Modifier.height(40.dp)) } } }추가로 필요한 import:
import androidx.compose.foundation.layout.PaddingValuesapp/src/main/java/com/texthip/thip/ui/feed/viewmodel/MySubscriptionViewModel.kt (1)
74-121: 동시 탭/중복 요청 가드를 추가해 경쟁 상태를 방지하세요사용자가 빠르게 연타하면 비동기 응답 순서가 뒤섞여 예기치 않은 상태가 될 수 있습니다. 동일 userId에 대한 토글이 진행 중이면 추가 요청을 차단하는 가드를 권장합니다.
아래처럼 in-flight 세트를 두고, 함수 초반 가드 및 완료 시 정리를 추가해 보세요:
클래스 필드 추가(함수 바깥):
// ViewModel 내부 private val togglingUserIds = mutableSetOf<Long>()함수 내부 변경:
fun toggleFollow(userId: Long, followedMessage: String, unfollowedMessage: String) { - val currentState = _uiState.value + // 이미 토글 중이면 무시 + if (!togglingUserIds.add(userId)) return + + val currentState = _uiState.value val userToUpdate = currentState.followings.find { it.userId == userId } ?: return val currentIsFollowing = userToUpdate.isFollowing @@ viewModelScope.launch { val requestType = !currentIsFollowing - userRepository.toggleFollow(followingUserId = userId, isFollowing = requestType) + userRepository.toggleFollow(followingUserId = userId, isFollowing = requestType) .onSuccess { response -> val serverState = response?.isFollowing ?: requestType _uiState.update { state -> state.copy(followings = state.followings.map { user -> if (user.userId == userId) { user.copy(isFollowing = serverState) } else { user } }) } + togglingUserIds.remove(userId) } .onFailure { _uiState.update { state -> state.copy(followings = state.followings.map { user -> if (user.userId == userId) { user.copy(isFollowing = currentIsFollowing) // 원래 상태로 복원 } else { user } }) } + togglingUserIds.remove(userId) } } }추가 개선 여지(선택):
- 실패 시 사용자 피드백(토스트나 스낵바)도 함께 노출하면 혼란을 줄일 수 있습니다.
app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt (1)
45-47: 예외 메시지 null 대비 기본 문구를 추가하세요exception.message가 null일 수 있습니다. 사용자에게 전달되는 메시지에 기본값을 지정하면 UX가 개선됩니다.
- _uiState.update { it.copy(isLoading = false, errorMessage = exception.message) } + _uiState.update { + it.copy( + isLoading = false, + errorMessage = exception.message ?: "일시적인 오류가 발생했어요. 잠시 후 다시 시도해 주세요." + ) + }app/src/main/java/com/texthip/thip/ui/signin/viewmodel/SignupAliasViewModel.kt (1)
34-53: 빈/불완전 데이터에 대한 방어 로직 제안imageUrl/aliasColor가 빈 문자열이거나 형식이 잘못된 경우(예: 색상 hex 누락) UI에서 깨질 수 있습니다. 기본값 보정을 고려해 보세요.
예시:
val roleCards = response?.aliasChoices?.map { aliasChoice -> RoleItem( genre = aliasChoice.aliasName, role = aliasChoice.categoryName, imageUrl = aliasChoice.imageUrl.takeIf { !it.isNullOrBlank() } ?: DEFAULT_ROLE_IMAGE_URL, roleColor = aliasChoice.aliasColor.takeIf { it?.startsWith("#") == true } ?: "#FFFFFF" ) } ?: emptyList()DEFAULT_ROLE_IMAGE_URL은 상수로 별도 정의하는 것을 권장합니다.
app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageEditScreen.kt (2)
138-147: Grid 항목에 안정 키 부여 권장itemsIndexed에 key를 부여하면 아이템 순서/선택 상태 변경 시 재구성 안정성이 높아집니다. genre가 중복될 가능성이 있다면 genre+role 조합 또는 서버에서 제공되는 고유 id 사용을 권장합니다.
- itemsIndexed(roleCards) { index, roleItem -> + itemsIndexed(roleCards, key = { _, item -> "${item.genre}-${item.role}" }) { index, roleItem -> RoleCard( genre = roleItem.genre, role = roleItem.role, imageUrl = roleItem.imageUrl, roleColor = roleItem.roleColor, selected = selectedIndex == index, onClick = { selectedIndex = index } ) }
130-137: userScrollEnabled = false로 인한 콘텐츠 클리핑 가능성 확인Grid 스크롤을 끈 상태에서 상위 Column도 스크롤 불가입니다. 작은 화면/큰 폰트 환경에서 카드가 2줄 이상이면 하단이 잘릴 수 있습니다. 상위 컨테이너 스크롤 처리 또는 Grid 스크롤 허용 여부를 검토해 주세요.
app/src/main/java/com/texthip/thip/ui/feed/screen/OthersSubscriptionListScreen.kt (2)
52-59: 무한 스크롤 감지의 안정성/가독성 개선 (키 지정 및 기본값).derivedStateOf를 remember로 감싸되 lazyListState를 키로 명시하면 의도가 더 명확하고, lastVisibleItemIndex 기본값을 -1로 두어 안전합니다.
- val isScrolledToEnd by remember { + val isScrolledToEnd by remember(lazyListState) { derivedStateOf { val layoutInfo = lazyListState.layoutInfo if (layoutInfo.totalItemsCount == 0) return@derivedStateOf false - val lastVisibleItemIndex = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0 + val lastVisibleItemIndex = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1 lastVisibleItemIndex >= layoutInfo.totalItemsCount - 1 } }
114-124: 'thipNum' 레이블과 followerCount 매핑이 의미적으로 어긋날 가능성.AuthorHeader의 'thipNum'은 리소스 상 "띱" 관련 카운트로 해석되는데, 여기서는 followerCount를 전달하고 있습니다. 레이블/카운트의 의미가 일치하는지 재확인이 필요합니다. 팔로워 수를 표시하려는 의도라면 별도 레이아웃/문구가 필요할 수 있습니다.
원하는 UI/문구가 팔로워 수라면 AuthorHeader 대신 전용 텍스트(예: "팔로워 N명") 표기를 고려하거나, AuthorHeader에 follower 전용 슬롯/플래그를 확장하는 것도 방법입니다. 필요 시 수정 패치 제안 가능합니다.
app/src/main/java/com/texthip/thip/ui/mypage/component/RoleCard.kt (5)
46-51: 변수명 의미 명확화: backgroundBrush → backgroundColor.변수가 실제로 Color 타입이므로 Brush처럼 오해되지 않도록 이름을 정리하는 것이 가독성에 좋습니다.
- val backgroundBrush = if (selected) { + val backgroundColor = if (selected) { colors.Black800 } else { colors.Black700 } @@ - .background(color = backgroundBrush, shape = RoundedCornerShape(12.dp)) + .background(color = backgroundColor, shape = RoundedCornerShape(12.dp))Also applies to: 57-57
44-45: 테마 일관성: 개별 색상 심볼(White/DarkGrey) 대신 ThipTheme.colors 사용 고려.현재 borderColor에서 White/DarkGrey를 직접 참조하고, 다른 곳은 colors.*를 사용하고 있습니다. 전반적으로 ThipTheme.colors를 일관되게 쓰면 다크모드/테마 확장 대응이 수월해집니다.
65-72: 이미지 로딩 UX 개선: crossfade/placeholder/error 적용 권장.네트워크 이미지 로딩 시 깜빡임/지연을 줄이기 위해 crossfade와 placeholder/error를 설정하는 것이 바람직합니다.
- AsyncImage( - model = imageUrl, + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(imageUrl) + .crossfade(true) + .build(), contentDescription = null, modifier = Modifier .align(Alignment.BottomStart), contentScale = ContentScale.Fit, alpha = bgAlpha )추가로 필요한 import:
import androidx.compose.ui.platform.LocalContext import coil.request.ImageRequest
87-88: hexToColor 연산 메모이제이션으로 불필요 재계산 방지.recomposition 시 매번 parseColor를 시도하게 됩니다. roleColor가 바뀔 때만 계산하도록 remember를 사용하세요.
- Text( + val roleTextColor = remember(roleColor) { hexToColor(roleColor) } + Text( text = role, style = typography.info_r400_s12, - color = hexToColor(roleColor) + color = roleTextColor )
65-72: 접근성(콘텐츠 설명) 고려.AsyncImage의 contentDescription이 null로 고정되어 있어 보조기술 사용자에게 정보가 전달되지 않습니다. 가능하다면 role/genre를 조합해 설명을 제공하는 것을 검토하세요.
app/src/main/java/com/texthip/thip/ui/signin/screen/SignupNicknameScreen.kt (2)
60-81: 다음 버튼 클릭 시 키보드 내리기(UX 미세 개선).다음 단계로 이동/검증 시 키보드를 내리면 UI 전환이 매끄럽습니다.
+ val focusManager = LocalFocusManager.current @@ InputTopAppBar( title = stringResource(R.string.settings_1), isRightButtonEnabled = isRightButtonEnabled, rightButtonName = stringResource(R.string.next), isLeftIconVisible = false, onLeftClick = {}, - onRightClick = onNextClick + onRightClick = { + focusManager.clearFocus() + onNextClick() + } )추가 import:
import androidx.compose.ui.platform.LocalFocusManager
96-106: 로딩 중 입력 비활성화 고려.서버 검증 중에는 TextField를 읽기 전용으로 전환하거나 버튼을 Progress로 교체하는 등의 피드백을 추가하면 중복 제출/수정으로 인한 혼선을 줄일 수 있습니다. WarningTextField가 enabled/readOnly를 지원한다면 isLoading에 따라 제어하는 것을 권장합니다.
app/src/main/java/com/texthip/thip/ui/signin/viewmodel/NicknameViewModel.kt (2)
15-22: 사용되지 않는 상태 필드 정리: isVerified 제거.현재 UI에서 isVerified를 사용하지 않고 있으며, navigateToNext로 흐름이 충분히 표현됩니다. 상태 단순화를 위해 제거를 권장합니다.
data class NicknameUiState( val isLoading: Boolean = false, val nickname: String = "", - val isVerified: Boolean? = null, val warningMessageResId: Int? = null, val errorMessage: String? = null, val navigateToNext: Boolean = false )
68-75: 에러 메시지 국제화 및 메시지 일관성.하드코딩된 기본 에러 문자열 대신 string 리소스를 사용하는 편이 좋습니다(번역/일관성). ViewModel에서 리소스 접근이 어렵다면 에러 메시지 키를 상태로 올리고 UI에서 stringResource로 치환하는 패턴을 고려하세요.
필요하시면 ResourceManager/UiText 패턴으로 리소스 안전하게 접근하는 샘플 구현 드리겠습니다.
app/src/main/java/com/texthip/thip/ui/feed/screen/MySubscriptionListScreen.kt (1)
49-53: NavController가 null인 경우 뒤로가기 아이콘 가시성도 함께 제어 권장navController를 nullable로 받는 변경은 좋습니다. 다만 navController가 없는 미리보기/단독 사용 시에도 DefaultTopAppBar에 뒤로가기 아이콘이 노출되면 UX가 어색할 수 있습니다. 컴포저블이 지원한다면 isLeftIconVisible 등을 navController != null로 연동하는 것을 권장합니다.
예시:
DefaultTopAppBar( onLeftClick = onNavigateBack, /* ... */ isLeftIconVisible = navController != null // 컴포넌트가 지원하는 경우 )Also applies to: 121-124
app/src/main/java/com/texthip/thip/data/repository/UserRepository.kt (2)
67-75: toggleFollow의 불리언 의미를 명확히 (파라미터 네이밍 개선 제안)isFollowing가 “현재 상태”인지 “수행할 액션(팔로우/언팔로우)”인지 혼동될 수 있습니다. 호출부에서 실수가 잦은 포인트이므로 의미가 드러나도록 네이밍을 개선하는 것을 권장합니다.
다음과 같이 파라미터명을 변경하고, FollowRequest에 그대로 매핑하세요(호출부 동반 수정 필요):
- suspend fun toggleFollow( - followingUserId: Long, - isFollowing: Boolean - ): Result<FollowResponse?> = runCatching { - val request = FollowRequest(type = isFollowing) + suspend fun toggleFollow( + followingUserId: Long, + shouldFollow: Boolean + ): Result<FollowResponse?> = runCatching { + val request = FollowRequest(type = shouldFollow) userService.toggleFollow(followingUserId, request) .handleBaseResponse() .getOrThrow() }
24-25: 중복되는 페이지 사이즈 기본값 상수화 권장여러 메소드에서 size: Int = 10을 반복 사용 중입니다. companion object의 상수로 추출하면 유지보수성이 좋아집니다.
예시:
@Singleton class UserRepository @Inject constructor( private val userService: UserService ) { + private companion object { + private const val DEFAULT_PAGE_SIZE = 10 + } //내 팔로잉 목록 조회 suspend fun getMyFollowings( cursor: String?, - size: Int = 10 + size: Int = DEFAULT_PAGE_SIZE ): Result<MyFollowingsResponse?> = runCatching { ... } suspend fun getOthersFollowers( userId: Long, cursor: String?, - size: Int = 10 + size: Int = DEFAULT_PAGE_SIZE ): Result<OthersFollowersResponse?> = runCatching { ... }Also applies to: 41-42
app/src/main/java/com/texthip/thip/ui/signin/screen/SignupGenreScreen.kt (2)
94-101: Grid 스크롤 비활성화는 콘텐츠 절단 위험userScrollEnabled = false로 인해 카드 수가 많을 때 스크롤이 되지 않아 하단 아이템이 보이지 않을 수 있습니다. 스크롤을 허용하거나 상위 컬럼에 verticalScroll을 부여하는 것을 권장합니다.
간단히 스크롤 허용:
- userScrollEnabled = false, + userScrollEnabled = true,또는 상위 레이아웃에 verticalScroll를 적용하는 대안도 고려해 주세요.
25-27: RoleItem이 mock 패키지에 위치실제 가입 플로우에서 사용하는 데이터 타입이라면 mock 패키지에서 분리해 공용 UI/도메인 패키지로 이동하는 것이 적절합니다. 네임스페이스만으로도 실제/목 데이터를 구분하기 용이해집니다.
app/src/main/java/com/texthip/thip/ui/mypage/viewmodel/MyPageViewModel.kt (2)
27-29: uiState의 공개 타입을 명시하면 가독성이 좋아집니다StateFlow 타입을 명시하면 IDE/리뷰 시 이해가 쉬워집니다.
- val uiState = _uiState.asStateFlow() + import kotlinx.coroutines.flow.StateFlow + val uiState: StateFlow<MyPageUiState> = _uiState.asStateFlow()
65-68: 로그아웃 TODO로그아웃 로직은 TODO로 남아 있습니다. 인증 토큰/세션 클리어 및 홈으로 내비게이션 등 요구사항 확정 시 구현 도와드릴 수 있습니다.
구현 스펙이 정리되면 알려주세요. ViewModel/Repository/AuthManager의 책임 분리를 고려하여 스캐폴딩 코드를 드리겠습니다.
app/src/main/java/com/texthip/thip/data/service/UserService.kt (1)
22-23: 서비스 레벨의 페이지 사이즈 기본값 상수화 고려Repository와 마찬가지로 size의 기본값(10)을 상수로 관리하면 일관성 유지에 도움이 됩니다(변경 시 누락 방지).
Also applies to: 32-33
app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageScreen.kt (4)
41-47: navController 파라미터가 사용되지 않습니다현재 함수 시그니처에
navController: NavController가 있지만 내부에서 사용되지 않습니다. 호출부와의 정합성만 유지된다면 제거하는 편이 간결합니다. 추후 내비게이션 용도로 쓸 계획이라면 그대로 두되, 경고를 피하려면 일시적으로@Suppress("UnusedParameter")를 고려하세요.가능한 수정안:
-fun MyPageScreen( - navController: NavController, - viewModel: MyPageViewModel = hiltViewModel(), +fun MyPageScreen( + viewModel: MyPageViewModel = hiltViewModel(), onNavigateToEditProfile: () -> Unit,주의: 호출부도 함께 수정되어야 합니다.
16-16: StateFlow 수집은 collectAsStateWithLifecycle 사용을 권장수명 주기와 연동되지 않은
collectAsState()는 화면이 비활성화된 동안에도 수집될 수 있어 낭비가 생길 수 있습니다.collectAsStateWithLifecycle()로 교체해 수명 주기 안전성을 확보하세요.적용 예:
-import androidx.compose.runtime.collectAsState +import androidx.lifecycle.compose.collectAsStateWithLifecycle- val uiState by viewModel.uiState.collectAsState() + val uiState by viewModel.uiState.collectAsStateWithLifecycle()Also applies to: 48-48
82-86: TopAppBar 아이콘 가시성 플래그 확인
leftIcon/rightIcon을 전달하지만isLeftIconVisible/isRightIconVisible기본값이 false여서 실제로 아이콘이 보이지 않습니다. 아이콘 노출 의도가 있다면 가시성 플래그를 명시하세요. 아니라면 아이콘 파라미터 전달을 제거해 의도를 명확히 해도 좋습니다.예:
-LeftNameTopAppBar( +LeftNameTopAppBar( title = stringResource(R.string.my_page), leftIcon = painterResource(R.drawable.ic_search), rightIcon = painterResource(R.drawable.ic_plus) + , isLeftIconVisible = true + , isRightIconVisible = true )
184-191: 앱 버전은 하드코딩 대신 BuildConfig에서 읽어오세요버전을 문자열 리소스로 고정(
version_1_0)하기보다BuildConfig.VERSION_NAME을 사용하면 유지보수가 쉬워집니다.적용 예:
- text = stringResource(R.string.version_1_0), + text = stringResource(R.string.version_format, BuildConfig.VERSION_NAME),추가로, 상단 import에 다음을 추가하세요:
import com.texthip.thip.BuildConfig그리고 strings.xml에 형식 문자열이 없다면:
<string name="version_format">버전 %1$s</string>
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (33)
app/src/main/java/com/texthip/thip/data/model/users/request/FollowRequest.kt(1 hunks)app/src/main/java/com/texthip/thip/data/model/users/request/NicknameRequest.kt(1 hunks)app/src/main/java/com/texthip/thip/data/model/users/response/AliasChoiceResponse.kt(1 hunks)app/src/main/java/com/texthip/thip/data/model/users/response/FollowResponse.kt(1 hunks)app/src/main/java/com/texthip/thip/data/model/users/response/MyFollowingsResponse.kt(2 hunks)app/src/main/java/com/texthip/thip/data/model/users/response/MyPageInfoResponse.kt(1 hunks)app/src/main/java/com/texthip/thip/data/model/users/response/NicknameResponse.kt(1 hunks)app/src/main/java/com/texthip/thip/data/repository/UserRepository.kt(2 hunks)app/src/main/java/com/texthip/thip/data/service/UserService.kt(2 hunks)app/src/main/java/com/texthip/thip/ui/common/cards/CardBookList.kt(6 hunks)app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt(4 hunks)app/src/main/java/com/texthip/thip/ui/feed/screen/MySubscriptionListScreen.kt(5 hunks)app/src/main/java/com/texthip/thip/ui/feed/screen/OthersSubscriptionListScreen.kt(1 hunks)app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt(1 hunks)app/src/main/java/com/texthip/thip/ui/feed/viewmodel/MySubscriptionViewModel.kt(2 hunks)app/src/main/java/com/texthip/thip/ui/feed/viewmodel/OthersSubscriptionViewModel.kt(1 hunks)app/src/main/java/com/texthip/thip/ui/mypage/component/BookContent.kt(2 hunks)app/src/main/java/com/texthip/thip/ui/mypage/component/FeedContent.kt(2 hunks)app/src/main/java/com/texthip/thip/ui/mypage/component/RoleCard.kt(5 hunks)app/src/main/java/com/texthip/thip/ui/mypage/component/SavedFeedCard.kt(3 hunks)app/src/main/java/com/texthip/thip/ui/mypage/mock/RoleItem.kt(1 hunks)app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageEditScreen.kt(3 hunks)app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageSaveScreen.kt(1 hunks)app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageScreen.kt(2 hunks)app/src/main/java/com/texthip/thip/ui/mypage/viewmodel/MyPageViewModel.kt(1 hunks)app/src/main/java/com/texthip/thip/ui/mypage/viewmodel/SavedBookViewModel.kt(0 hunks)app/src/main/java/com/texthip/thip/ui/navigator/extensions/MyPageNavigationExtensions.kt(1 hunks)app/src/main/java/com/texthip/thip/ui/navigator/navigations/MyPageNavigation.kt(1 hunks)app/src/main/java/com/texthip/thip/ui/navigator/routes/MyPageRoutes.kt(1 hunks)app/src/main/java/com/texthip/thip/ui/signin/screen/SignupGenreScreen.kt(4 hunks)app/src/main/java/com/texthip/thip/ui/signin/screen/SignupNicknameScreen.kt(5 hunks)app/src/main/java/com/texthip/thip/ui/signin/viewmodel/NicknameViewModel.kt(1 hunks)app/src/main/java/com/texthip/thip/ui/signin/viewmodel/SignupAliasViewModel.kt(1 hunks)
💤 Files with no reviewable changes (1)
- app/src/main/java/com/texthip/thip/ui/mypage/viewmodel/SavedBookViewModel.kt
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-01T07:19:01.239Z
Learnt from: rbqks529
PR: THIP-TextHip/THIP-Android#30
File: app/src/main/java/com/texthip/thip/ui/myPage/groupPage/GroupPageScreen.kt:34-37
Timestamp: 2025-07-01T07:19:01.239Z
Learning: GroupPageScreen의 MyPageViewModel은 임시 ViewModel로, 현재는 프로토타입 단계이므로 오류 처리 등의 추가 기능은 나중에 구현 예정입니다.
Applied to files:
app/src/main/java/com/texthip/thip/ui/mypage/viewmodel/MyPageViewModel.kt
🧬 Code Graph Analysis (9)
app/src/main/java/com/texthip/thip/ui/mypage/component/SavedFeedCard.kt (1)
app/src/main/java/com/texthip/thip/ui/common/header/ProfileBar.kt (1)
ProfileBar(29-101)
app/src/main/java/com/texthip/thip/ui/mypage/component/RoleCard.kt (1)
app/src/main/java/com/texthip/thip/utils/color/HexToColor.kt (1)
hexToColor(5-12)
app/src/main/java/com/texthip/thip/ui/feed/screen/OthersSubscriptionListScreen.kt (4)
app/src/main/java/com/texthip/thip/ui/common/topappbar/DefaultTopAppBar.kt (1)
DefaultTopAppBar(22-65)app/src/main/java/com/texthip/thip/ui/common/header/AuthorHeader.kt (1)
AuthorHeader(34-118)app/src/main/java/com/texthip/thip/utils/color/HexToColor.kt (1)
hexToColor(5-12)app/src/main/java/com/texthip/thip/ui/theme/Theme.kt (1)
ThipTheme(41-69)
app/src/main/java/com/texthip/thip/ui/mypage/component/BookContent.kt (1)
app/src/main/java/com/texthip/thip/ui/common/cards/CardBookList.kt (1)
CardBookList(34-97)
app/src/main/java/com/texthip/thip/ui/mypage/component/FeedContent.kt (1)
app/src/main/java/com/texthip/thip/ui/mypage/component/SavedFeedCard.kt (1)
SavedFeedCard(33-133)
app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageScreen.kt (6)
app/src/main/java/com/texthip/thip/ui/common/topappbar/LeftNameTopAppBar.kt (1)
LeftNameTopAppBar(26-73)app/src/main/java/com/texthip/thip/ui/common/header/AuthorHeader.kt (1)
AuthorHeader(34-118)app/src/main/java/com/texthip/thip/utils/color/HexToColor.kt (1)
hexToColor(5-12)app/src/main/java/com/texthip/thip/ui/common/buttons/MenuItemButton.kt (1)
MenuItemButton(28-74)app/src/main/java/com/texthip/thip/ui/mypage/viewmodel/MyPageViewModel.kt (2)
onLogoutClick(57-59)onDismissLogoutDialog(61-63)app/src/main/java/com/texthip/thip/ui/common/modal/DialogPopup.kt (1)
DialogPopup(22-71)
app/src/main/java/com/texthip/thip/ui/navigator/navigations/MyPageNavigation.kt (5)
app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageScreen.kt (1)
MyPageScreen(39-60)app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageEditScreen.kt (1)
EditProfileScreen(34-152)app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageSaveScreen.kt (1)
SavedScreen(46-119)app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageNotificationEditScreen.kt (1)
NotificationScreen(34-112)app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageLeavethipScreen.kt (1)
DeleteAccountScreen(41-167)
app/src/main/java/com/texthip/thip/ui/signin/screen/SignupGenreScreen.kt (2)
app/src/main/java/com/texthip/thip/ui/mypage/component/RoleCard.kt (1)
RoleCard(34-91)app/src/main/java/com/texthip/thip/ui/theme/Theme.kt (1)
ThipTheme(41-69)
app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageEditScreen.kt (1)
app/src/main/java/com/texthip/thip/ui/mypage/component/RoleCard.kt (1)
RoleCard(34-91)
🔇 Additional comments (32)
app/src/main/java/com/texthip/thip/data/model/users/request/NicknameRequest.kt (1)
5-8: 단순 DTO 설계 적절 — 추가 조치 불필요요청 페이로드를 최소 필드로 잘 정의하셨습니다. kotlinx.serialization 사용에도 문제 없습니다.
app/src/main/java/com/texthip/thip/ui/common/cards/CardBookList.kt (4)
13-15: 아이콘 관련 import 추가 적절Icon/IconButton 및 ImageVector.vectorResource 사용을 위한 import 추가가 목적에 부합합니다.
Also applies to: 23-25
104-104: 프리뷰에서 showBookmark 상태 추가 적절프리뷰에서 showBookmark 토글을 도입해 UI 변화를 확인할 수 있게 한 점 좋습니다.
114-117: CardBookList 호출부 업데이트 확인프리뷰 호출부에 showBookmark 전달이 반영되어 API 변경사항과 일치합니다.
41-41: API 시그니처 변경(매개변수 추가) — 호출부 영향 없음 확인rg로 호출부를 검사한 결과, 발견된 모든 CardBookList 호출은 명명 인자(title=, author=, imageUrl= 등)를 사용하고 있어 showBookmark 파라미터 추가로 인한 위치 기반 인자 충돌(컴파일 에러)은 발생하지 않습니다. true/false 포지셔널 불리언 사용 사례도 검색되지 않았습니다.
점검된 호출 위치:
- app/src/main/java/com/texthip/thip/ui/common/cards/CardBookList.kt — 정의(35), 프리뷰 호출(110–113)
- app/src/main/java/com/texthip/thip/ui/mypage/component/BookContent.kt — 호출(46–49)
- app/src/main/java/com/texthip/thip/ui/search/component/SearchActiveField.kt — 호출(62–65)
- app/src/main/java/com/texthip/thip/ui/search/component/SearchBookFilteredResult.kt — 호출(89–92)
변경된 시그니처(부분):
showBookmark: Boolean = false,app/src/main/java/com/texthip/thip/ui/mypage/component/BookContent.kt (2)
41-41: 키 지정 적절 — 리스트 안정성 향상itemsIndexed에서 key = book.id 지정으로 재구성/애니메이션 안정성이 높아집니다.
46-54: 북마크 노출 플래그 사용 적절CardBookList의 showBookmark = true 전달이 API 변경과 일치하고, onBookmarkClick 콜백도 명확합니다.
app/src/main/java/com/texthip/thip/data/model/users/response/MyFollowingsResponse.kt (2)
24-34: 최근 작성 유저 응답 모델 추가 적절MyRecentFollowingsResponse / RecentWriterList 도입이 서비스/레포지토리 변경과 일치합니다. 필드/타입 정의도 명확합니다.
16-16: userId 타입 변경(Int→Long) — 영향 범위 검증 완료FollowingList.userId가 Long으로 변경된 것을 확인했고, 해당 모델을 참조하는 주요 경로들을 모두 검사한 결과 컴파일 오류 가능성은 없습니다.
- 변경 위치
- app/src/main/java/com/texthip/thip/data/model/users/response/MyFollowingsResponse.kt
- @SerializedName("userId") val userId: Long,
- 검증한 참조(모두 Long 사용)
- app/src/main/java/com/texthip/thip/ui/feed/viewmodel/MySubscriptionViewModel.kt — toggleFollow(userId: Long), 비교(user.userId == userId) 등
- app/src/main/java/com/texthip/thip/ui/feed/screen/MySubscriptionListScreen.kt — onToggleFollow: (userId: Long, ...) 및 user.userId 전달
- app/src/main/java/com/texthip/thip/data/repository/UserRepository.kt — toggleFollow/getOthersFollowers 파라미터 Long
- app/src/main/java/com/texthip/thip/data/service/UserService.kt — @path("userId") userId: Long 등 API 시그니처
참고: 프로젝트 내에 RoomsUsersResponse.UserList, ReplyData/CommentData 등 일부 모델은 여전히 userId: Int로 선언되어 있으나 이들은 별도 응답/모델(mock)이며 FollowingList 변경과 직접적인 충돌은 없습니다.
변경된 코드 스니펫:
@SerializedName("userId") val userId: Long,app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageSaveScreen.kt (1)
111-111: 가변 높이 영역에 weight 적용 적절탭 영역 아래 컨텐츠 박스에 weight(1f)를 주어 남은 영역을 차지하도록 한 점이 적절합니다. 스크롤 가능한 콘텐츠가 더 안정적으로 배치됩니다.
app/src/main/java/com/texthip/thip/ui/navigator/routes/MyPageRoutes.kt (1)
7-11: LGTM — 직렬화 가능한 라우트 선언 추가 적절Typed route 선언으로 내비게이션 안정성이 좋아집니다. 특히 data object로 선언해 싱글턴/타입 안정성을 확보한 점이 좋습니다.
app/src/main/java/com/texthip/thip/ui/mypage/component/SavedFeedCard.kt (2)
49-49: 패딩 수평 전환은 OK카드 좌우 패딩만 적용하도록 변경된 점은 레이아웃 일관성 측면에서 자연스럽습니다.
168-172: Preview 호출부 시그니처 정리
SavedFeedCard의profileImage파라미터 제거에 맞춰 Preview 호출부도 잘 정리되었습니다. 별도 수정 사항 없습니다.Also applies to: 183-187
app/src/main/java/com/texthip/thip/ui/mypage/component/FeedContent.kt (1)
36-36: 인덱스 기반 렌더링 + 안정 키 적용 좋아요itemsIndexed에 stable key(feed.id)를 부여해 스크롤/애니메이션 안정성이 좋아졌습니다.
app/src/main/java/com/texthip/thip/ui/feed/viewmodel/MySubscriptionViewModel.kt (1)
74-121: 낙관적 업데이트와 실패 롤백 흐름이 깔끔합니다UI 즉시 반영 → 서버 동기화 → 성공 시 서버 상태로 정합성 맞춤 → 실패 시 원복 흐름이 올바르게 구현되었습니다. 토스트 메시지 선택도 이전 상태에 기반해 일관적입니다.
app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt (1)
33-49: 간결하고 안전한 상태 관리, LGTM로딩 → 성공/실패 분기 및 불변 상태 업데이트 패턴이 깔끔합니다.
app/src/main/java/com/texthip/thip/ui/signin/viewmodel/SignupAliasViewModel.kt (1)
39-48: 서버 DTO → UI 모델 매핑 적절서버의 AliasChoice를 UI에 필요한 RoleItem으로 필요한 필드만 매핑하여 저장했습니다. 불필요한 의존 없이 ViewModel에서 매핑하는 방향 좋습니다.
app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageEditScreen.kt (1)
38-67: RoleItem 스키마 변경 반영, 호출부 정리 👍imageUrl/roleColor(String) 기반으로 RoleCard 호출부가 깔끔히 정리되었습니다. 미리보기/더미 데이터에서도 새 스키마를 잘 반영했습니다.
app/src/main/java/com/texthip/thip/ui/feed/screen/OthersSubscriptionListScreen.kt (1)
137-150: 로딩 인디케이터 위치/표시 조건 적절.하단에만 추가 페이지 로딩 인디케이터를 렌더링하고, 마지막 페이지가 아닐 때만 표시하는 조건이 정확합니다. UX 관점에서도 자연스럽습니다.
app/src/main/java/com/texthip/thip/ui/navigator/navigations/MyPageNavigation.kt (1)
21-29: 탭 내 상세 라우팅 연동 LGTM.
- 메인 마이페이지 엔트리와 4개 상세 화면(Edit/Save/NotificationEdit/LeaveThip) 라우트 연결이 일관됩니다.
- MyPageScreen의 콜백 기반 네비게이션 설계로 화면 간 의존도를 낮춘 점 좋습니다.
Also applies to: 31-42
app/src/main/java/com/texthip/thip/ui/signin/screen/SignupNicknameScreen.kt (2)
40-49: 네비게이션/에러 처리 플로우 적절.
- navigateToNext와 errorMessage를 관찰해 단발 이벤트를 처리하고, onNavigated로 플래그 리셋까지 해주는 구조가 안정적입니다.
- collectAsStateWithLifecycle 사용으로 라이프사이클 안전성도 확보되었습니다.
51-57: 입력 검증/상태 분리 깔끔.
- Presentational 컴포넌트로 분리한 점, 경고 메시지의 리소스 id를 통해 텍스트를 가져오는 방식 모두 일관되고 테스트 용이합니다.
- 버튼 활성화 조건(isRightButtonEnabled)도 명확합니다.
Also applies to: 96-106
app/src/main/java/com/texthip/thip/ui/feed/screen/MySubscriptionListScreen.kt (2)
90-91: userId 타입(Long) 전환 및 콜백 시그니처 정합성 확인onToggleFollow의 시그니처(Long, String)와 호출부(user.userId, user.nickname)가 일관성 있게 반영되어 있습니다. 목록 key에도 user.userId를 사용해 안정적입니다.
Also applies to: 159-160
195-204: 미리보기 목 데이터의 Long 전환 적절mockUsers에서 userId를 Long으로 변환해 최신 모델에 맞췄습니다. 미리보기와 실제 UI 상태 모델이 일치합니다.
app/src/main/java/com/texthip/thip/data/repository/UserRepository.kt (2)
31-35: 신규 메소드들의 응답 처리 패턴 일관성 양호handleBaseResponse().getOrThrow()를 runCatching으로 감싼 패턴이 일관적이고, 레포지토리 레벨에서 Result로 노출하는 결정도 합리적입니다.
Also applies to: 37-66
71-73: 확인 필요 — FollowRequest(type) 직렬화 키가 서버 스펙과 일치하는지 확인해주세요검증 결과 요약:
- app/src/main/java/com/texthip/thip/data/model/users/request/FollowRequest.kt
- @serializable data class FollowRequest(val type: Boolean) → 기본 직렬화 키는 "type"
- app/src/main/java/com/texthip/thip/data/service/UserService.kt
- toggleFollow(@Body request: FollowRequest)로 POST 요청 사용
- app/src/main/java/com/texthip/thip/data/repository/UserRepository.kt
- FollowRequest(type = isFollowing) 생성 후 전송
- app/src/main/java/com/texthip/thip/data/di/NetworkModule.kt
- kotlinx.serialization Json.asConverterFactory 사용 → Retrofit 요청 바디는 kotlinx.serialization 규칙(프로퍼티 이름 그대로)으로 직렬화되어 {"type": true/false} 로 전송됩니다
요청사항: 서버(또는 API 문서/백엔드팀)에서 해당 엔드포인트가 boolean 필드 "type"을 기대하는지 확인해주세요. 만약 서버가 다른 필드명 또는 문자열(action: "follow"/"unfollow" 등)을 요구하면 FollowRequest에 @SerialName을 붙이거나 타입/값을 변경해야 합니다.
app/src/main/java/com/texthip/thip/ui/signin/screen/SignupGenreScreen.kt (3)
32-49: VM 기반 상태수집(collectAsStateWithLifecycle) 전환 좋습니다ViewModel로 상태를 단일 소스로 관리하고, 선택/이동 액션을 명확히 분리한 구조가 깔끔합니다.
102-111: 아이템 바인딩과 선택 상태 처리 적절itemsIndexed + 선택 인덱스 비교로 선택상태 반영이 명확합니다. imageUrl/hex color로의 마이그레이션도 RoleCard 변경과 잘 맞습니다.
121-131: 프리뷰 상태 구성 적절프리뷰용 RoleItem/UiState 구성으로 디자이너/리뷰용 미리보기 확인이 용이합니다.
app/src/main/java/com/texthip/thip/data/service/UserService.kt (1)
26-51: 신규 엔드포인트 정의 적절Retrofit 어노테이션과 요청/응답 DTO 매핑이 깔끔합니다. getRecentWriters, getUserFollowers, getMyPage, checkNickname, getAliasChoices, toggleFollow 모두 시그니처가 명확합니다.
app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageScreen.kt (1)
94-101: UI 상태 기반 바인딩과 색상 변환 처리가 적절합니다
uiState에서 값을 직접 바인딩하고,hexToColor를 통해 방어적으로 색상을 처리한 부분이 깔끔합니다. 구성요소 재사용성도 좋습니다.app/src/main/java/com/texthip/thip/ui/feed/viewmodel/OthersSubscriptionViewModel.kt (1)
28-28: SavedStateHandle 검증 필요 — userId 전달 보장 확인 요청실행한 검색 스크립트가 출력되지 않아 자동 확인에 실패했습니다. 수동으로 다음 항목을 확인해 주세요.
문제 코드
app/src/main/java/com/texthip/thip/ui/feed/viewmodel/OthersSubscriptionViewModel.kt private val userId: Long = requireNotNull(savedStateHandle["userId"])검증 포인트
- 네비게이션 그래프(res/navigation/*.xml)에 userId가 반드시 정의되어 있는지 (예: ).
- 화면 전환(또는 SafeArgs 사용) 지점에서 항상 userId를 전달하는지(예: Directions.action...(userId) 또는 bundleOf("userId", ...)/navigate()).
- SafeArgs를 쓰는 경우 생성된 Directions/Args에 userId 파라미터가 있는지 확인.
권장 대응
- 네비그래프에서 필수 인자로 선언해 전달을 보장하거나,
- 전달이 불확실하면 requireNotNull → savedStateHandle.get("userId") ?: (에러 처리 / UI로 노출) 등으로 안전하게 처리.
로컬에서 돌려볼 점검 스크립트(리포지토리 루트에서 실행)
#!/bin/bash rg -n --type=xml 'name="userId"' -S -g '!**/build/**' rg -n --type=xml 'app:argType="long"' -S -g '!**/build/**' rg -n --type=kotlin -C3 'userId' -S -g '!**/build/**'실행 결과를 붙여 보내주시면 제가 재검증해 드리겠습니다.
| fun OthersSubsciptionListScreen( | ||
| navController: NavController, | ||
| viewModel: OthersSubscriptionViewModel = hiltViewModel() | ||
| ) { |
There was a problem hiding this comment.
🛠️ Refactor suggestion
타이포: Subsciption → Subscription으로 일괄 수정 권장 (공개 API 네이밍).
공개 Composable 이름에 오타가 반복됩니다. 네이밍은 공개 API의 일부이므로 조기에 바로잡는 것이 좋습니다. 함수/프리뷰/호출부를 일관되게 정정해주세요.
적용 예시:
-fun OthersSubsciptionListScreen(
+fun OthersSubscriptionListScreen(
navController: NavController,
viewModel: OthersSubscriptionViewModel = hiltViewModel()
) {
@@
- OthersSubsciptionContent(
+ OthersSubscriptionContent(
uiState = uiState,
lazyListState = lazyListState,
onNavigateBack = { navController.popBackStack() }
)
@@
-fun OthersSubsciptionContent(
+fun OthersSubscriptionContent(
uiState: OthersSubscriptionUiState,
lazyListState: LazyListState,
onNavigateBack: () -> Unit
) {
@@
-private fun OthersSubsciptionListScreenPrev() {
+private fun OthersSubscriptionListScreenPrev() {
val mockUsers = (1..10).map {
FollowerList(
userId = it.toLong(),Also applies to: 67-72, 74-79, 155-181
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/ui/feed/screen/OthersSubscriptionListScreen.kt
around lines 45-48 (and also at the other locations noted: 67-72, 74-79,
155-181), there is a repeated typo in the public Composable name
"OthersSubsciptionListScreen" and related previews/usages; rename all
occurrences from "Subsciption" to "Subscription" (function name, preview
functions, any calls/imports, and references) so the public API is consistent,
update KDoc or comments if present, and run a project-wide refactor/rename to
ensure callers and tests are updated to the corrected spelling.
| LaunchedEffect(isScrolledToEnd) { | ||
| if (isScrolledToEnd && !uiState.isLoading && !uiState.isLastPage) { | ||
| viewModel.fetchOthersFollowers() | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
최초 진입 시 초기 페이지 로딩 보장.
현재는 스크롤 바닥 감지 시에만 fetchOthersFollowers()가 호출됩니다. ViewModel에서 init에서 불러오지 않는다면 첫 화면이 비어 보일 수 있습니다. 최초 진입 시 1회 로딩을 보장하는 트리거를 추가하는 것을 권장합니다.
+ LaunchedEffect(Unit) {
+ if (!uiState.isLoading && uiState.followers.isEmpty()) {
+ viewModel.fetchOthersFollowers()
+ }
+ }초기 로딩이 이미 ViewModel(init 등)에서 수행된다면 이 블록은 생략해도 됩니다. 확인 부탁드립니다. 필요 시 코드베이스에서 OthersSubscriptionViewModel 초기 로딩 여부를 점검해보세요. 아래 스크립트로 확인할 수 있습니다.
🏁 Script executed:
#!/bin/bash
# OthersSubscriptionViewModel 초기 로딩 여부 확인(초기 fetch 호출/데이터 흐름 유무)
rg -nP -C3 '(?s)class\s+OthersSubscriptionViewModel\b.*' --type=kt
rg -nP 'fetchOthersFollowers\s*\(' --type=kt
rg -nP 'init\s*\{|init\s*\(' --type=ktLength of output: 237
추가 검증을 위해 아래 스크립트를 실행합니다.
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "PWD: $(pwd)"
echo "---- Search for OthersSubscriptionViewModel class ----"
rg -nP -C3 'class\s+OthersSubscriptionViewModel\b' -S || true
echo "---- Search for fetchOthersFollowers occurrences ----"
rg -nP -n -C3 'fetchOthersFollowers\s*\(' -S || true
echo "---- Search for init blocks in Kotlin files (init {) ----"
rg -nP -n -C3 'init\s*\{' -S --glob '!**/build/**' || true
echo "---- Search for 'followers' identifier (property/usages) ----"
rg -nP -n -C3 '\bfollowers\b' -S --glob '!**/build/**' || true
echo "---- Locate the screen file ----"
rg -n --hidden -n 'OthersSubscriptionListScreen\.kt' -S || true
if [ -f "app/src/main/java/com/texthip/thip/ui/feed/screen/OthersSubscriptionListScreen.kt" ]; then
echo "---- Showing top 200 lines of OthersSubscriptionListScreen.kt ----"
sed -n '1,200p' app/src/main/java/com/texthip/thip/ui/feed/screen/OthersSubscriptionListScreen.kt
fiLength of output: 24877
검증 결과 — 초기 로딩은 ViewModel에서 이미 수행됩니다 / ViewModel 수정 필요
OthersSubscriptionViewModel.init에서 fetchOthersFollowers(isInitial = true)를 호출하므로 화면에 LaunchedEffect(Unit)를 추가할 필요가 없습니다. 다만 ViewModel의 팔로워 병합 로직에 명백한 오타(버그)가 발견되어 수정이 필요합니다.
- app/src/main/java/com/texthip/thip/ui/feed/viewmodel/OthersSubscriptionViewModel.kt — init에서 초기 로드 호출 (라인 ~34–36).
- app/src/main/java/com/texthip/thip/ui/feed/screen/OthersSubscriptionListScreen.kt — LaunchedEffect(isScrolledToEnd) (라인 ~61–66): 스크롤 끝 감지용으로 유지 권장.
- app/src/main/java/com/texthip/thip/ui/feed/viewmodel/OthersSubscriptionViewModel.kt — 팔로워 병합 로직(라인 ~54): 현재 코드
- followers = if (isInitial) it.followers else currentState.followers + currentState.followers,
+ followers = if (isInitial) it.followers else currentState.followers + it.followers,요약: 화면에 초기 로딩 트리거를 추가할 필요 없음(제안 취소). 대신 ViewModel의 위 병합 라인을 수정해 주세요.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/ui/feed/viewmodel/OthersSubscriptionViewModel.kt
around line ~54, the follower-merge logic uses the wrong collection (typo) when
merging paged results; replace the incorrect variable with the newly fetched
followers list so you append/merge newFollowers into the existing followers
state (e.g., currentFollowers + newFollowers), deduplicate if necessary, and
update loading/last-page flags accordingly; leave the screen LaunchedEffect
as-is for scroll-end detection since initial load is handled in init.
| fun fetchOthersFollowers(isInitial: Boolean = false) { | ||
| if (_uiState.value.isLoading || (!isInitial && _uiState.value.isLastPage)) { | ||
| return | ||
| } | ||
|
|
||
| viewModelScope.launch { | ||
| _uiState.update { it.copy(isLoading = true) } | ||
| val cursorToFetch = if (isInitial) null else nextCursor | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
요청 가드 및 초기화 개선: nextCursor, errorMessage 처리 권장
- 마지막 페이지 이후에도
nextCursor == null이면 불필요한 호출을 막아야 합니다. - 새 요청 시 이전 에러 메시지를 초기화하지 않아 에러 상태가 잔류할 수 있습니다.
- 초기 로드(isInitial) 시
nextCursor를 명시적으로 초기화해 재로딩 시 안전하게 시작하세요.
수정안:
- fun fetchOthersFollowers(isInitial: Boolean = false) {
- if (_uiState.value.isLoading || (!isInitial && _uiState.value.isLastPage)) {
+ fun fetchOthersFollowers(isInitial: Boolean = false) {
+ if (_uiState.value.isLoading || (!isInitial && (_uiState.value.isLastPage || nextCursor == null))) {
return
}
viewModelScope.launch {
- _uiState.update { it.copy(isLoading = true) }
+ _uiState.update { it.copy(isLoading = true, errorMessage = null) }
+ if (isInitial) nextCursor = null
val cursorToFetch = if (isInitial) null else nextCursor📝 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.
| fun fetchOthersFollowers(isInitial: Boolean = false) { | |
| if (_uiState.value.isLoading || (!isInitial && _uiState.value.isLastPage)) { | |
| return | |
| } | |
| viewModelScope.launch { | |
| _uiState.update { it.copy(isLoading = true) } | |
| val cursorToFetch = if (isInitial) null else nextCursor | |
| fun fetchOthersFollowers(isInitial: Boolean = false) { | |
| if (_uiState.value.isLoading || (!isInitial && (_uiState.value.isLastPage || nextCursor == null))) { | |
| return | |
| } | |
| viewModelScope.launch { | |
| _uiState.update { it.copy(isLoading = true, errorMessage = null) } | |
| if (isInitial) nextCursor = null | |
| val cursorToFetch = if (isInitial) null else nextCursor |
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/ui/feed/viewmodel/OthersSubscriptionViewModel.kt
around lines 38 to 46, the fetchOthersFollowers function should avoid requests
when nextCursor is null after last page, clear previous errorMessage on new
requests, and reset nextCursor on initial loads; update the pre-call guard to
also return if nextCursor == null when not isInitial and isLastPage check isn't
sufficient, set _uiState.update to clear errorMessage at the start of a new
request, and if isInitial explicitly set nextCursor to null (or its initial
value) before proceeding so reloads start from a clean state.
| result.onSuccess { data -> | ||
| data?.let { | ||
| _uiState.update { currentState -> | ||
| currentState.copy( | ||
| isLoading = false, | ||
| followers = if (isInitial) it.followers else currentState.followers + currentState.followers, | ||
| totalCount = it.totalFollowerCount, | ||
| isLastPage = it.isLast | ||
| ) | ||
| } | ||
| nextCursor = it.nextCursor | ||
| } | ||
| }.onFailure { exception -> |
There was a problem hiding this comment.
data == null 성공 케이스에서 isLoading이 해제되지 않는 잠김 버그
Result.onSuccess { data -> ... }에서 data가 null인 경우 _uiState.update { ... isLoading = false }가 호출되지 않아 로딩 상태가 영구히 유지될 수 있습니다. 성공-널 케이스를 명시적으로 처리해 로딩을 해제하세요. 또한 마지막 페이지로 간주하도록 isLastPage 설정도 고려됩니다.
수정안:
- result.onSuccess { data ->
- data?.let {
- _uiState.update { currentState ->
- currentState.copy(
- isLoading = false,
- followers = if (isInitial) it.followers else currentState.followers + currentState.followers,
- totalCount = it.totalFollowerCount,
- isLastPage = it.isLast
- )
- }
- nextCursor = it.nextCursor
- }
+ result.onSuccess { data ->
+ if (data == null) {
+ // 더 이상 받을 데이터가 없다고 간주
+ _uiState.update { it.copy(isLoading = false, isLastPage = true) }
+ return@onSuccess
+ }
+ _uiState.update { currentState ->
+ currentState.copy(
+ isLoading = false,
+ followers = if (isInitial) data.followers else currentState.followers + data.followers,
+ totalCount = data.totalFollowerCount,
+ isLastPage = data.isLast
+ )
+ }
+ nextCursor = data.nextCursor
}.onFailure { exception ->📝 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.
| result.onSuccess { data -> | |
| data?.let { | |
| _uiState.update { currentState -> | |
| currentState.copy( | |
| isLoading = false, | |
| followers = if (isInitial) it.followers else currentState.followers + currentState.followers, | |
| totalCount = it.totalFollowerCount, | |
| isLastPage = it.isLast | |
| ) | |
| } | |
| nextCursor = it.nextCursor | |
| } | |
| }.onFailure { exception -> | |
| result.onSuccess { data -> | |
| if (data == null) { | |
| // 더 이상 받을 데이터가 없다고 간주 | |
| _uiState.update { it.copy(isLoading = false, isLastPage = true) } | |
| return@onSuccess | |
| } | |
| _uiState.update { currentState -> | |
| currentState.copy( | |
| isLoading = false, | |
| followers = if (isInitial) data.followers else currentState.followers + data.followers, | |
| totalCount = data.totalFollowerCount, | |
| isLastPage = data.isLast | |
| ) | |
| } | |
| nextCursor = data.nextCursor | |
| }.onFailure { exception -> |
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/ui/feed/viewmodel/OthersSubscriptionViewModel.kt
around lines 49 to 61, the onSuccess branch ignores the case where data == null
so isLoading is never cleared; update the onSuccess handler to explicitly handle
a null data case by calling _uiState.update to set isLoading = false (and set
isLastPage = true if appropriate), avoid changing followers or nextCursor when
data is null, and keep the existing path for non-null data unchanged.
| _uiState.update { currentState -> | ||
| currentState.copy( | ||
| isLoading = false, | ||
| followers = if (isInitial) it.followers else currentState.followers + currentState.followers, | ||
| totalCount = it.totalFollowerCount, | ||
| isLastPage = it.isLast | ||
| ) |
There was a problem hiding this comment.
페이지네이션 합치기 로직 버그: 기존 목록을 자기 자신과 중복 결합하고 있습니다
현재 구현은 currentState.followers + currentState.followers로 되어 있어, 새로 받은 페이지가 아닌 기존 리스트를 한 번 더 이어붙입니다. 이는 중복 데이터와 무한 증가를 유발합니다. 새 데이터인 it.followers를 결합해야 합니다.
수정안:
- followers = if (isInitial) it.followers else currentState.followers + currentState.followers,
+ followers = if (isInitial) it.followers else currentState.followers + it.followers,📝 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.
| _uiState.update { currentState -> | |
| currentState.copy( | |
| isLoading = false, | |
| followers = if (isInitial) it.followers else currentState.followers + currentState.followers, | |
| totalCount = it.totalFollowerCount, | |
| isLastPage = it.isLast | |
| ) | |
| _uiState.update { currentState -> | |
| currentState.copy( | |
| isLoading = false, | |
| followers = if (isInitial) it.followers else currentState.followers + it.followers, | |
| totalCount = it.totalFollowerCount, | |
| isLastPage = it.isLast | |
| ) |
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/ui/feed/viewmodel/OthersSubscriptionViewModel.kt
around lines 51 to 57, the pagination merge incorrectly appends the existing
list to itself (currentState.followers + currentState.followers) causing
duplicates and runaway growth; replace that expression so that when not initial
you append the newly fetched page (it.followers) to the existing list (e.g.,
currentState.followers + it.followers) and keep the initial-case assignment as
it.followers, ensuring list immutability/nullability is respected when
concatenating.
| if (uiState.showLogoutDialog) { | ||
| Dialog(onDismissRequest = { onDismissLogoutDialog() }) { | ||
| DialogPopup( | ||
| modifier = Modifier | ||
| .fillMaxWidth(), | ||
| modifier = Modifier.fillMaxWidth(), | ||
| title = stringResource(R.string.log_out), | ||
| description = stringResource(R.string.logout_description), | ||
| onCancel = { showLogoutDialog = false }, | ||
| onConfirm = { | ||
| showLogoutDialog = false | ||
| // TODO: 로그아웃 로직 | ||
| } | ||
| onCancel = { onDismissLogoutDialog() }, | ||
| onConfirm = { onConfirmLogout() } | ||
| ) |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
로그아웃 confirm 핸들러 구현 확인 필요
onConfirmLogout = { viewModel.confirmLogout() }가 호출되고 있는데, MyPageViewModel에 해당 메서드가 실제로 존재하고 토큰/세션 정리 및 후속 내비게이션(예: 로그인 화면 이동)이 구현되어 있는지 확인이 필요합니다.
아래 스크립트로 뷰모델에 구현 여부를 빠르게 확인할 수 있습니다.
🏁 Script executed:
#!/bin/bash
# MyPageViewModel에 confirmLogout 존재 여부 및 구현 확인
rg -nP -C3 '(?s)class\s+MyPageViewModel\b.*?\bconfirmLogout\s*\(' --type=kotlin
# confirmLogout에서 실제 로그아웃 로직(토큰 삭제 등) 수행하는지 탐색
rg -nP -C2 'confirmLogout\s*\(.*\)\s*\{|fun\s+confirmLogout\s*\(' --type=kotlinLength of output: 674
확인 결과: MyPageViewModel.confirmLogout 구현 필요 (로그아웃 로직 미구현)
확인: app/src/main/java/com/texthip/thip/ui/mypage/viewmodel/MyPageViewModel.kt에 fun confirmLogout()가 존재하지만 내부에 "TODO: 실제 로그아웃 로직 구현" 주석만 있어 토큰/세션 정리 및 로그인 화면 이동이 미구현 상태입니다.
-
위치
- app/src/main/java/com/texthip/thip/ui/mypage/viewmodel/MyPageViewModel.kt — fun confirmLogout() (약 65–67행)
- 현재 내용 예: fun confirmLogout() { _uiState.update { it.copy(showLogoutDialog = false) } // TODO: 실제 로그아웃 로직 구현 }
-
조치 권고
- confirmLogout에서 토큰/세션 삭제(SharedPreferences/Datastore 등), 인증 상태 갱신 또는 서버 로그아웃 호출(필요 시), 그리고 로그인 화면으로의 내비게이션 또는 앱 상태 리셋을 구현하세요. MypageScreen의 onConfirmLogout이 viewModel.confirmLogout()을 호출하므로 이 구현이 반드시 필요합니다.
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/ui/mypage/viewmodel/MyPageViewModel.kt
around lines ~65–67, confirmLogout currently only hides the dialog and contains
a TODO; implement logout by clearing stored auth tokens/sessions (Datastore or
SharedPreferences), optionally call server logout API if required, update
authentication state (e.g., emit UiState/authenticated = false or update shared
auth repository), and trigger navigation to the login screen (via a navigation
event/side-effect from the ViewModel or resetting app state) so that
MypageScreen.onConfirmLogout() causes a full logout flow.
| val aliasColor: String = "#0XFFFFFF", | ||
| val errorMessage: String? = null, | ||
| val showLogoutDialog: Boolean = false | ||
| ) |
There was a problem hiding this comment.
aliasColor 기본값이 잘못된 HEX 포맷입니다
"#0XFFFFFF"는 잘못된 포맷으로 파싱 에러(또는 예상치 못한 색상) 가능성이 큽니다. "#FFFFFF" 등 유효한 HEX로 변경하세요.
- val aliasColor: String = "#0XFFFFFF",
+ val aliasColor: String = "#FFFFFF",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| val aliasColor: String = "#0XFFFFFF", | |
| val errorMessage: String? = null, | |
| val showLogoutDialog: Boolean = false | |
| ) | |
| val aliasColor: String = "#FFFFFF", | |
| val errorMessage: String? = null, | |
| val showLogoutDialog: Boolean = false | |
| ) |
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/ui/mypage/viewmodel/MyPageViewModel.kt
around lines 18 to 21, the default aliasColor value uses an invalid HEX string
"#0XFFFFFF" which can cause parsing errors; change it to a valid HEX format such
as "#FFFFFF" (or another valid 6-hex digit string with leading '#') so color
parsing works correctly and update any related tests or usages if they assume
the old value.
| .onSuccess { data -> | ||
| data?.let { | ||
| _uiState.update { | ||
| it.copy( | ||
| isLoading = false, | ||
| profileImageUrl = data.profileImageUrl, | ||
| nickname = data.nickname, | ||
| aliasName = data.aliasName, | ||
| aliasColor = data.aliasColor | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| .onFailure { exception -> | ||
| _uiState.update { it.copy(isLoading = false, errorMessage = exception.message) } | ||
| } |
There was a problem hiding this comment.
API 성공이지만 data가 null인 경우 로딩이 해제되지 않음
onSuccess에서 data가 null이면 isLoading을 false로 내리지 않아 UI가 로딩 상태에 고정될 수 있습니다. null 응답에 대한 처리(로딩 해제 및 에러 메시지 설정)가 필요합니다.
다음과 같이 보완을 제안드립니다:
- .onSuccess { data ->
- data?.let {
- _uiState.update {
- it.copy(
- isLoading = false,
- profileImageUrl = data.profileImageUrl,
- nickname = data.nickname,
- aliasName = data.aliasName,
- aliasColor = data.aliasColor
- )
- }
- }
- }
+ .onSuccess { data ->
+ if (data != null) {
+ _uiState.update {
+ it.copy(
+ isLoading = false,
+ profileImageUrl = data.profileImageUrl,
+ nickname = data.nickname,
+ aliasName = data.aliasName,
+ aliasColor = data.aliasColor
+ )
+ }
+ } else {
+ _uiState.update {
+ it.copy(
+ isLoading = false,
+ errorMessage = "Empty my-page response"
+ )
+ }
+ }
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| .onSuccess { data -> | |
| data?.let { | |
| _uiState.update { | |
| it.copy( | |
| isLoading = false, | |
| profileImageUrl = data.profileImageUrl, | |
| nickname = data.nickname, | |
| aliasName = data.aliasName, | |
| aliasColor = data.aliasColor | |
| ) | |
| } | |
| } | |
| } | |
| .onFailure { exception -> | |
| _uiState.update { it.copy(isLoading = false, errorMessage = exception.message) } | |
| } | |
| .onSuccess { data -> | |
| if (data != null) { | |
| _uiState.update { | |
| it.copy( | |
| isLoading = false, | |
| profileImageUrl = data.profileImageUrl, | |
| nickname = data.nickname, | |
| aliasName = data.aliasName, | |
| aliasColor = data.aliasColor | |
| ) | |
| } | |
| } else { | |
| _uiState.update { | |
| it.copy( | |
| isLoading = false, | |
| errorMessage = "Empty my-page response" | |
| ) | |
| } | |
| } | |
| } | |
| .onFailure { exception -> | |
| _uiState.update { it.copy(isLoading = false, errorMessage = exception.message) } | |
| } |
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/ui/mypage/viewmodel/MyPageViewModel.kt
around lines 38 to 53, onSuccess currently only updates UI when data != null so
a null response leaves isLoading true; update onSuccess to handle the null case
by always setting isLoading = false and setting an appropriate errorMessage (or
clearing fields) when data is null so the UI is not stuck loading; ensure the
_uiState.update is called for both non-null and null data paths to set isLoading
false and provide a clear error or fallback state.
| fun checkNickname() { | ||
| if (_uiState.value.isLoading) return | ||
| if (_uiState.value.nickname.isBlank()) return | ||
|
|
||
| viewModelScope.launch { | ||
| _uiState.update { | ||
| it.copy( | ||
| isLoading = true, | ||
| warningMessageResId = null, | ||
| errorMessage = null | ||
| ) | ||
| } | ||
|
|
||
| userRepository.checkNickname(_uiState.value.nickname) | ||
| .onSuccess { response -> |
There was a problem hiding this comment.
🛠️ Refactor suggestion
닉네임 전송 전 공백 정리(trim) 및 검증 일치.
서버와의 검증 일관성을 위해 전송 전에 trim한 값을 사용하세요. isBlank 체크도 동일한 기준을 적용하면 UX가 명확해집니다.
fun checkNickname() {
if (_uiState.value.isLoading) return
- if (_uiState.value.nickname.isBlank()) return
+ val nickname = _uiState.value.nickname.trim()
+ if (nickname.isBlank()) return
viewModelScope.launch {
_uiState.update {
it.copy(
isLoading = true,
warningMessageResId = null,
errorMessage = null
)
}
- userRepository.checkNickname(_uiState.value.nickname)
+ userRepository.checkNickname(nickname)
.onSuccess { response ->
if (response?.isVerified == true) {
_uiState.update { it.copy(isLoading = false, navigateToNext = true) }
} else {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| fun checkNickname() { | |
| if (_uiState.value.isLoading) return | |
| if (_uiState.value.nickname.isBlank()) return | |
| viewModelScope.launch { | |
| _uiState.update { | |
| it.copy( | |
| isLoading = true, | |
| warningMessageResId = null, | |
| errorMessage = null | |
| ) | |
| } | |
| userRepository.checkNickname(_uiState.value.nickname) | |
| .onSuccess { response -> | |
| fun checkNickname() { | |
| if (_uiState.value.isLoading) return | |
| val nickname = _uiState.value.nickname.trim() | |
| if (nickname.isBlank()) return | |
| viewModelScope.launch { | |
| _uiState.update { | |
| it.copy( | |
| isLoading = true, | |
| warningMessageResId = null, | |
| errorMessage = null | |
| ) | |
| } | |
| userRepository.checkNickname(nickname) | |
| .onSuccess { response -> |
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/ui/signin/viewmodel/NicknameViewModel.kt
around lines 41 to 55, the ViewModel uses the raw nickname value for isBlank
checks and sending to the repository, which can differ from server-side
validation; trim the nickname once at the start (e.g., val trimmed =
_uiState.value.nickname.trim()), use trimmed for the isBlank check, and pass
trimmed to userRepository.checkNickname and any subsequent state updates so the
UI and server validations are consistent.
| import kotlin.onSuccess | ||
|
|
There was a problem hiding this comment.
컴파일 오류: 잘못된 import 제거 필요 (kotlin.onSuccess)
onSuccess는 Kotlin 표준 라이브러리의 Result 확장이며 별도 kotlin.onSuccess import가 존재하지 않습니다. 이 import는 컴파일 에러를 유발합니다. 제거하세요.
-import kotlin.onSuccess📝 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.
| import kotlin.onSuccess |
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/ui/signin/viewmodel/SignupAliasViewModel.kt
around lines 13-14, remove the invalid import "import kotlin.onSuccess" which
does not exist and causes a compile error; simply delete that import line and
rely on the standard Result.onSuccess extension without an explicit import.
| OthersSubsciptionContent( | ||
| uiState = uiState, | ||
| lazyListState = lazyListState, | ||
| onNavigateBack = { navController.popBackStack() } |
There was a problem hiding this comment.
이렇게 말고 이미 만들어둔 navigateBack() 함수 이용 바랍니당
| modifier = Modifier | ||
| .fillMaxWidth() | ||
| .padding(vertical = 16.dp), | ||
| verticalAlignment = Alignment.CenterVertically | ||
| ) { | ||
| Spacer(modifier = Modifier.weight(1f)) | ||
| CircularProgressIndicator(modifier = Modifier.size(24.dp)) | ||
| Spacer(modifier = Modifier.weight(1f)) | ||
| } |
There was a problem hiding this comment.
여기에서 fillMaxSize로 적용하고 Spacer 빼면 가운데 정렬 되지 않을까여 ?
| ) { | ||
| ProfileBar( | ||
| profileImage = "https://example.com/image1.jpg", | ||
| profileImage = feedItem.userProfileImage.toString(), |
There was a problem hiding this comment.
이거 feedItem에서 userProfileImage가 int로 설정돼있던데 string으로 안바꾸고 이렇게 한 이유가 있나요?
➕ 이슈 링크
🔎 작업 내용
📸 스크린샷
😢 해결하지 못한 과제
[] TASK
📢 리뷰어들에게
Summary by CodeRabbit
신규 기능
스타일