Conversation
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughGroup 도메인을 Rooms로 이전하고 RecentSearch를 분리해 서비스/레포지토리/모델/네비게이션/UI 흐름을 재배치함. 방 관련 API(생성·참여·비공개 비밀번호·마감 등)를 RoomsService/RoomsRepository로 연결하고 관련 뷰모델과 네비게이션을 갱신함. Changes
Sequence Diagram(s)sequenceDiagram
participant UI as GroupRoomUnlockScreen
participant VM as GroupRoomUnlockViewModel
participant Repo as RoomsRepository
participant Svc as RoomsService
UI->>VM: checkPassword(roomId, password)
VM->>Repo: postParticipateSecretRoom(roomId, password)
Repo->>Svc: POST /rooms/{roomId}/password
Svc-->>Repo: BaseResponse<RoomSecretRoomResponse>
Repo-->>VM: Result<RoomSecretRoomResponse>
VM-->>UI: uiState.passwordMatched = true/false
UI->>UI: 성공 시 onSuccessNavigation()
sequenceDiagram
participant UI as GroupRoomRecruitScreen
participant VM as GroupRoomRecruitViewModel
participant Repo as RoomsRepository
participant Svc as RoomsService
UI->>VM: onCloseRecruitmentClick(roomId)
VM->>Repo: closeRoom(roomId)
Repo->>Svc: POST /rooms/{roomId}/close
Svc-->>Repo: BaseResponse<RoomCloseResponse>
Repo-->>VM: Result<RoomCloseResponse>
VM-->>UI: shouldNavigateToRoomPlayingScreen = true, roomId set
sequenceDiagram
participant UI as GroupMakeRoomScreen
participant VM as GroupMakeRoomViewModel
participant Repo as RoomsRepository
participant Svc as RoomsService
UI->>VM: createGroup()
VM->>Repo: createRoom(CreateRoomRequest)
Repo->>Svc: POST /rooms
Svc-->>Repo: BaseResponse<CreateRoomResponse>
Repo-->>VM: Result<Int>
VM-->>UI: onGroupCreated(roomId)
Estimated code review effort🎯 5 (Critical) | ⏱️ ~75 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Possibly related issues
Possibly related PRs
Suggested reviewers
Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate 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.
Pull Request Overview
This PR implements the remaining rooms API connections including room creation, password-protected room participation, recruitment closure, and recent search functionality with server integration.
- Adds comprehensive room management features including creation flow navigation, password unlock screens, and recruitment closure with navigation to activity screens
- Integrates server-side recent search functionality for both books and rooms, replacing local storage with API calls
- Refactors service and repository architecture by moving group-related APIs to rooms namespace and creating dedicated search repository
Reviewed Changes
Copilot reviewed 53 out of 53 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| app/src/main/res/values/strings.xml | Updates error and success messages with more descriptive Korean text |
| app/src/main/java/com/texthip/thip/ui/search/viewmodel/SearchBookViewModel.kt | Switches to use RecentSearchRepository for server-side search history |
| app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt | Implements room closure API with proper error handling and navigation |
| app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomUnlockScreen.kt | Creates password input screen with real-time validation |
| app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt | Adds complex navigation flow between room creation, recruitment, and password screens |
| app/src/main/java/com/texthip/thip/data/service/RoomsService.kt | Consolidates room-related API endpoints including password verification and closure |
| app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt | Implements comprehensive room operations with proper error handling |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
You can also share your feedback on Copilot code review for a chance to win a $100 gift card. Take the survey.
app/src/main/res/values/strings.xml
Outdated
| <string name="error_room_close_failed">모집 마감 중 오류가 발생했습니다.</string> | ||
| <string name="error_room_info_not_found">방 정보를 찾을 수 없습니다.</string> | ||
|
|
||
| <!-- SearchBookGroupViewModel -->\n <string name="error_recruiting_rooms_not_found">모집중인 방 정보를 찾을 수 없습니다.</string> |
There was a problem hiding this comment.
There is an invalid escape sequence '\n' in the comment line. This should be removed or properly formatted.
| <!-- SearchBookGroupViewModel -->\n <string name="error_recruiting_rooms_not_found">모집중인 방 정보를 찾을 수 없습니다.</string> | |
| <!-- SearchBookGroupViewModel --> | |
| <string name="error_recruiting_rooms_not_found">모집중인 방 정보를 찾을 수 없습니다.</string> |
| currentButtonType = buttonType, | ||
| isLoading = false | ||
| ) | ||
| if (data != null) { |
There was a problem hiding this comment.
The null check pattern suggests the API might return null unexpectedly. Consider making the repository method return a non-null result or handle the null case more explicitly with proper error messaging.
| text = detail.category, | ||
| style = typography.info_m500_s12, | ||
| color = colors.SocialScience | ||
| color = hexToColor(detail.categoryColor) |
There was a problem hiding this comment.
The hexToColor function should handle invalid color strings gracefully. Consider adding error handling for malformed hex color strings to prevent crashes.
| color = hexToColor(detail.categoryColor) | |
| color = safeHexToColor(detail.categoryColor) |
| private fun BookSavedResponse.toBookData(): BookData { | ||
| return BookData( | ||
| title = this.bookTitle, | ||
| imageUrl = this.bookImageUrl, |
There was a problem hiding this comment.
The property name 'bookImageUrl' doesn't match the property name 'imageUrl' used in the BookSearchItem.toBookData() method below (line 121). This inconsistency could cause runtime errors.
| imageUrl = this.bookImageUrl, | |
| imageUrl = this.imageUrl, |
There was a problem hiding this comment.
Actionable comments posted: 17
🔭 Outside diff range comments (8)
app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupMainCard.kt (1)
145-156: 진행률 범위 미검증으로 인한 크래시 위험fillMaxWidth(fraction)은 0f..1f 범위만 유효합니다. userPercentage가 0~100을 벗어나면 IllegalArgumentException이 발생할 수 있으므로 범위 보정이 필요합니다.
아래처럼 0~100으로 clamp한 fraction을 사용해 주세요:
- val percentage = data.userPercentage.toFloat() + val progressFraction = (data.userPercentage.coerceIn(0, 100)) / 100f Box( modifier = Modifier .fillMaxWidth() .height(8.dp) .background(color = colors.Grey02, shape = RoundedCornerShape(12.dp)) ) { Box( modifier = Modifier - .fillMaxWidth(fraction = percentage / 100f) + .fillMaxWidth(fraction = progressFraction) .fillMaxHeight() .background(color = colors.Purple, shape = RoundedCornerShape(12.dp)) ) }app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt (1)
49-58: 페이지네이션 진행 중(UI) 표시 누락: isLoadingMore 플래그를 true로 세팅해 주세요.
reset=false(추가 로드) 시 내부 플래그(isLoadingMore)는 true로 바꾸지만, UI 상태의 isLoadingMore는 true로 세팅되지 않습니다. GroupMyViewModel과 불일치하며, 로딩 인디케이터가 보이지 않을 수 있습니다.아래 패치를 적용해 추가 로드 시작 시 UI 상태도 반영해 주세요.
} else { if (isLoadingMore || isLastPage) return isLoadingMore = true + updateState { it.copy(isLoadingMore = true) } }Also applies to: 71-76, 90-97
app/src/main/java/com/texthip/thip/ui/group/search/screen/GroupSearchScreen.kt (2)
69-76: liveFilteredRoomList가 roomList 변경에 반응하지 않는 버그remember 키에 roomList가 포함되지 않아, roomList가 갱신돼도 searchText가 바뀌지 않으면 파생 목록이 갱신되지 않습니다.
아래처럼 roomList를 키에 추가하세요.
- val liveFilteredRoomList by remember(searchText) { + val liveFilteredRoomList by remember(searchText, roomList) { derivedStateOf { if (searchText.isBlank()) emptyList() else roomList.filter { room -> room.title.contains(searchText, ignoreCase = true) } } }
78-97: filteredRoomList도 roomList 변경에 반응하도록 키 확장 필요여기도 roomList를 키에 포함하지 않아 최신 데이터 반영이 지연될 수 있습니다.
- val filteredRoomList by remember( - searchText, - selectedGenreIndex, - selectedSortOptionIndex, - isSearched - ) { + val filteredRoomList by remember( + searchText, + selectedGenreIndex, + selectedSortOptionIndex, + isSearched, + roomList + ) { derivedStateOf { if (!isSearched) emptyList() else { val filtered = roomList.filter { room -> (searchText.isBlank() || room.title.contains(searchText, ignoreCase = true)) } when (selectedSortOptionIndex) { 0 -> filtered.sortedBy { it.endDate } // 마감임박순 1 -> filtered.sortedByDescending { it.participants } // 인기순 else -> filtered } } } }app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt (1)
36-61: 모집 상세 불러오기: null 응답 시 이전 데이터 정리 및 에러 피드백 필요null 응답일 때
roomDetail과currentButtonType을 정리하지 않으면 이전 방의 상태가 남아 잘못된 UI가 보일 수 있습니다. 실패 시에는 사용자에게 에러 토스트도 안내하는 편이 낫습니다..onSuccess { data -> - if (data != null) { + if (data != null) { val buttonType = when { data.isHost -> GroupBottomButtonType.CLOSE data.isJoining -> GroupBottomButtonType.CANCEL else -> GroupBottomButtonType.JOIN } updateState { it.copy( roomDetail = data, currentButtonType = buttonType, isLoading = false ) } } else { - updateState { - it.copy( - isLoading = false - ) - } + updateState { + it.copy( + roomDetail = null, + currentButtonType = GroupBottomButtonType.JOIN, + isLoading = false + ) + } } } .onFailure { error -> - updateState { it.copy(isLoading = false) } + updateState { it.copy(isLoading = false) } + showToastMessage( + stringResourceProvider.getString(R.string.error_room_info_not_found) + ) }app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt (1)
286-288: roomId 하드코딩(1)으로 인해 잘못된 방으로 내비게이션됩니다여러 화면에서 전달받은
roomId를 사용하지 않고 1로 고정되어 있어 실제 방으로 이동하지 못합니다. 반드시 전달된roomId를 사용하도록 수정하세요.@@ GroupRoomMatesScreen( -// roomId = roomId, - roomId = 1, + roomId = roomId, onBackClick = { navigateBack() }, @@ GroupNoteScreen( -// roomId = roomId, - roomId = 1, + roomId = roomId, resultTabIndex = result, initialPage = page, initialIsOverview = isOverview, @@ GroupNoteCreateScreen( - roomId = 1, + roomId = roomId, recentPage = route.recentBookPage, totalPage = route.totalBookPage, isOverviewPossible = route.isOverviewPossible, @@ GroupVoteCreateScreen( -// roomId = roomId, - roomId = 1, + roomId = roomId, recentPage = route.recentPage, totalPage = route.totalPage, isOverviewPossible = route.isOverviewPossible,Also applies to: 311-313, 345-347, 366-369
app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt (2)
351-362: 카테고리 컬러 변환을 remember로 메모이즈하고, 유효하지 않은 HEX를 관대하게 처리
- 매 recomposition마다 hexToColor가 호출됩니다. categoryColor 변경시에만 재계산되도록 remember를 사용하면 불필요한 파싱 비용을 줄일 수 있습니다.
- 서버가 '#' 없는 HEX("8B7CF6")를 줄 가능성을 고려해 hexToColor에서 자동 보정하도록 개선하면 안전합니다.
메모이즈 적용:
- Text( - text = detail.category, - style = typography.info_m500_s12, - color = hexToColor(detail.categoryColor) - ) + val genreColor = remember(detail.categoryColor) { + hexToColor(detail.categoryColor) + } + Text( + text = detail.category, + style = typography.info_m500_s12, + color = genreColor + )추가 import:
import androidx.compose.runtime.rememberhexToColor 개선(별도 파일: utils/color/HexToColor.kt):
fun hexToColor(hex: String): Color { val normalized = hex.trim().let { if (it.isEmpty()) return Color.White if (it.startsWith("#")) it else "#$it" } return try { Color(android.graphics.Color.parseColor(normalized)) } catch (e: IllegalArgumentException) { Color.White } }
149-156: 배경 이미지: AsyncImage 접근성·레이아웃 개선 제안 (검증 완료)검증 요약: gradle/libs.versions.toml 및 app/build.gradle.kts에 coil.compose 의존성이 존재하고, AsyncImage 사용처가 여러 파일에서 확인됩니다. 코드베이스에는 ImageRequest 사용 예시는 없습니다. 제안한 변경은 적용 가능하지만, ImageRequest.Builder에 필요한 context는 LocalContext.current로 받아와야 합니다.
수정 권장 위치:
- app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt (약 149–156행)
- 참고(검토 권장) : AsyncImage를 사용하는 다른 파일들 — SearchBookDetailScreen.kt, FeedWriteScreen.kt, CardBookSearch.kt, ProfileBar.kt, GroupRoomScreen.kt 등 (장식적 이미지면 동일 처리 권장)
제안된 diff:
- AsyncImage( - model = detail.roomImageUrl, - contentDescription = "모임방 배경 이미지", - modifier = Modifier - .align(Alignment.TopCenter) - .fillMaxWidth(), - contentScale = ContentScale.Crop - ) + val context = LocalContext.current + AsyncImage( + model = ImageRequest.Builder(context) + .data(detail.roomImageUrl) + .crossfade(true) + .build(), + contentDescription = null, // 배경 이미지는 접근성 노출 불필요 + modifier = Modifier + .align(Alignment.TopCenter) + .fillMaxSize(), + contentScale = ContentScale.Crop + )추가 import:
import coil.request.ImageRequest import androidx.compose.ui.platform.LocalContext요약: 의존성은 확인되었고 제안대로 수정 가능하니, 위 파일을 수정하고 다른 AsyncImage 사용처도 장식적 이미지인지 검토 후 동일 패턴 적용을 권장합니다.
♻️ Duplicate comments (1)
app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyUiState.kt (1)
6-16: 레거시 import 점검은 앞선 코멘트와 동일동일한 성격의 마이그레이션이므로, GroupDoneUiState에 첨부한 스크립트로 레거시 group.response 참조 잔존 여부를 함께 확인해 주세요.
🧹 Nitpick comments (33)
app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupEmptyBookSheetContent.kt (1)
52-52: 람다 대신 함수 레퍼런스로 간결화 가능불필요한 람다 래핑을 제거하면 가독성이 좋아집니다.
- onClick = { onRequestBook() }, + onClick = onRequestBook,app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupUiState.kt (1)
6-20: 네이밍 도메인 일관성 제안(옵션)Rooms로 마이그레이션 되는 흐름과 맞춰,
GroupUiState내 일부 프로퍼티 네이밍(예:hasMoreMyGroups)도 Rooms 용어로 정리되면 추후 유지보수성이 좋아집니다. PR 범위를 넘는다면 후속 리팩터로 충분합니다.data class GroupUiState( val myJoinedRooms: List<JoinedRoomResponse> = emptyList(), - val hasMoreMyGroups: Boolean = true, + val hasMoreMyRooms: Boolean = true, val isRefreshing: Boolean = false, val isLoadingMoreMyGroups: Boolean = false, val roomMainList: RoomMainList? = null, val roomSectionsError: String? = null, val userName: String = "", val selectedGenreIndex: Int = 0, val showToast: Boolean = false, val toastMessage: String = "" ) { val hasContent: Boolean get() = myJoinedRooms.isNotEmpty() || (roomMainList != null) - val canLoadMore: Boolean get() = hasMoreMyGroups && !isRefreshing && !isLoadingMoreMyGroups + val canLoadMore: Boolean get() = hasMoreMyRooms && !isRefreshing && !isLoadingMoreMyGroups }app/src/main/java/com/texthip/thip/data/model/rooms/response/MyRoomListResponse.kt (1)
1-1: 타입 안정성 및 날짜 표현 개선 제안 (type, endDate)
- type: String → enum(@serializable)로 변환하면 오타/허용값 관리가 쉬워집니다.
- endDate: String → kotlinx-datetime(LocalDate/Instant)로 받으면 파싱/포맷 일원화 및 타임존 혼동을 줄일 수 있습니다.
예시:
@Serializable enum class RoomType { @SerialName("PUBLIC") PUBLIC, @SerialName("PRIVATE") PRIVATE, @SerialName("SECRET") SECRET // 서버 스키마에 맞춰 값 정의 } @Serializable data class MyRoomResponse( @SerialName("roomId") val roomId: Int, @SerialName("bookImageUrl") val bookImageUrl: String, @SerialName("roomName") val roomName: String, @SerialName("recruitCount") val recruitCount: Int, @SerialName("memberCount") val memberCount: Int, // kotlinx-datetime 사용 시 // @SerialName("endDate") val endDate: LocalDate @SerialName("type") val type: RoomType )참고: kotlinx-datetime 도입 시 의존성 추가 필요.
app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomJoinResponse.kt (1)
1-1: Join 타입을 enum으로 타이핑하여 안정성 확보 제안서버가 반환하는 type 필드가 제한된 값 집합이라면 enum(@serializable)로 모델링해 요청/응답 모두에서 동일 enum을 쓰면 안전합니다.
예시:
@Serializable enum class JoinType { @SerialName("JOIN") JOIN, @SerialName("CANCEL") CANCEL // 서버 스펙에 맞춰 보강 } @Serializable data class RoomJoinResponse( @SerialName("roomId") val roomId: Int, @SerialName("type") val type: JoinType )app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomJoinRequest.kt (1)
1-1: 요청의 type도 enum으로 통일 사용 권장 (응답과 동일 enum 공유)요청/응답 모두 동일 enum(예: JoinType)으로 강제하면 오타 및 허용값 누락을 방지할 수 있습니다.
예시:
@Serializable data class RoomJoinRequest( @SerialName("type") val type: JoinType )추가로, 허용값 목록과 의미(JOIN/CANCEL 등)가 명확히 정의되어 있는지 서버 스펙 확인이 필요합니다.
app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomUiState.kt (2)
25-26: 로딩 상태 네이밍/책임 범위 정리 제안isLoadingBooks vs isSearching가 혼동될 수 있습니다. 검색 로딩은 isSearching, 저장/그룹 북 로딩은 isLoadingBooks처럼 도메인을 이름에 드러내거나, 각각 isLoadingSearchBooks / isLoadingSavedBooks로 구분하면 가독성이 올라갑니다.
25-26: (별도) 비공개방 비밀번호 검증 강화 고려현재 isPasswordValid가 길이 4만 확인합니다. 숫자 4자리 제약이 요구사항이라면 숫자만 허용하도록 강화가 필요합니다.
예시:
val isPasswordValid: Boolean get() = !isPrivate || (password.length == 4 && password.all { it.isDigit() })app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomCloseResponse.kt (1)
1-9: KDoc 및 도메인 용어 정합성(모집 마감 vs 방 종료) 명시 제안해당 응답이 “모집 마감(close recruiting)”을 의미함을 KDoc에 명시하면 혼동을 줄일 수 있습니다. (예: 방 삭제/종료와의 구분)
예시:
/** * 방의 '모집 마감' 결과를 나타내는 응답 모델. * @property roomId 마감 처리된 방 ID */ @Serializable data class RoomCloseResponse( @SerialName("roomId") val roomId: Int )app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupDeadlineRoomSection.kt (1)
185-204: 긴 목록 가능성을 고려해 LazyColumn 사용을 권장현재 Column + forEach로 렌더링하고 있는데, 방 개수가 많아질 경우 초기 컴포지션/리컴포지션 비용이 불필요하게 커질 수 있습니다. LazyColumn으로 교체하면 성능과 스크롤 처리가 개선됩니다.
아래 diff는 '정상 데이터 표시' 블록의 목록 부분만 LazyColumn으로 치환한 예시입니다:
- Column( - verticalArrangement = Arrangement.spacedBy(20.dp), - modifier = Modifier.fillMaxWidth() - ) { - rooms.forEach { room -> - // RoomMainResponse를 CardItemRoom에 맞게 변환 - val daysLeft = DateUtils.extractDaysFromDeadline(room.deadlineDate) - CardItemRoom( - title = room.roomName, - participants = room.memberCount, - maxParticipants = room.recruitCount, - isRecruiting = true, // RoomMainResponse에는 모집중인 방만 있음 - endDate = daysLeft, - imageUrl = room.bookImageUrl, - onClick = { onRoomClick(room) }, - hasBorder = true, - ) - } - } + LazyColumn( + verticalArrangement = Arrangement.spacedBy(20.dp), + modifier = Modifier.fillMaxWidth() + ) { + items(rooms, key = { it.roomId }) { room -> + val daysLeft = DateUtils.extractDaysFromDeadline(room.deadlineDate) + CardItemRoom( + title = room.roomName, + participants = room.memberCount, + maxParticipants = room.recruitCount, + isRecruiting = true, + endDate = daysLeft, + imageUrl = room.bookImageUrl, + onClick = { onRoomClick(room) }, + hasBorder = true, + ) + } + }LazyColumn/Items 추가 import:
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsapp/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupMainCard.kt (1)
78-84: 이미지 로딩 견고성 개선(placeholder/error/fallback 활용 제안)현재는 model에 바로 리소스 ID를 대입하고 있습니다. Coil의 placeholder/error/fallback을 활용하면 null/에러 상황에서 일관된 처리와 캐시 활용이 가능합니다.
아래처럼 교체를 고려해 주세요:
- AsyncImage( - model = data.bookImageUrl ?: R.drawable.img_book_cover_sample, - contentDescription = "책 이미지", - modifier = Modifier - .size(width = 80.dp, height = 107.dp), - contentScale = ContentScale.Crop - ) + AsyncImage( + model = data.bookImageUrl, + placeholder = painterResource(R.drawable.img_book_cover_sample), + error = painterResource(R.drawable.img_book_cover_sample), + fallback = painterResource(R.drawable.img_book_cover_sample), + contentDescription = "책 이미지", + modifier = Modifier.size(width = 80.dp, height = 107.dp), + contentScale = ContentScale.Crop + )app/src/main/java/com/texthip/thip/ui/navigator/extensions/GroupNavigationExtensions.kt (1)
59-63: 비밀번호 입력 화면 네비게이션 추가 적절타입 세이프 라우트
RoomUnlock로의 이동이 명확히 분리되어 플로우 가독성이 좋아졌습니다. 연속 탭으로 동일 화면이 중복 쌓이는 것을 방지하려면launchSingleTop옵션을 고려할 수 있습니다(선택).// 비밀번호 입력 화면으로 이동 fun NavHostController.navigateToGroupRoomUnlock(roomId: Int) { - navigate(GroupRoutes.RoomUnlock(roomId)) + navigate(GroupRoutes.RoomUnlock(roomId)) { + launchSingleTop = true + } }app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt (1)
19-26: 중복 페이지네이션 로직(플래그/커서/에러 처리) 공통화 제안
GroupDoneViewModel과 GroupMyViewModel이 매우 유사한 로딩/커서/마지막 페이지/에러 처리 로직을 중복 보유합니다. 공통 Paginator 유틸(예: suspend fetch(nextCursor) -> response, 내부에서 nextCursor/isLast 관리)을 도입하면 유지보수 비용과 버그 확률을 줄일 수 있습니다.원하시면 공용 유틸 시그니처와 간단한 구현 틀을 제안드리겠습니다.
Also applies to: 49-58, 67-86
app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt (1)
38-41: roomType 전환 직후 중복 호출/레이스 방지를 위해 이전 로딩 Job 취소를 권장합니다.
roomType 변경 시 reset=true로 즉시 재호출되므로, 이전 로딩이 진행 중이면 중첩되어 상태가 요동칠 수 있습니다. Job을 보관/취소하는 방식으로 안정성을 높이세요.아래 변경을 제안합니다.
- 변경 1: 로딩 Job 보관
// 파일 상단의 다른 상태값들과 함께 선언 private var loadJob: Job? = null
- 변경 2: loadMyRooms 내 launch를 Job에 할당 (라인 38 인근)
- viewModelScope.launch { + loadJob = viewModelScope.launch { try { isLoadingData = true ... } finally { isLoadingData = false updateState { it.copy(isLoading = false, isLoadingMore = false) } } }
- 변경 3: roomType 변경 시 이전 Job 취소 (라인 87-92)
fun changeRoomType(roomType: RoomType) { if (roomType != uiState.value.currentRoomType) { + loadJob?.cancel() updateState { it.copy(currentRoomType = roomType) } loadMyRooms(reset = true) } }Also applies to: 73-75, 87-92
app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt (1)
25-26: 명명 규칙 정렬 제안: provideRoomsService로 변경
동일 모듈 내 다른 함수들이 provideX 네이밍을 따르는 반면 이 함수만 providesRoomsService로 되어 있습니다. 일관성을 위해 rename 권장합니다. (주입 지점에서 함수명을 직접 참조하지 않으므로 변경 영향은 없습니다.)아래처럼 변경 가능합니다.
- fun providesRoomsService(retrofit: Retrofit): RoomsService = + fun provideRoomsService(retrofit: Retrofit): RoomsService = retrofit.create(RoomsService::class.java)app/src/main/java/com/texthip/thip/data/service/RecentSearchService.kt (1)
12-17: type을 String으로 직접 받기보다는 타입세이프 열거형 도입 권장
허용 가능한 타입이 제한적(예: BOOK, ROOM 등)이라면 enum/sealed class로 캡슐화하면 오타와 불변식을 방지할 수 있습니다. 저장소 계층에서 매핑해도 좋지만, 서비스 레벨에서부터 타입세이프를 적용하는 편이 안전합니다.예시:
enum class RecentSearchType(val query: String) { BOOK("BOOK"), ROOM("ROOM"), } @GET("recent-searches") suspend fun getRecentSearches( @Query("type") type: String ): BaseResponse<RecentSearchResponse> // 호출부 service.getRecentSearches(RecentSearchType.BOOK.query)Also applies to: 18-22
app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitUiState.kt (2)
15-17: 네비게이션 상태 표현을 단일 이벤트로 타입-세이프하게 개선 제안불리언 플래그 2개(shouldNavigateToGroupScreen, shouldNavigateToRoomPlayingScreen)와 roomId(null 가능) 조합은 상호 배타성 보장과 상태 리셋 처리에서 실수가 발생하기 쉽습니다. 단일 이벤트(Sealed class)로 전환하면 중복/충돌을 방지하고 타입 안정성이 좋아집니다.
아래처럼 변경을 고려해 주세요.
- val shouldNavigateToGroupScreen: Boolean = false, - val shouldNavigateToRoomPlayingScreen: Boolean = false, - val roomId: Int? = null + val navigationEvent: NavigationEvent? = null추가 코드(파일 하단 등, 혹은 별도 파일 권장):
sealed class NavigationEvent { data object GroupScreen : NavigationEvent() data class RoomPlayingScreen(val roomId: Int) : NavigationEvent() }추가로, 현 구조를 유지한다면
- 네비게이션 완료 시 두 불리언과 roomId를 모두 원자적으로 초기화(copy 호출 1회)하는 onNavigated 처리
- roomId가 null일 수 있는 상태에서 shouldNavigateToRoomPlayingScreen이 true가 되는 일이 없도록 불변식 보장
을 반드시 확인해 주세요.
3-3: Mock 타입 의존 여부 확인currentButtonType이 mock 패키지(GroupBottomButtonType from mock)에 의존하고 있습니다. 프로덕션 코드에서 mock 타입을 참조하지 않도록 실제 도메인/디자인 시스템의 enum으로 치환하는지 확인 바랍니다.
app/src/main/java/com/texthip/thip/data/repository/RecentSearchRepository.kt (1)
15-19: 최근 검색어 타입을 String 대신 타입 안정적인 enum/클래스로 변경 제안"BOOK", "ROOM" 등 하드코딩된 문자열은 오탈자/케이스 오류로 쉽게 깨질 수 있습니다. API와 합의된 값으로 캡슐화된 enum을 사용하면 안전합니다.
예시:
enum class RecentSearchType(val apiValue: String) { BOOK("BOOK"), ROOM("ROOM") }- suspend fun getRecentSearches(type: String): Result<RecentSearchResponse?> = runCatching { - recentSearchService.getRecentSearches(type) + suspend fun getRecentSearches(type: RecentSearchType): Result<RecentSearchResponse?> = runCatching { + recentSearchService.getRecentSearches(type.apiValue)app/src/main/java/com/texthip/thip/data/repository/BookRepository.kt (1)
21-21: type 파라미터 문자열 상수의 타입 안전성 강화 제안RecentSearch와 동일하게,
type: String은 enum(예: BookListType.SAVED/ROOM)으로 래핑해 오타/케이스 오류를 방지하는 것을 권장합니다.app/src/main/java/com/texthip/thip/ui/group/search/screen/GroupSearchScreen.kt (2)
49-49: collectAsStateWithLifecycle 사용 권장수명 주기 인지 수집으로 화면 이동 시 불필요한 수집을 줄이고 안전성을 높일 수 있습니다.
-import androidx.compose.runtime.collectAsState +import androidx.lifecycle.compose.collectAsStateWithLifecycle ... - val uiState by viewModel.uiState.collectAsState() + val uiState by viewModel.uiState.collectAsStateWithLifecycle()
208-247: Preview가 Hilt ViewModel 기본값 때문에 실패할 가능성
viewModel: GroupSearchViewModel = hiltViewModel()기본값 때문에 Preview 환경에서 크래시가 발생합니다. 프리뷰 친화적으로 아래 중 하나를 권장합니다.
- UI 상태/콜백을 파라미터로 받는 순수 UI Composable로 분리하고, 현재 함수는 ViewModel 래퍼로 유지.
- LocalInspectionMode 기반으로 프리뷰 시 더미 상태를 주입하고 hiltViewModel 호출을 우회.
- 혹은 프리뷰에서 사용할 FakeViewModel 구현체를 생성하여 전달.
원하시면 최소 변경으로 적용 가능한 패치를 제안드리겠습니다.
app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomUnlockViewModel.kt (1)
25-27: 공개 타입 명시로 API 안정성 향상외부 노출 프로퍼티 타입을 명시하면 추론 변경으로 인한 외부 영향도를 줄일 수 있습니다.
-import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.StateFlow ... - val uiState = _uiState.asStateFlow() + val uiState: StateFlow<GroupRoomUnlockUiState> = _uiState.asStateFlow()app/src/main/java/com/texthip/thip/data/service/RoomsService.kt (1)
44-47: 하드코딩된 기본값 대신 상수 사용 고려
category파라미터의 기본값이 "문학"으로 하드코딩되어 있습니다. 유지보수성을 위해 상수로 추출하는 것이 좋겠습니다.+ companion object { + private const val DEFAULT_CATEGORY = "문학" + } + /** 카테고리별 모임방 목록 조회 (마감임박/인기) */ @GET("rooms") suspend fun getRooms( - @Query("category") category: String = "문학" + @Query("category") category: String = DEFAULT_CATEGORY ): BaseResponse<RoomMainList>app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt (2)
136-137: 디바운싱 지연 시간을 상수로 추출300ms 디바운싱 지연 시간이 하드코딩되어 있습니다. 상수로 추출하면 가독성과 유지보수성이 향상됩니다.
companion object { private val DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy.MM.dd") + private const val SEARCH_DEBOUNCE_DELAY_MS = 300L }searchJob = viewModelScope.launch { - delay(300) // 디바운싱 + delay(SEARCH_DEBOUNCE_DELAY_MS) // 디바운싱 updateState { it.copy(isSearching = true) }
147-150: 예외 처리 시 로깅 추가 권고예외가 발생했을 때 단순히 빈 리스트로 상태를 업데이트하고 있습니다. 디버깅을 위해 로그를 남기는 것이 좋겠습니다.
} catch (e: Exception) { + // 디버깅을 위한 로그 추가 + e.printStackTrace() updateState { it.copy(searchResults = emptyList(), isSearching = false) } }app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomUnlockScreen.kt (2)
69-70: 매직 넘버를 상수로 추출에러 메시지 표시 시간(1000ms)이 하드코딩되어 있습니다. 상수로 추출하면 가독성이 향상됩니다.
컴포저블 함수 외부에 상수 추가:
private const val ERROR_MESSAGE_DISPLAY_DURATION_MS = 1000L그리고 사용 부분 수정:
showError = true - delay(1000) // 사용자에게 에러 메시지를 보여줄 시간 + delay(ERROR_MESSAGE_DISPLAY_DURATION_MS) // 사용자에게 에러 메시지를 보여줄 시간 password = arrayOf("", "", "", "")
80-85: 키보드 표시 지연 시간 상수화화면 전환 애니메이션 후 키보드 표시를 위한 지연 시간(300ms)도 상수로 추출하는 것이 좋겠습니다.
+private const val KEYBOARD_SHOW_DELAY_MS = 300L LaunchedEffect(Unit) { viewModel.initializeState() // 상태 완전 초기화 - delay(300) // 화면 전환 애니메이션 후 키보드 표시 + delay(KEYBOARD_SHOW_DELAY_MS) // 화면 전환 애니메이션 후 키보드 표시 focusRequesters[0].requestFocus() keyboardController?.show() }app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookSearchBottomSheet.kt (1)
70-79: 입력 변경마다 onSearch 호출 — 디바운스 전제 확인 필요텍스트 변경 시마다
onSearch를 호출하고 있는데, VM에서 디바운스가 걸려있다는 전제(요약에 따르면 있음)가 반드시 충족되어야 서버 트래픽 폭증을 막을 수 있습니다. 디바운스가 없다면 최소 길이 조건(예: 2글자 이상)이나 IME/아이콘 액션에서만 검색하도록 조정이 필요합니다.예: 길이 0 또는 2 이상일 때만 호출
- onValueChange = { - searchText = it - onSearch(it) - }, + onValueChange = { + searchText = it + if (it.isEmpty() || it.length >= 2) { + onSearch(it) + } + },app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt (1)
246-263: 바텀시트 닫힘 시 검색 상태 초기화 고려바텀시트를 닫을 때 이전 검색어/결과가 남아있으면 다음 열기 시 혼란을 줄 수 있습니다. VM에서 디바운스 검색을 사용한다면, 닫힘 시 검색어를 초기화하거나 결과를 비우는 핸들러를 연결하는 것을 권장합니다.
예:
- onDismiss = { onToggleBookSearchSheet(false) }, + onDismiss = { + onToggleBookSearchSheet(false) + // 필요 시 검색 상태 초기화 + // onSearchBooks("") // 또는 viewModel에 clearSearch() 제공 + },app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt (1)
88-95: 액션 타입을 String 대신 타입 세이프한 Enum/클래스로 받는 것을 고려하세요
joinOrCancelRoom(roomId: Int, type: String)은 오타/스펙 변경에 취약합니다.RoomAction같은 enum을 인자로 받고 내부에서value를 사용하면 안정성이 높아집니다.예:
suspend fun joinOrCancelRoom(roomId: Int, action: RoomAction): Result<String> = runCatching { val request = RoomJoinRequest(type = action.value) // ... }app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt (1)
197-205: SavedStateHandle Flow 수집에 collectAsStateWithLifecycle 사용 권장
collectAsState()대신collectAsStateWithLifecycle()을 사용하면 수명 주기를 고려한 안전한 수집이 가능합니다. 파일 내 다른 곳에서도 이미 사용 중이므로 일관성 측면에서도 이득입니다.- val participationApproved by backStackEntry.savedStateHandle - .getStateFlow(PARTICIPATION_APPROVED_KEY, false) - .collectAsState() + val participationApproved by backStackEntry.savedStateHandle + .getStateFlow(PARTICIPATION_APPROVED_KEY, false) + .collectAsStateWithLifecycle()app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt (2)
65-67: 네비게이션 콜백 파라미터가 많습니다 — Action 객체로 묶어 간결화 권장Composable 시그니처가 계속 비대해질 수 있습니다. 화면 액션을 묶은 data class/인터페이스(예: GroupRoomRecruitActions)를 도입해 파라미터 폭발을 방지하고 유지보수성을 높이는 것을 고려해 주세요.
85-95: LaunchedEffect 키 구성으로 인한 재트리거 리스크를 줄이세요현재 두 키(shouldNavigateToRoomPlayingScreen, roomId)에 의존해 effect가 더 자주 재시작할 수 있습니다. 플래그만 키로 두고 내부에서 roomId null-safe 처리로 내비게이션을 수행하면 불필요한 재시작을 줄일 수 있습니다.
적용 제안:
- LaunchedEffect(uiState.shouldNavigateToRoomPlayingScreen, uiState.roomId) { - if (uiState.shouldNavigateToRoomPlayingScreen) { - val roomIdValue = uiState.roomId - if (roomIdValue != null) { - onNavigateToRoomPlayingScreen(roomIdValue) - viewModel.onNavigatedToRoomPlayingScreen() - } - } - } + LaunchedEffect(uiState.shouldNavigateToRoomPlayingScreen) { + if (uiState.shouldNavigateToRoomPlayingScreen) { + uiState.roomId?.let { id -> + onNavigateToRoomPlayingScreen(id) + viewModel.onNavigatedToRoomPlayingScreen() + } + } + }내비게이션 이벤트가 중복 발생하지 않는지 실제 디바이스/에뮬레이터에서 버튼 연타 및 configuration change(회전) 상황을 포함해 한 번 더 점검해 주세요.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (53)
app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt(2 hunks)app/src/main/java/com/texthip/thip/data/model/book/response/BookListResponse.kt(1 hunks)app/src/main/java/com/texthip/thip/data/model/rooms/request/CreateRoomRequest.kt(1 hunks)app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomJoinRequest.kt(1 hunks)app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomSecreteRoomRequest.kt(1 hunks)app/src/main/java/com/texthip/thip/data/model/rooms/response/CreateRoomResponse.kt(1 hunks)app/src/main/java/com/texthip/thip/data/model/rooms/response/JoinedRoomListResponse.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/model/rooms/response/RoomCloseResponse.kt(1 hunks)app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomJoinResponse.kt(1 hunks)app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomMainResponse.kt(1 hunks)app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomRecruitingResponse.kt(2 hunks)app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomSecreteRoomResponse.kt(1 hunks)app/src/main/java/com/texthip/thip/data/provider/StringResourceProvider.kt(1 hunks)app/src/main/java/com/texthip/thip/data/repository/BookRepository.kt(2 hunks)app/src/main/java/com/texthip/thip/data/repository/GroupRepository.kt(0 hunks)app/src/main/java/com/texthip/thip/data/repository/RecentSearchRepository.kt(1 hunks)app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt(1 hunks)app/src/main/java/com/texthip/thip/data/service/BookService.kt(1 hunks)app/src/main/java/com/texthip/thip/data/service/GroupService.kt(0 hunks)app/src/main/java/com/texthip/thip/data/service/RecentSearchService.kt(1 hunks)app/src/main/java/com/texthip/thip/data/service/RoomsService.kt(2 hunks)app/src/main/java/com/texthip/thip/ui/group/done/screen/GroupDoneScreen.kt(1 hunks)app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneUiState.kt(1 hunks)app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt(2 hunks)app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookSearchBottomSheet.kt(3 hunks)app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupEmptyBookSheetContent.kt(1 hunks)app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt(6 hunks)app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomUiState.kt(1 hunks)app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt(7 hunks)app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupDeadlineRoomSection.kt(1 hunks)app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupMainCard.kt(1 hunks)app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupPager.kt(1 hunks)app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt(1 hunks)app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyUiState.kt(1 hunks)app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt(2 hunks)app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt(13 hunks)app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomUnlockScreen.kt(4 hunks)app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitUiState.kt(2 hunks)app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt(7 hunks)app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomUnlockViewModel.kt(1 hunks)app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt(4 hunks)app/src/main/java/com/texthip/thip/ui/group/search/screen/GroupSearchScreen.kt(4 hunks)app/src/main/java/com/texthip/thip/ui/group/search/viewmodel/GroupSearchViewModel.kt(1 hunks)app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupUiState.kt(1 hunks)app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt(2 hunks)app/src/main/java/com/texthip/thip/ui/navigator/extensions/GroupNavigationExtensions.kt(1 hunks)app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt(7 hunks)app/src/main/java/com/texthip/thip/ui/navigator/routes/GroupRoutes.kt(1 hunks)app/src/main/java/com/texthip/thip/ui/search/viewmodel/BookDetailViewModel.kt(4 hunks)app/src/main/java/com/texthip/thip/ui/search/viewmodel/SearchBookGroupViewModel.kt(3 hunks)app/src/main/java/com/texthip/thip/ui/search/viewmodel/SearchBookViewModel.kt(4 hunks)app/src/main/res/values/strings.xml(1 hunks)
💤 Files with no reviewable changes (2)
- app/src/main/java/com/texthip/thip/data/service/GroupService.kt
- app/src/main/java/com/texthip/thip/data/repository/GroupRepository.kt
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-13T05:42:13.580Z
Learnt from: rbqks529
PR: THIP-TextHip/THIP-Android#79
File: app/src/main/java/com/texthip/thip/ui/search/viewmodel/BookDetailViewModel.kt:26-27
Timestamp: 2025-08-13T05:42:13.580Z
Learning: ViewModelScope는 기본적으로 Dispatchers.Main을 사용하므로, Repository의 suspend 함수에서 네트워크 호출 시 명시적으로 withContext(Dispatchers.IO)로 전환해야 메인 스레드 블로킹을 방지할 수 있다. suspend 함수라고 해서 자동으로 적절한 디스패처가 사용되지는 않는다.
Applied to files:
app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.ktapp/src/main/java/com/texthip/thip/ui/search/viewmodel/SearchBookViewModel.kt
📚 Learning: 2025-07-01T07:19:10.981Z
Learnt from: rbqks529
PR: THIP-TextHip/THIP-Android#30
File: app/src/main/java/com/texthip/thip/ui/common/cards/CardItemRoomSmall.kt:108-112
Timestamp: 2025-07-01T07:19:10.981Z
Learning: The user rbqks529 prefers to extract hardcoded UI strings to StringResource files (strings.xml) rather than keeping them inline in Compose components, which is a good practice for maintainability and localization in Android development.
Applied to files:
app/src/main/java/com/texthip/thip/data/provider/StringResourceProvider.kt
🧬 Code Graph Analysis (6)
app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt (4)
app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt (1)
updateState(41-43)app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt (1)
updateState(27-29)app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt (1)
updateState(26-28)app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt (2)
updateState(33-35)showToastMessage(192-199)
app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookSearchBottomSheet.kt (5)
app/src/main/java/com/texthip/thip/ui/common/bottomsheet/CustomBottomSheet.kt (1)
CustomBottomSheet(44-129)app/src/main/java/com/texthip/thip/ui/common/forms/SearchBookTextField.kt (1)
SearchBookTextField(38-112)app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupEmptyBookSheetContent.kt (1)
EmptyBookSheetContent(22-55)app/src/main/java/com/texthip/thip/ui/common/header/HeaderMenuBarTab.kt (1)
HeaderMenuBarTab(23-69)app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookListWithScrollbar.kt (1)
GroupBookListWithScrollbar(24-62)
app/src/main/java/com/texthip/thip/ui/group/search/screen/GroupSearchScreen.kt (1)
app/src/main/java/com/texthip/thip/ui/group/search/component/GroupRecentSearch.kt (1)
GroupRecentSearch(30-65)
app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt (4)
app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt (1)
updateState(28-30)app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt (1)
updateState(27-29)app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt (1)
updateState(26-28)app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt (1)
updateState(33-35)
app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.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/navigator/navigations/GroupNavigation.kt (1)
app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomUnlockScreen.kt (1)
GroupRoomUnlockScreen(41-176)
🪛 detekt (1.23.8)
app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt
[warning] 147-147: The caught exception is swallowed. The original exception could be lost.
(detekt.exceptions.SwallowedException)
🔇 Additional comments (64)
app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupEmptyBookSheetContent.kt (1)
29-29: 상단 패딩 추가 LGTMUI 간격 조정(Top 20dp)은 레이아웃 안정성 측면에서 문제 없습니다.
app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomMainResponse.kt (1)
1-1: 패키지 마이그레이션 적절rooms.response로의 패키지 이동은 컨텍스트상 일관성을 높여줍니다. 직렬화 키 변경이 없고, 종속 import만 정리되면 문제 없습니다.
app/src/main/java/com/texthip/thip/data/model/rooms/response/CreateRoomResponse.kt (1)
1-1: 패키지 경로 변경 승인서비스/레포지토리 참조만 최신 경로로 정리되면 런타임 영향은 없습니다.
app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupUiState.kt (1)
3-4: *rooms.로의 import 정리 LGTM모델 패키지 리네임과 맞춰 잘 반영되어 있습니다.
app/src/main/java/com/texthip/thip/data/model/rooms/request/CreateRoomRequest.kt (2)
1-1: 패키지 이동 문제 없음생성 요청 DTO의 패키지 이동은 구조 정리에 유익하며, 직렬화 키가 유지되므로 API 호환성에는 영향 없어 보입니다.
8-17: 확인 결과 요약 — UI에서 대부분 검증 처리됨(백엔드 스펙 확인 필요)요약:
- 레포 내에서 요청 생성/포맷은 확인됨. 하지만 백엔드(또는 API 스펙) 요구사항과 정확히 일치하는지 확인이 필요합니다.
코드 위치 및 확인 내용:
- app/src/main/java/com/texthip/thip/data/model/rooms/request/CreateRoomRequest.kt
- recruitCount: Int, password: String? = null, isPublic: Boolean (요청 스키마)
- app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt
- DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy.MM.dd") (날짜 포맷 사용)
- CreateRoomRequest 생성부(약: progressStartDate/EndDate = meetingStartDate/EndDate.format(DATE_FORMATTER), recruitCount = memberLimit, password = if (isPrivate) password else null, isPublic = !isPrivate)
- app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomUiState.kt
- UI 유효성 검사: isDurationValid(1..90일), isCountValid(memberLimit in 2..30), isPasswordValid(!isPrivate || password.length == 4), isFormValid 체크 포함
- mock 변환(toRequest())에서는 비공개 아닐 때 password = "" (Mock 전송용)
- app/src/.../GroupRoomRecruitScreen.kt, GroupRoomScreen.kt (미리보기/하드코드)
- UI 샘플/프리뷰에서 날짜 문자열 예: "2025.02.01" (현재 UI가 점(.) 구분 포맷 사용)
추가 확인·조치 권고:
- 날짜 포맷: 백엔드가 "yyyy.MM.dd" (예: 2025.02.01) 를 기대하는지, 아니면 ISO("yyyy-MM-dd") 같은 다른 포맷을 기대하는지 확인 — 필요하면 DATE_FORMATTER 변경 또는 서버 쪽 변환 반영하세요.
- password 전송 규칙: 백엔드가 공개방(isPublic=true)일 때 password 필드를 null로 받아도 되는지(또는 빈 문자열/생략을 요구하는지) 확인. 현재 ViewModel은 공개방일 때 null로 전송합니다.
- recruitCount/비밀번호 제약: 백엔드의 허용 최소/최대 값(및 비밀번호 형식(숫자만 등))이 UI의 검증(2..30, 패스워드 길이 == 4)과 일치하는지 확인. 불일치 시 UI 검증을 조정하거나 서버 에러를 사용자에게 적절히 노출하세요.
참고: 레포 검색 결과, CreateRoomRequest를 isPublic=true로 생성하면서 password가 non-null인 호출은 발견되지 않았습니다.
태그:
app/src/main/java/com/texthip/thip/data/model/rooms/response/MyRoomListResponse.kt (2)
1-1: 패키지 rooms.response로의 마이그레이션 적절합니다Rooms 도메인으로의 이동이 PR 방향(그룹 → 룸 분리)과 일치합니다.
1-1: 확인 — group 네임스페이스 및 'Secrete' 오탈자 잔여 없음제공하신 스크립트로 전체 검색을 실행한 결과, 'com.texthip.thip.data.model.group.(request|response)' 경로와 'Secrete' 문자열(및 RoomsService/RoomsRepository 내 'rooms.response' 관련 항목)에 대한 일치 항목이 발견되지 않았습니다. 추가 조치 불필요합니다.
app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomJoinResponse.kt (1)
1-1: 패키지 이동 적절합니다서비스/리포지토리에서 rooms.response로 통일된 점과 일관됩니다.
app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomJoinRequest.kt (1)
1-1: 패키지 이동 적절합니다RoomsService/Repository 사용처와 정합됩니다.
app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomUiState.kt (2)
25-26: 검색 상태 필드 추가(LGTM)searchResults, isSearching 분리는 UI 명확성에 도움이 됩니다. ViewModel의 디바운스/로딩 상태와도 매칭됩니다.
25-26: 검색 상태 초기화 타이밍 점검 요청
- 검색 시트 닫힘, 검색어 비움, 책 선택 시 searchResults 클리어 및 isSearching=false로의 리셋이 일관되게 동작하는지 확인이 필요합니다.
리셋 지점이 누락되어 있다면 알려주시면 관련 핸들러/Reducer 코드 스니펫을 제안드리겠습니다.
app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomCloseResponse.kt (1)
1-9: 신규 응답 모델 추가(LGTM)모집 마감 API 응답으로 roomId만 필요하다면 충분합니다. 후속적으로 서버가 상태/시간 정보를 확장 제공하면 필드 보강 고려.
app/src/main/java/com/texthip/thip/ui/group/done/screen/GroupDoneScreen.kt (1)
27-27: MyRoomResponse import 경로 마이그레이션 LGTMrooms.response로의 경로 변경이 일관적입니다. 관련 ViewModel/UiState도 함께 이관되었다면 문제 없겠습니다.
app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt (1)
30-30: MyRoomResponse import 경로 마이그레이션 LGTM도메인 재구성 방향(rooms.*)과 일치합니다. 화면 로직 변경이 없어 리스크도 낮습니다.
app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneUiState.kt (2)
3-3: 패키지 변경 반영 LGTMrooms.response로의 마이그레이션이 일관성 있게 반영되었습니다.
5-15: 검사 결과: 레거시 import(com.texthip.thip.data.model.group.response.*) 참조 없음 — 수동 확인 권고제공하신 ripgrep 스크립트로 전체 저장소를 검색했으며 일치 항목이 발견되지 않았습니다. 다만 검색 결과가 없었던 점을 감안해 IDE(또는 에디터)의 전체 검색/자동 리팩토링 결과로 한 번 더 확인해 주세요.
- 권장 조치:
- IDE에서 "MyRoomResponse" 등 대상 타입으로 프로젝트 전체 검색
- PR 변경 파일들(+관련 모듈)의 import 경로만 최종 확인
app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupDeadlineRoomSection.kt (2)
32-35: 모델 패키지 이동 반영 OKRoomMainList/RoomMainResponse import 경로 변경이 PR 전반의 rooms.response 마이그레이션과 일치합니다.
196-197: API 보장 검증: isRecruiting 하드코딩주석에 의존해
isRecruiting = true로 고정되어 있습니다. 향후 API 응답이 비모집 항목을 포함하게 되면 UX 오류가 발생할 수 있습니다. 응답 모델에 모집 상태 필드가 있다면 해당 값을 사용하는 것으로 전환하는지 확인해 주세요.app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyUiState.kt (1)
3-3: 패키지 변경 반영 LGTMrooms.response로의 import 변경이 적절합니다.
app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupMainCard.kt (1)
35-35: 패키지 이동 반영 OKJoinedRoomResponse import 경로 변경이 일관되며, 사용처 타입도 유지됩니다.
app/src/main/java/com/texthip/thip/data/model/rooms/response/JoinedRoomListResponse.kt (2)
1-1: 패키지 경로 변경 LGTMgroup.response → rooms.response 이동이 데이터/서비스 계층 리팩터링 방향과 일치합니다.
7-24: 마이그레이션 영향 범위 점검 — 레거시 참조 없음레포 전역 검색 결과
com.texthip.thip.data.model.group.response패키지로의 레거시 참조는 발견되지 않았습니다. JoinedRoom 관련 타입은 모두com.texthip.thip.data.model.rooms.response를 사용하고 있습니다.점검된 주요 파일:
- app/src/main/java/com/texthip/thip/data/model/rooms/response/JoinedRoomListResponse.kt (정의)
- app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupUiState.kt
- app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupPager.kt
- app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupMainCard.kt
- app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt
- app/src/main/java/com/texthip/thip/data/service/RoomsService.kt
- app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt
추가 조치 불필요합니다.
app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupPager.kt (1)
19-19: 모델 패키지 마이그레이션 반영 적절
JoinedRoomResponseimport 경로를 rooms로 변경한 점, 파일 내 사용 필드들과도 일관됩니다. 로직 영향 없음 확인되었습니다.app/src/main/java/com/texthip/thip/ui/navigator/routes/GroupRoutes.kt (1)
30-32: 비밀번호 입력용 라우트(RoomUnlock) 추가 적절기존 패턴과 동일하게
@Serializable데이터 클래스로 추가되어 타입-세이프 네비게이션과 일관성이 유지됩니다.app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt (1)
7-7: Repository 교체는 무난 — 추가(수동) 검증 필요제공하신 스크립트가 출력 없이 종료되어 자동 확인이 불확실합니다. 아래 항목을 직접 확인하거나, 다음 개선된 스크립트를 실행해 잔존 참조·DI 바인딩·디스패처 사용 여부를 점검해 주세요.
확인해야 할 항목(간단 요약)
- Hilt 바인딩: @module / @BINDS / @provides에서 GroupRepository가 남아있지 않고 RoomsRepository로 바인딩되는지 확인.
- 잔여 참조: 프로젝트 전체에서 "GroupRepository"가 더 이상 사용되지 않는지 확인.
- RoomsRepository 내부: 네트워크/DB 등 IO 작업이 withContext(Dispatchers.IO) 또는 적절한 dispatcher 주입으로 처리되는지 확인.
- DI 주입 지점: GroupViewModel 등 생성자 타입(Repository)이 올바르게 변경되었는지 확인(예: app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt).
- 빌드/테스트: 변경 후 앱 빌드 및 관련 단위/통합 테스트에서 주입 오류가 없는지 확인.
권장 스크립트 (로컬에서 실행)
# 전체에서 잔존 참조 검색 rg -n --hidden --glob '!**/build/**' "GroupRepository|RoomsRepository" || true # Hilt 모듈/바인딩(간단 스캔) rg -n --hidden --glob '!**/build/**' "@Module|@Binds|@Provides" -S | rg -n "GroupRepository|RoomsRepository" -A2 -B2 || true # RoomsRepository 내부에서 Dispatchers.IO/withContext 사용 흔적 확인 rg -n --hidden --glob '!**/build/**' "Dispatchers\\.IO|withContext\\(" app/src/main/java app/src/main/kotlin || true검증이 끝나면 결과(출력 또는 스냅샷)를 알려주시면, 추가로 스크립트를 실행해 더 자세히 확인하거나 수정이 필요한 부분을 구체적으로 지적하겠습니다.
app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt (1)
5-17: 검증 결과 — RoomsRepository는 @Inject로 제공되며 GroupRepository 참조 없음결과 요약: RoomsRepository.kt에
class RoomsRepository @Inject constructor(...)가 존재해 Hilt가 별도 @Module/@Provides/@BINDS 없이도 주입합니다. DI 모듈에서 RoomsRepository 관련 바인딩 항목은 발견되지 않았으나 이는 정상입니다. 코드베이스에서 GroupRepository 참조도 발견되지 않았습니다.주의/확인할 점:
- app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt — class RoomsRepository @Inject constructor (약 라인 26)
- app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt — ViewModel이 RoomsRepository를 주입하도록 변경됨
- 검색 결과: DI 모듈에 RoomsRepository용 @Binds/@provides 항목 없음(정상). GroupRepository 잔존 참조 없음.
권장 추가 확인:
- RoomsRepository 생성자 의존성(예: RoomsService, GenreManager 등)이 Hilt 그래프에 제공되는지(해당 클래스들이 @Inject 생성자이거나 모듈에서 제공되는지) 확인해 주세요.
app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt (1)
5-17: 확인 완료 — RoomsRepository 주입 및 GroupRepository 잔존 여부 검증RoomsRepository.kt가 존재하며 클래스 선언에
@Inject생성자가 있어 Hilt로 직접 주입됩니다. 명시적@Module/@Provides/@Binds바인딩은 검색되지 않았으나 현재 상태에서는 필요하지 않습니다.GroupRepository에 대한 잔존 참조도 발견되지 않았습니다. 따라서 변경은 일관되고 문제가 없어 보입니다.app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt (1)
34-38: RecentSearchService DI 등록 LGTM
Retrofit 기반 RecentSearchService 제공자 추가가 명세와 일치합니다. RecentSearchRepository가 해당 프로바이더를 의존하도록 연결되었는지만 확인하면 됩니다.app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt (2)
27-30: 모델 패키지 마이그레이션 반영 LGTM
rooms.response로의 타입 교체 및 Preview 데이터 업데이트가 잘 정렬되어 있습니다. 런타임 로직 변경 없이 타입만 이전되어 안정적입니다.
146-155: 확인 완료 — roomId 네비게이션 인수는 Int로 일치합니다GroupScreen에서 전달한 room.roomId(Int)는 NavGraph/route 정의와 네비게이션 확장 함수에서 모두 Int로 선언·수신되고 있음을 확인했습니다.
- app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt — onNavigateToGroupRecruit: (Int) -> Unit 선언 및 호출(onNavigateToGroupRecruit(room.roomId)) (라인 51, 153)
- app/src/main/java/com/texthip/thip/ui/navigator/extensions/GroupNavigationExtensions.kt — fun navigateToGroupRecruit(roomId: Int) { navigate(GroupRoutes.Recruit(roomId)) } (라인 ~55–56)
- app/src/main/java/com/texthip/thip/ui/navigator/routes/GroupRoutes.kt — data class Recruit(val roomId: Int) : GroupRoutes() (라인 28)
- app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt — composable<GroupRoutes.Recruit>에서 backStackEntry.toRoute<GroupRoutes.Recruit>()로 route.roomId 사용 (라인 192–195)
따라서 수정 불필요합니다.
app/src/main/java/com/texthip/thip/data/service/RecentSearchService.kt (1)
1-23: 최근 검색어 서비스 인터페이스 — 패키지 경로 확인 완료 (LGTM)인터페이스 시그니처는 명확하며, BaseResponse와 RecentSearchResponse의 패키지 경로가 코드베이스와 일치함을 확인했습니다:
- app/src/main/java/com/texthip/thip/data/model/base/BaseResponse.kt — package com.texthip.thip.data.model.base
- app/src/main/java/com/texthip/thip/data/model/book/response/RecentSearchResponse.kt — package com.texthip.thip.data.model.book.response
변경 승인합니다.
app/src/main/java/com/texthip/thip/ui/search/viewmodel/SearchBookGroupViewModel.kt (4)
5-5: 새로운 의존성 추가 확인StringResourceProvider 도입으로 하드코딩된 오류 메시지를 리소스 기반으로 변경하는 것은 좋은 개선입니다.
Also applies to: 8-8
19-22: 생성자 변경으로 인한 의존성 주입 확인StringResourceProvider가 추가된 생성자 변경사항이 올바르게 구현되었습니다. Hilt DI가 제대로 동작할 것으로 보입니다.
88-88: 리소스 기반 오류 메시지로 개선하드코딩된 오류 메시지를 StringResourceProvider를 통한 리소스 기반 메시지로 변경한 것은 좋은 개선입니다. 국제화 지원과 유지보수성이 향상되었습니다.
96-96: 일관성 있는 오류 처리exception.message가 null일 때 리소스 기반 폴백 메시지를 제공하는 것은 일관성 있는 오류 처리 패턴입니다.
app/src/main/java/com/texthip/thip/ui/search/viewmodel/BookDetailViewModel.kt (4)
5-5: StringResourceProvider 도입리소스 기반 문자열 처리를 위한 새로운 의존성 추가가 적절합니다.
Also applies to: 7-7
17-20: 생성자 의존성 확장BookRepository와 함께 StringResourceProvider를 주입받도록 생성자가 확장되었습니다. DI 구성이 올바릅니다.
40-40: 오류 메시지 국제화 개선exception.message가 null일 때 리소스 기반 오류 메시지를 제공하는 것은 사용자 경험 향상에 도움이 됩니다.
65-65: 일관된 오류 처리 패턴loadBookDetail과 동일한 패턴으로 saveBook에서도 리소스 기반 오류 메시지를 사용하는 것은 일관성 있는 구현입니다.
app/src/main/java/com/texthip/thip/data/service/BookService.kt (1)
17-56: 최근 검색(RecentSearch) 분리 — 구현 확인 완료RecentSearchService 인터페이스와 관련 Repository/DI 파일이 존재하며, @get("recent-searches")와 @delete("recent-searches/{recentSearchId}") 엔드포인트를 포함하는 것을 확인했습니다. 리팩토링(관심사 분리)은 적절합니다.
검토한 파일:
- app/src/main/java/com/texthip/thip/data/service/RecentSearchService.kt — 인터페이스 확인 (getRecentSearches, deleteRecentSearch)
- app/src/main/java/com/texthip/thip/data/repository/RecentSearchRepository.kt — 레포지토리 존재 확인
- app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt — 서비스 모듈 파일 존재 (필요 시 등록 여부 추가 확인 가능)
app/src/main/java/com/texthip/thip/ui/search/viewmodel/SearchBookViewModel.kt (5)
7-7: 새로운 Repository 의존성 추가RecentSearchRepository 의존성이 추가되어 최근 검색 기능이 전용 Repository로 분리되었습니다.
19-22: 생성자 의존성 확장BookRepository와 RecentSearchRepository 두 개의 Repository를 주입받도록 생성자가 확장되었습니다. 관심사 분리가 잘 이루어졌습니다.
204-204: 최근 검색 데이터 소스 변경BookRepository에서 RecentSearchRepository로 최근 검색 데이터 소스가 변경되었고, "BOOK" 타입을 명시적으로 전달하고 있습니다. 이는 다른 타입의 검색도 지원할 수 있는 확장 가능한 구조입니다.
226-226: 삭제 기능 데이터 소스 변경최근 검색 삭제도 동일하게 RecentSearchRepository로 변경되었습니다. 일관성 있는 변경사항입니다.
54-57: 결론: Repository에 withContext(Dispatchers.IO)를 일괄 추가할 필요 없음검증 결과, 리포지토리들은 Retrofit의 suspend 서비스 메서드들을 직접 호출하고 있으며 응답 핸들러(handleBaseResponse)도 단순 매핑만 수행하므로 네트워크 호출은 비동기적으로 처리되어 ViewModel에서 별도 Dispatchers.IO 전환이 필요하지 않습니다.
확인한 위치:
- app/src/main/java/com/texthip/thip/ui/search/viewmodel/SearchBookViewModel.kt — viewModelScope.launch { performSearch(...) } 호출
- app/src/main/java/com/texthip/thip/data/service/BookService.kt — suspend fun getBooks / searchBooks 등 (네트워크 API)
- app/src/main/java/com/texthip/thip/data/repository/BookRepository.kt — bookService.* 호출(명시적 Dispatcher 전환 없음)
- app/src/main/java/com/texthip/thip/data/service/RecentSearchService.kt — suspend fun getRecentSearches / deleteRecentSearch
- app/src/main/java/com/texthip/thip/data/repository/RecentSearchRepository.kt — recentSearchService.* 호출(명시적 Dispatcher 전환 없음)
- app/src/main/java/com/texthip/thip/data/service/RoomsService.kt — 여러 suspend API 정의
- app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt — roomsService.* 호출(명시적 Dispatcher 전환 없음)
- app/src/main/java/com/texthip/thip/data/model/base/ApiResponseHandler.kt — handleBaseResponse 구현(단순 매핑)
주의: 리포지토리에서 향후 파일 I/O, 동기 DB 호출 또는 다른 블로킹 작업을 직접 수행할 경우에는 withContext(Dispatchers.IO)로 전환해야 합니다.
Likely an incorrect or invalid review comment.
app/src/main/java/com/texthip/thip/data/provider/StringResourceProvider.kt (4)
9-12: Singleton 스코프와 DI 구성@singleton 어노테이션과 @ApplicationContext를 통한 Context 주입이 올바르게 구성되었습니다. 앱 전체에서 하나의 인스턴스만 생성되어 메모리 효율적입니다.
14-16: 기본 문자열 리소스 접근 메서드기본적인 문자열 리소스 접근을 위한 메서드가 적절히 구현되었습니다.
18-20: 포맷팅 지원 문자열 메서드가변 인자를 지원하는 문자열 포맷팅 메서드가 추가되어 동적 문자열 생성이 가능합니다. spread 연산자(
*formatArgs) 사용이 올바릅니다.
1-21: 전체적인 구현 평가StringResourceProvider는 이전 학습 내용에서 언급된 사용자 선호사항(하드코딩된 문자열을 리소스로 추출)을 잘 반영한 구현입니다. Context에 직접 의존하지 않고 DI를 통해 문자열 리소스에 접근할 수 있게 해주는 깔끔한 추상화 계층입니다.
app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitUiState.kt (1)
4-4: 응집도 있는 모델로의 import 교체 LGTMRoomRecruitingResponse 패키지 이동에 따른 import 갱신이 적절합니다. 모델 변경(예: categoryColor 추가)이 있다면 해당 필드가 사용하는 UI까지 반영되었는지 한 번만 점검하세요.
app/src/main/java/com/texthip/thip/data/repository/RecentSearchRepository.kt (1)
22-27: 삭제 API null 응답 처리 로직은 간결하고 명확합니다BaseResponse.data가 null이어도 성공 시 Unit을 반환하도록 처리한
?: Unit은 호출부 사용성을 높입니다. 기존 레포 패턴과도 일관적입니다.app/src/main/java/com/texthip/thip/data/repository/BookRepository.kt (1)
21-25: 검증 완료 — getBooks 반환 타입 변경( Result<BookListResponse?> ) 반영 확인됨GroupMakeRoomViewModel에서 response?.bookList로 매핑되어 있으며, BookSavedResponse의 필드(bookImageUrl, bookTitle, authorName, isbn 등)를 toBookData에서 정상 사용하고 있습니다. 추가로 MostSearchedBooksResponse의 bookList 사용도 정상입니다.
점검한 위치:
- app/src/main/java/com/texthip/thip/data/repository/BookRepository.kt — getBooks 반환 타입 확인
- app/src/main/java/com/texthip/thip/data/model/book/response/BookListResponse.kt — BookSavedResponse(필드: bookId, bookTitle, authorName, publisher, bookImageUrl, isbn)
- app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt — savedBooks/groupBooks 호출부 및 BookSavedResponse.toBookData 매핑 확인
- app/src/main/java/com/texthip/thip/ui/search/viewmodel/SearchBookViewModel.kt — mostSearchedBooks.bookList 사용 확인
결론: 호출부와 필드명 반영이 이미 되어 있어 추가 수정 불필요합니다.
app/src/main/java/com/texthip/thip/ui/group/search/screen/GroupSearchScreen.kt (1)
33-34: GroupRecentSearch import 경로 확인 필요현재 import가
ui.group.myroom.component.GroupRecentSearch를 가리키는데, 컴포넌트가ui/group/search/component로 이동했을 가능성이 있습니다. 실제 정의 위치와 일치하는지 확인 바랍니다. 빌드 에러가 발생하면 import만 교정하면 됩니다.Also applies to: 151-154
app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomUnlockViewModel.kt (1)
63-75: 상태 초기화 함수 initializeState는 명확하고 유용합니다화면 진입 시 상태를 일괄 초기화하는 접근이 합리적입니다. 호출 위치에서 누락만 없도록 주의해 주세요.
app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt (1)
41-43: 일관된 상태 업데이트 패턴 사용다른 ViewModel들과 동일한
updateState패턴을 사용하고 있어 좋습니다. 코드 일관성이 잘 유지되고 있습니다.app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookSearchBottomSheet.kt (1)
53-58: 검색/탭 데이터 소스 결정 로직 명확하고 적절합니다.
searchText유무로 검색 결과 vs 탭별 목록을 선택하는 방식은 단순명료하고 상태 호이스팅 설계와도 잘 맞습니다.app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt (1)
168-175: 네비게이션 후 상태 초기화 처리 적절합니다
shouldNavigateToRoomPlayingScreen와roomId를 네비게이션 완료 시점에 초기화해 중복 네비게이션을 방지하는 패턴이 깔끔합니다.app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt (1)
258-263: 검색 바인딩/상태 전달 구조 적절합니다
searchResults,isLoading,isSearching상태를 외부에서 주입하고,onSearch로 이벤트를 상위로 올리는 상태 호이스팅이 잘 되어 있습니다.app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt (1)
78-86: 응답 널가드 및 결과 반환 설계가 명확하고 견고합니다
createRoom/closeRoom에서 널 응답을 명시적으로 예외로 전환해Result에 담는 패턴이 호출측 에러 처리에 유리합니다.Also applies to: 108-116
app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt (1)
201-207: 비밀번호 해제 → 참여 승인 신호 연결 로직이 명확하고 견고합니다RoomUnlock에서 SavedStateHandle로 신호를 전달하고 Recruit에서 수신 후 원복하는 흐름이 깔끔합니다. 중복 트리거 방지까지 잘 처리되었습니다.
Also applies to: 246-257
app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt (3)
42-44: 모델 패키지 마이그레이션 반영 적절rooms.response로의 마이그레이션이 코드 흐름과 일관되며, 타입 사용도 정상입니다.
373-375: 도서 상세 클릭 핸들링 추가 좋습니다ISBN 기반으로 상세 화면으로 라우팅하도록 연결한 부분 적절합니다.
503-515: Preview 데이터 최신 스키마 반영 확인categoryColor 추가 및 rooms.response 경로 반영이 올바릅니다. 프리뷰로 시각 검증하기 좋습니다.
| @SerialName("bookId") val bookId: Int = 0, | ||
| @SerialName("bookTitle") val bookTitle: String = "", | ||
| @SerialName("authorName") val authorName: String = "", | ||
| @SerialName("publisher") val publisher: String = "", | ||
| @SerialName("imageUrl") val imageUrl: String? = null | ||
| @SerialName("bookImageUrl") val bookImageUrl: String? = null, | ||
| @SerialName("isbn") val isbn: String = "", | ||
| ) No newline at end of file |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
BookSavedResponse 스키마 변경 — 호출부/백엔드 정합성 확인 필요
- bookId(Int) 추가(기본값 0): API가 아직 값을 내려주지 않는 경우 0이 주입됩니다. 0을 유효 ID로 오인하는 로직이 없는지 확인 필요.
- imageUrl → bookImageUrl로 필드명 변경: 모든 호출부가 새 필드명으로 마이그레이션되었는지 확인 필요.
- isbn 위치 변경은 직렬화에 영향 없음(@SerialName 사용) — 문제 없음.
다음 점검을 권장합니다.
검증 스크립트:
🏁 Script executed:
#!/bin/bash
# 이전 필드명 사용 흔적 확인
rg -n --no-heading "\.imageUrl"
# bookId가 0일 때의 분기/검증 로직 확인
rg -n --no-heading "bookId"Length of output: 2835
BookSavedResponse 스키마 변경 — imageUrl 미마이그레이션 다수 발견, bookId(0) 처리 검토 필요
검증 요약: 실행한 grep 결과 다수 파일에서 여전히 .imageUrl을 참조하고 있어 필드명 변경(bookImageUrl) 마이그레이션이 완료되지 않았습니다. bookId는 선언부(app/src/main/java/com/texthip/thip/data/model/book/response/BookListResponse.kt:14) 외에 검색되지 않았으나, 기본값 0을 유효 ID로 오인하는 로직이 없는지 확인이 필요합니다.
수정이 필요한 호출부(발견된 주요 파일)
- app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookScreen.kt
- app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookGroupScreen.kt
- app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookDetailScreen.kt
- app/src/main/java/com/texthip/thip/ui/search/component/SearchBookFilteredResult.kt
- app/src/main/java/com/texthip/thip/ui/search/component/SearchActiveField.kt
- app/src/main/java/com/texthip/thip/ui/search/component/SearchRecentBook.kt
- app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupSelectBook.kt
- app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt
- app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookListWithScrollbar.kt
- app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt
권장 조치 (간단)
- Book 관련 호출부에서 item.imageUrl → item.bookImageUrl로 교체(또는 DTO/매핑 계층에서 변환).
- nullable 처리·기본값(예: selectedBook.bookImageUrl ?: R.drawable...) 확인.
- bookId 기본값 0에 대한 방어 코드 추가(0을 유효 ID로 처리하는 분기 제거 또는 Int? 사용 등) 또는 백엔드 안정화 협의.
- 변경 반영 여부 재검증: rg -n --no-heading ".imageUrl" 및 rg -n --no-heading ".bookImageUrl"
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/data/model/book/response/BookListResponse.kt
lines 14-20, the model renamed imageUrl→bookImageUrl and sets bookId default to
0; update call sites and defensively handle defaults: replace all usages of
item.imageUrl with item.bookImageUrl (or map DTOs at a single conversion layer),
ensure nullable handling where used (e.g., use selectedBook.bookImageUrl ?:
fallbackResource or conditional UI rendering), and audit code that treats
bookId==0 as a valid id—either change bookId to Int? or add explicit guards that
treat 0 as invalid before using it. After edits, re-run searches (rg -n
--no-heading "\.imageUrl" and rg -n --no-heading "\.bookImageUrl") to confirm
migration completeness.
| @@ -1,4 +1,4 @@ | |||
| package com.texthip.thip.data.model.group.request | |||
| package com.texthip.thip.data.model.rooms.request | |||
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
허용값(Join 타입) 사용 현황 점검 요청
코드 내 하드코딩된 문자열이 있는지 빠르게 확인해 enum 전환 난이도를 파악하면 좋겠습니다.
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Join/CANCEL/LEAVE 등 조인 관련 상수 후보 검색"
rg -n --hidden --glob '!.git' -e '"JOIN"' -e '"CANCEL"' -e '"LEAVE"' -e '"REQUEST"'Length of output: 184
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "1) 대문자 토큰(JOIN/CANCEL/LEAVE/REQUEST) 단어 경계 검색 (대소문자 무시)"
rg -n --hidden --glob '!.git' -S -i -e '\b(JOIN|CANCEL|LEAVE|REQUEST)\b' -g '!**/build/**' -g '!**/node_modules/**' -A2 -B2 || true
echo
echo "2) 큰따옴표로 감싼 토큰(\"JOIN\" 등) 검색"
rg -n --hidden --glob '!.git' -S -i -e '"(JOIN|CANCEL|LEAVE|REQUEST)"' -g '!**/build/**' -g '!**/node_modules/**' -A2 -B2 || true
echo
echo "3) 소문자 포함 일반 토큰(join/cancel/leave/request) 검색"
rg -n --hidden --glob '!.git' -S -i -e '\b(join|cancel|leave|request)\b' -g '!**/build/**' -g '!**/node_modules/**' -A2 -B2 || true
echo
echo "4) enum class .*Join 형태 검색"
rg -n --hidden --glob '!.git' -S -i -e 'enum class .*Join' -g '!**/build/**' -g '!**/node_modules/**' -A2 -B2 || true
echo
echo "5) JoinType/JoinStatus/JoinRequest 등 관련 식별자 검색"
rg -n --hidden --glob '!.git' -S -i -e 'Join(Type|Status|Action|Request)?' -g '!**/build/**' -g '!**/node_modules/**' -A2 -B2 || true
echo
echo "6) 파일 경로에 'join' 포함된 파일 목록(대소문자 무시)"
rg --files --hidden -g '!.git' -g '!**/build/**' -g '!**/node_modules/**' | rg -i '/.*join.*' || trueLength of output: 140317
허용값(Join 타입) enum 전환 검토 — 사용처 확인 결과
간단 요약: 레포 전체 검색 결과, API 요청/저장소는 문자열 타입을 사용하고 있고 UI에만 지역 enum(RoomAction)이 존재합니다. 중앙화된 enum(예: JoinType/RoomActionType)으로 정리하는 것이 안전합니다.
주의가 필요한 파일(발견된 위치)
- app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomJoinRequest.kt
- data class RoomJoinRequest(@SerialName("type") val type: String)
- app/src/main/java/com/texthip/thip/data/service/RoomsService.kt
- app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt
- suspend fun joinOrCancelRoom(roomId: Int, type: String) { val request = RoomJoinRequest(type = type) ... }
- app/src/main/java/com/texthip/thip/ui/group/room/mock/RoomAction.kt
- enum class RoomAction(val value: String) { JOIN("join"), CANCEL("cancel") } (현재 UI/mock 영역에 위치)
- app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt
- repository.joinOrCancelRoom(roomId, RoomAction.JOIN.value) / RoomAction.CANCEL.value 사용
권장 작업(간단)
- 공용 enum 생성 (예: com.texthip.thip.data.model.rooms.JoinType 또는 RoomActionType) 및 각 케이스에 API 값 매핑(@SerialName 또는 직렬화 매핑).
- RoomJoinRequest.type을 enum으로 변경하거나, 최소한 RoomsRepository에서 enum → 문자열 매핑을 한 곳에서만 수행하도록 리팩터링.
- UI에 있는 RoomAction/GroupBottomButtonType 등은 공용 enum을 사용하도록 교체(단, UI 전용 값과 API 값 구분 필요).
- 변경 후 문자열 리터럴("join","cancel","leave" 등) 누락 여부 전역 재검증 및 관련 호출부(Repository/Service/ViewModel) 테스트 수행.
위 파일들을 중심으로 수정하면 enum 전환 난이도가 낮고 영향 범위를 관리하기 쉬울 것으로 보입니다. 상세 변경안 원하시면 간단한 코드 예시(enum 정의 + Request/Repository 변환)를 드리겠습니다.
| package com.texthip.thip.data.model.rooms.request | ||
|
|
||
| import kotlinx.serialization.SerialName | ||
| import kotlinx.serialization.Serializable | ||
|
|
||
|
|
||
| @Serializable | ||
| data class RoomSecreteRoomRequest( | ||
| @SerialName("password") val password: String | ||
| ) No newline at end of file |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
타입/파일명 오탈자: Secrete → Secret (명명 정정 권장)
Secrete는 “분비하다”의 의미로, 의도는 “Secret”인 것으로 보입니다. 조기에 정정하지 않으면 전역적으로 오타가 전파되어 유지보수 비용이 커집니다. 의미 전달 측면에서도 RoomPasswordRequest 같은 명명은 더 직관적입니다.
최소 변경으로 정정하려면 클래스명을 다음과 같이 바꾸고 파일명도 일치시키세요.
-@Serializable
-data class RoomSecreteRoomRequest(
+@Serializable
+data class RoomSecretRoomRequest(
@SerialName("password") val password: String
)추가로 권장(선택):
- 서비스/레포지토리 메서드들도
postParticipateSecretRoom(...)처럼 일관 명명으로 수정. - 더 직관적인 이름:
RoomPasswordRequest또는SecretRoomRequest.
레포 전역에 퍼진 오타를 일괄 점검하려면 아래 스크립트를 실행하세요.
🏁 Script executed:
#!/bin/bash
# 'Secrete' 오탈자 전역 검색
rg -n "Secrete" -g "!**/build/**"
# 요청/응답 클래스 쌍 확인
rg -n "RoomSecreteRoom(Request|Response)|RoomSecretRoom(Request|Response)|RoomPasswordRequest" -g "!**/build/**"Length of output: 3334
오타: Secrete → Secret (전역 수정 필요)
검색 결과 'Secrete' 오타가 여러 곳에서 사용되고 있어 클래스명·파일명·메서드명 일괄 수정이 필요합니다. 변경 시 모든 import/호출부를 함께 갱신하세요.
문제 발견 위치:
- app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomSecreteRoomRequest.kt — data class RoomSecreteRoomRequest
- app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomSecreteRoomResponse.kt — data class RoomSecreteRoomResponse
- app/src/main/java/com/texthip/thip/data/service/RoomsService.kt — import 및 suspend fun postParticipateSecreteRoom(...)
- app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt — import, suspend fun postParticipateSecreteRoom(...), RoomSecreteRoomRequest 생성 및 호출
- app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomUnlockViewModel.kt — roomsRepository.postParticipateSecreteRoom(roomId, password) 호출
권장 최소 변경(diff 예시 — Request 클래스):
-@Serializable
-data class RoomSecreteRoomRequest(
+@Serializable
+data class RoomSecretRoomRequest(
@SerialName("password") val password: String
)추가 권장 변경:
- RoomSecreteRoomResponse → RoomSecretRoomResponse (파일명 포함)
- postParticipateSecreteRoom → postParticipateSecretRoom (또는 더 직관적인 이름: RoomPasswordRequest / SecretRoomRequest)
- 파일명(예: RoomSecreteRoomRequest.kt → RoomSecretRoomRequest.kt) 및 모든 import/참조 일괄 수정
📝 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.
| package com.texthip.thip.data.model.rooms.request | |
| import kotlinx.serialization.SerialName | |
| import kotlinx.serialization.Serializable | |
| @Serializable | |
| data class RoomSecreteRoomRequest( | |
| @SerialName("password") val password: String | |
| ) | |
| package com.texthip.thip.data.model.rooms.request | |
| import kotlinx.serialization.SerialName | |
| import kotlinx.serialization.Serializable | |
| @Serializable | |
| data class RoomSecretRoomRequest( | |
| @SerialName("password") val password: String | |
| ) |
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomSecreteRoomRequest.kt
lines 1-10 there is a typo in the class and filename ("Secrete" → "Secret");
rename the file to RoomSecretRoomRequest.kt and the data class to
RoomSecretRoomRequest (retain the property @SerialName("password") val password:
String), then update all imports and call sites accordingly: rename
RoomSecreteRoomResponse → RoomSecretRoomResponse (and its file), change any
service/repository/viewmodel usages and function names such as
postParticipateSecreteRoom → postParticipateSecretRoom (or another agreed name),
update repository to construct RoomSecretRoomRequest, and refactor all affected
files listed (RoomsService.kt, RoomsRepository.kt, GroupRoomUnlockViewModel.kt)
so that all imports and usages compile and tests pass.
| @SerialName("progressEndDate") val progressEndDate: String, | ||
| @SerialName("recruitEndDate") val recruitEndDate: String, | ||
| @SerialName("category") val category: String, | ||
| @SerialName("categoryColor") val categoryColor: String, |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
새 필드(categoryColor) 추가 — 역직렬화 실패 가능성 점검 필요
필드가 non-null이고 기본값이 없어, 백엔드에서 누락 시 kotlinx.serialization 디코딩이 실패할 수 있습니다. 스키마 보장이 명확하지 않다면 nullable + 기본값 또는 안전한 기본값을 권장합니다.
적용 제안(diff):
- @SerialName("categoryColor") val categoryColor: String,
+ @SerialName("categoryColor") val categoryColor: String? = null,검증 스크립트: categoryColor 사용처가 null/빈 문자열에 대해 안전하게 처리되는지 확인
🏁 Script executed:
#!/bin/bash
# categoryColor 사용처 점검
rg -n --no-heading "categoryColor"
# 색 변환 유틸이 존재한다면 함께 확인(예: hexToColor)
rg -n --no-heading "hexToColor|colorFromHex|toColor"Length of output: 1619
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== RoomRecruitingResponse.kt ==="
nl -ba app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomRecruitingResponse.kt | sed -n '1,200p' || true
echo
echo "=== HexToColor.kt ==="
nl -ba app/src/main/java/com/texthip/thip/utils/color/HexToColor.kt | sed -n '1,200p' || true
echo
echo "=== GroupRoomRecruitScreen.kt (1-420) ==="
nl -ba app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt | sed -n '1,420p' || true
echo
echo "=== GroupRoomRecruitScreen.kt (420-620) ==="
nl -ba app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt | sed -n '420,620p' || true
echo
echo "=== GroupRoomHeader.kt ==="
nl -ba app/src/main/java/com/texthip/thip/ui/group/room/component/GroupRoomHeader.kt | sed -n '1,240p' || true
echo
echo "=== GenreColor/related color utilities search ==="
rg -n --no-heading "GenreColor|colorProvider|fromString" || trueLength of output: 2602
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== RoomRecruitingResponse.kt ==="
sed -n '1,200p' app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomRecruitingResponse.kt || true
echo
echo "=== HexToColor.kt ==="
sed -n '1,200p' app/src/main/java/com/texthip/thip/utils/color/HexToColor.kt || true
echo
echo "=== GroupRoomRecruitScreen.kt (330-380) ==="
sed -n '330,380p' app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt || true
echo
echo "=== GroupRoomRecruitScreen.kt (500-520) ==="
sed -n '500,520p' app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt || true
echo
echo "=== GroupRoomHeader.kt (1-120) ==="
sed -n '1,120p' app/src/main/java/com/texthip/thip/ui/group/room/component/GroupRoomHeader.kt || true
echo
echo "=== GenreColor.kt ==="
sed -n '1,240p' app/src/main/java/com/texthip/thip/utils/type/GenreColor.kt || trueLength of output: 11458
categoryColor: 서버 누락 대비 — 모델을 nullable로 바꾸고 사용처를 null-safe 처리하세요
RoomRecruitingResponse의 categoryColor가 non-null(String)으로 정의되어 있어 백엔드가 필드를 보내지 않으면 kotlinx.serialization에서 디코딩이 실패합니다. 또한 GroupRoomRecruitScreen.kt에서 hexToColor(detail.categoryColor)를 바로 호출하고 있어 null 허용으로 변경 시 사용처도 수정해야 합니다. (참고: hexToColor는 잘못된 포맷일 경우 Color.White로 안전하게 폴백합니다.)
수정이 필요한 위치:
- app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomRecruitingResponse.kt
- app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt (hexToColor 호출부)
권장 변경(diff):
--- a/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomRecruitingResponse.kt
@@
- @SerialName("categoryColor") val categoryColor: String,
+ @SerialName("categoryColor") val categoryColor: String? = null,--- a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt
@@
- Text(
- text = detail.category,
- style = typography.info_m500_s12,
- color = hexToColor(detail.categoryColor)
- )
+ Text(
+ text = detail.category,
+ style = typography.info_m500_s12,
+ color = detail.categoryColor?.let { hexToColor(it) } ?: colors.White
+ )Committable suggestion skipped: line range outside the PR's diff.
| data class RoomSecreteRoomResponse( | ||
| @SerialName("matched") val matched: Boolean = false, | ||
| @SerialName("roomId") val roomId: Int = 0 | ||
| ) No newline at end of file |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
클래스/파일명 오탈자(Secrete → Secret) 가능 — 지금 정정 권장
영어 의미상 비공개 방은 “Secret”이 자연스럽습니다. 오탈자가 퍼블릭 API/타 계층으로 확산되기 전에 바로잡는 편이 유지보수에 유리합니다. 파일명과 모든 사용처 일괄 변경을 권장합니다.
적용 제안(diff):
- data class RoomSecreteRoomResponse(
+ data class RoomSecretRoomResponse(추가 작업: 파일명도 RoomSecretRoomResponse.kt로 변경하고, 서비스/레포지토리/뷰모델 등 모든 참조를 함께 수정 필요.
검증 스크립트: “Secrete” 토큰 전역 검색(남은 사용처 파악)
🏁 Script executed:
#!/bin/bash
rg -n --no-heading "Secrete"Length of output: 1835
클래스/파일명 오탈자 수정 필요 (Secrete → Secret)
검증 결과 "Secrete" 토큰이 여러 파일에서 사용되고 있어, 오탈자 해결을 위해 파일명·클래스명·참조를 일괄 변경해야 합니다.
수정이 필요한 위치(검증된 사용처):
- app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomSecreteRoomResponse.kt — data class
- app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomSecreteRoomRequest.kt — data class
- app/src/main/java/com/texthip/thip/data/service/RoomsService.kt — import / suspend fun postParticipateSecreteRoom(...)
- app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt — import / suspend fun postParticipateSecreteRoom(...)
- app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomUnlockViewModel.kt — 호출부
권장 변경 예시(diff):
- data class RoomSecreteRoomResponse(
+ data class RoomSecretRoomResponse(
@SerialName("matched") val matched: Boolean = false,
@SerialName("roomId") val roomId: Int = 0
)- data class RoomSecreteRoomRequest(
+ data class RoomSecretRoomRequest(
@SerialName("password") val password: String
)- suspend fun postParticipateSecreteRoom(roomId: Int, password: String): Result<RoomSecreteRoomResponse> = ...
+ suspend fun postParticipateSecretRoom(roomId: Int, password: String): Result<RoomSecretRoomResponse> = ...- suspend fun postParticipateSecreteRoom(@Path("roomId") roomId: Int, @Body request: RoomSecreteRoomRequest): BaseResponse<RoomSecreteRoomResponse>
+ suspend fun postParticipateSecretRoom(@Path("roomId") roomId: Int, @Body request: RoomSecretRoomRequest): BaseResponse<RoomSecretRoomResponse>추가 작업: 해당 클래스 파일명도 변경(Roo mSecreteRoomResponse.kt → RoomSecretRoomResponse.kt, RoomSecreteRoomRequest.kt → RoomSecretRoomRequest.kt)하고 모든 import/참조(메서드명·호출부 포함)를 일괄 업데이트하세요. 변경 후 잔여 사용처 확인: rg -n --no-heading "Secrete"
📝 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.
| data class RoomSecreteRoomResponse( | |
| @SerialName("matched") val matched: Boolean = false, | |
| @SerialName("roomId") val roomId: Int = 0 | |
| ) | |
| data class RoomSecretRoomResponse( | |
| @SerialName("matched") val matched: Boolean = false, | |
| @SerialName("roomId") val roomId: Int = 0 | |
| ) |
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomSecreteRoomResponse.kt
around lines 8-11, the class and filename use the misspelled token "Secrete";
rename the data class to RoomSecretRoomResponse and the file to
RoomSecretRoomResponse.kt, then update all occurrences and imports (including
the paired request class RoomSecreteRoomRequest -> RoomSecretRoomRequest and its
file), rename any API/service/repository method identifiers (e.g.,
postParticipateSecreteRoom -> postParticipateSecretRoom) and update all call
sites listed (RoomsService.kt, RoomsRepository.kt, GroupRoomUnlockViewModel.kt),
then run a global search (rg -n --no-heading "Secrete") to confirm no remaining
references and adjust tests/build files if needed.
| // 이전 요청이 있다면 취소 | ||
| passwordCheckJob?.cancel() | ||
|
|
||
| // 이미 요청 중이면 중복 실행 방지 | ||
| if (_uiState.value.isLoading) return | ||
|
|
There was a problem hiding this comment.
이전 Job 취소 후 isLoading 가드로 인해 요청이 영구 차단될 수 있음
passwordCheckJob?.cancel() 직후에도 _uiState.value.isLoading는 true일 수 있어, 바로 아래 가드에 걸리면 새 요청이 실행되지 않습니다(로딩 상태가 해제되지 않은 채 정지).
가드를 제거하고, 새 요청 시작 시점에 상태를 명시적으로 초기화/설정하세요.
- // 이전 요청이 있다면 취소
- passwordCheckJob?.cancel()
-
- // 이미 요청 중이면 중복 실행 방지
- if (_uiState.value.isLoading) return
+ // 이전 요청이 있다면 취소
+ passwordCheckJob?.cancel()그리고 launch 내부 시작부에서 아래처럼 초기화:
- _uiState.update { it.copy(isLoading = true) }
+ _uiState.update { it.copy(isLoading = true, passwordMatched = null, errorMessage = null) }🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomUnlockViewModel.kt
around lines 31 to 36, remove the immediate `_uiState.value.isLoading` guard
(which can permanently block new requests after cancelling the previous Job) and
instead: keep cancelling the previous passwordCheckJob, then start a new
coroutine for the request and explicitly reset/initialize the UI state when
starting (e.g., clear error fields and set isLoading = true at the top of the
new launch), and ensure you set isLoading = false and handle
cancellation/failure at the end of the coroutine so subsequent requests are
allowed.
| roomsRepository.postParticipateSecreteRoom(roomId, password) | ||
| .onSuccess { response -> | ||
| // API 호출은 성공했고, 응답 바디의 matched 값으로 상태 업데이트 | ||
| _uiState.update { | ||
| it.copy( | ||
| isLoading = false, | ||
| passwordMatched = response.matched | ||
| ) | ||
| } | ||
| } | ||
| .onFailure { throwable -> | ||
| // API 호출 자체 실패 (네트워크 오류 등) | ||
| _uiState.update { | ||
| it.copy( | ||
| isLoading = false, | ||
| passwordMatched = false, // 실패 시 불일치로 간주 | ||
| errorMessage = throwable.message ?: "알 수 없는 오류가 발생했습니다." | ||
| ) | ||
| } | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
성공/실패 상태 업데이트 보강: null 응답, 에러 메시지 정리, 실패 시 불일치로 오인 방지
- 성공 시 이전 에러 메시지가 남아있을 수 있으므로 반드시 초기화하세요.
- 응답이 null일 경우 NPE 가능성 또는 의미 모호성 있습니다.
- 실패를 곧바로
passwordMatched = false로 설정하면 네트워크 오류가 비밀번호 불일치로 오인될 수 있습니다.
아래처럼 수정 제안:
- roomsRepository.postParticipateSecreteRoom(roomId, password)
- .onSuccess { response ->
- // API 호출은 성공했고, 응답 바디의 matched 값으로 상태 업데이트
- _uiState.update {
- it.copy(
- isLoading = false,
- passwordMatched = response.matched
- )
- }
- }
- .onFailure { throwable ->
- // API 호출 자체 실패 (네트워크 오류 등)
- _uiState.update {
- it.copy(
- isLoading = false,
- passwordMatched = false, // 실패 시 불일치로 간주
- errorMessage = throwable.message ?: "알 수 없는 오류가 발생했습니다."
- )
- }
- }
+ roomsRepository.postParticipateSecreteRoom(roomId, password)
+ .onSuccess { response ->
+ _uiState.update {
+ it.copy(
+ isLoading = false,
+ passwordMatched = response?.matched == true,
+ errorMessage = null
+ )
+ }
+ }
+ .onFailure { throwable ->
+ _uiState.update {
+ it.copy(
+ isLoading = false,
+ passwordMatched = null, // 실패는 '불일치'와 구분
+ errorMessage = throwable.message ?: "알 수 없는 오류가 발생했습니다."
+ )
+ }
+ }추가로, postParticipateSecreteRoom 네이밍의 오탈자(Secrete→Secret)가 있다면 일관성 있는 네이밍으로 정리하는 것이 좋습니다.
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== repo root: $(pwd) ==="
echo
echo ">> locating GroupRoomUnlockViewModel.kt"
rg --hidden --line-number --no-ignore-vcs "GroupRoomUnlockViewModel.kt" || true
echo
echo ">> show file (if exists) with context"
if [ -f "app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomUnlockViewModel.kt" ]; then
sed -n '1,240p' app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomUnlockViewModel.kt
else
echo "File not found at expected path; searching repo for occurrences of postParticipateSecreteRoom..."
rg --hidden --line-number --no-ignore-vcs "postParticipateSecreteRoom|postParticipateSecret|postParticipateSecrete|Secrete|Secret" || true
fi
echo
echo ">> grep for repository method usages"
rg --hidden --line-number --no-ignore-vcs "postParticipateSecreteRoom|postParticipateSecretRoom" || trueLength of output: 4000
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== show RoomsService.kt ==="
sed -n '1,240p' app/src/main/java/com/texthip/thip/data/service/RoomsService.kt || true
echo
echo "=== show RoomsRepository.kt ==="
sed -n '1,240p' app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt || true
echo
echo "=== search for Secrete/SecreteRoom/SecreteRoomResponse types ==="
rg --hidden --line-number --no-ignore-vcs "Secrete|SecreteRoom|SecreteRoomResponse|RoomSecrete" || true
echo
echo "=== search for Secret (to compare) ==="
rg --hidden --line-number --no-ignore-vcs "Secret" || trueLength of output: 19595
비밀번호 검증 상태 업데이트 보강 필요
- 요약: 성공 시 이전 에러 메시지 초기화 필요, 실패를 passwordMatched = false로 두면 네트워크/서버 오류가 비밀번호 불일치로 오인될 수 있으므로 실패는 null로 구분하세요.
수정 대상:
- app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomUnlockViewModel.kt — checkPassword() 내 roomsRepository.postParticipateSecreteRoom 호출부 (현재 약 40–59행)
제안된 변경(diff):
- roomsRepository.postParticipateSecreteRoom(roomId, password)
- .onSuccess { response ->
- // API 호출은 성공했고, 응답 바디의 matched 값으로 상태 업데이트
- _uiState.update {
- it.copy(
- isLoading = false,
- passwordMatched = response.matched
- )
- }
- }
- .onFailure { throwable ->
- // API 호출 자체 실패 (네트워크 오류 등)
- _uiState.update {
- it.copy(
- isLoading = false,
- passwordMatched = false, // 실패 시 불일치로 간주
- errorMessage = throwable.message ?: "알 수 없는 오류가 발생했습니다."
- )
- }
- }
+ roomsRepository.postParticipateSecreteRoom(roomId, password)
+ .onSuccess { response ->
+ _uiState.update {
+ it.copy(
+ isLoading = false,
+ passwordMatched = response.matched,
+ errorMessage = null
+ )
+ }
+ }
+ .onFailure { throwable ->
+ _uiState.update {
+ it.copy(
+ isLoading = false,
+ passwordMatched = null, // 실패는 '불일치'와 구분
+ errorMessage = throwable.message ?: "알 수 없는 오류가 발생했습니다."
+ )
+ }
+ }추가 권고: 현재 코드베이스 전반에 "Secrete" 네이밍(RoomSecreteRoomRequest/Response, RoomsService.postParticipateSecreteRoom, RoomsRepository.postParticipateSecreteRoom 등)이 일관되게 사용되고 있어 네이밍 교정은 범위가 큽니다. 별도 리팩터(참조 일괄 교체 및 컴파일/테스트 확인)를 권장합니다.
📝 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.
| roomsRepository.postParticipateSecreteRoom(roomId, password) | |
| .onSuccess { response -> | |
| // API 호출은 성공했고, 응답 바디의 matched 값으로 상태 업데이트 | |
| _uiState.update { | |
| it.copy( | |
| isLoading = false, | |
| passwordMatched = response.matched | |
| ) | |
| } | |
| } | |
| .onFailure { throwable -> | |
| // API 호출 자체 실패 (네트워크 오류 등) | |
| _uiState.update { | |
| it.copy( | |
| isLoading = false, | |
| passwordMatched = false, // 실패 시 불일치로 간주 | |
| errorMessage = throwable.message ?: "알 수 없는 오류가 발생했습니다." | |
| ) | |
| } | |
| } | |
| roomsRepository.postParticipateSecreteRoom(roomId, password) | |
| .onSuccess { response -> | |
| _uiState.update { | |
| it.copy( | |
| isLoading = false, | |
| passwordMatched = response.matched, | |
| errorMessage = null | |
| ) | |
| } | |
| } | |
| .onFailure { throwable -> | |
| _uiState.update { | |
| it.copy( | |
| isLoading = false, | |
| passwordMatched = null, // 실패는 '불일치'와 구분 | |
| errorMessage = throwable.message ?: "알 수 없는 오류가 발생했습니다." | |
| ) | |
| } | |
| } |
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomUnlockViewModel.kt
around lines 40–59, the success branch should clear any prior error message and
the failure branch must not conflate network/server errors with a password
mismatch; make passwordMatched nullable (Boolean?) and on success set isLoading
= false, passwordMatched = response.matched, and errorMessage = null; on failure
set isLoading = false, passwordMatched = null, and errorMessage =
throwable.message ?: "알 수 없는 오류가 발생했습니다."; update the UI state type where needed
to accept a nullable passwordMatched.
| // Map 기반 빠른 최근 검색어 관리 | ||
| private val recentSearchMap = mutableMapOf<String, RecentSearchItem>() |
There was a problem hiding this comment.
🛠️ Refactor suggestion
스레드 안전성 고려
recentSearchMap이 mutableMapOf()로 선언되어 있는데, 코루틴에서 접근하므로 스레드 안전성을 고려해야 합니다.
// Map 기반 빠른 최근 검색어 관리
- private val recentSearchMap = mutableMapOf<String, RecentSearchItem>()
+ private val recentSearchMap = Collections.synchronizedMap(mutableMapOf<String, RecentSearchItem>())또는 ConcurrentHashMap 사용:
+import java.util.concurrent.ConcurrentHashMap
// Map 기반 빠른 최근 검색어 관리
- private val recentSearchMap = mutableMapOf<String, RecentSearchItem>()
+ private val recentSearchMap = ConcurrentHashMap<String, RecentSearchItem>()📝 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.
| // Map 기반 빠른 최근 검색어 관리 | |
| private val recentSearchMap = mutableMapOf<String, RecentSearchItem>() | |
| import java.util.concurrent.ConcurrentHashMap | |
| // Map 기반 빠른 최근 검색어 관리 | |
| private val recentSearchMap = ConcurrentHashMap<String, RecentSearchItem>() |
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/ui/group/search/viewmodel/GroupSearchViewModel.kt
around lines 29-30, recentSearchMap is a non-thread-safe mutableMapOf accessed
from coroutines; replace it with a thread-safe structure (e.g., instantiate it
as ConcurrentHashMap<String, RecentSearchItem> or use
Collections.synchronizedMap) or keep the current map but protect all accesses
with a Coroutine Mutex (with lock/unlock or withLock) so concurrent coroutine
reads/writes are serialized; update imports and all places that modify/read
recentSearchMap accordingly.
| error = throwable.message ?: "최근 검색어를 불러오는 중 오류가 발생했습니다." | ||
| ) |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
하드코딩된 에러 메시지를 리소스로 이동
에러 메시지가 하드코딩되어 있습니다. 다른 ViewModel들처럼 StringResourceProvider를 사용하는 것이 일관성 있겠습니다.
🏁 Script executed:
#!/bin/bash
# Description: StringResourceProvider가 GroupSearchViewModel에서도 사용 가능한지 확인
# StringResourceProvider 클래스 찾기
fd -e kt "StringResourceProvider.kt" --exec cat {}
# 다른 ViewModel에서 StringResourceProvider 사용 패턴 확인
rg "StringResourceProvider" --type kotlin -A 3 -B 3 app/src/main/java/com/texthip/thip/ui/Length of output: 8025
하드코딩된 에러 메시지를 StringResourceProvider로 이동해주세요
검증 결과: StringResourceProvider는 app/src/main/java/com/texthip/thip/data/provider/StringResourceProvider.kt에 존재하며, 다른 ViewModel들(SearchBookGroupViewModel, BookDetailViewModel, GroupRoomRecruitViewModel, GroupMakeRoomViewModel 등)에서 주입해 사용하고 있습니다. 해당 메시지도 동일한 패턴으로 리팩토링하세요.
수정 요약:
- 파일: app/src/main/java/com/texthip/thip/ui/group/search/viewmodel/GroupSearchViewModel.kt
- 생성자에 StringResourceProvider 주입 추가
- 하드코딩된 메시지 대체: stringResourceProvider.getString(R.string.error_load_recent_searches)
- 리소스: res/values/strings.xml에 문자열 리소스 추가
권장 변경(예시 diff):
@@
-@HiltViewModel
-class GroupSearchViewModel @Inject constructor(
- private val repository: GroupsRepository
-) : ViewModel() {
+@HiltViewModel
+class GroupSearchViewModel @Inject constructor(
+ private val repository: GroupsRepository,
+ private val stringResourceProvider: StringResourceProvider
+) : ViewModel() {
@@
- error = throwable.message ?: "최근 검색어를 불러오는 중 오류가 발생했습니다."
+ error = throwable.message
+ ?: stringResourceProvider.getString(R.string.error_load_recent_searches)strings.xml 추가(예시):
<string name="error_load_recent_searches">최근 검색어를 불러오는 중 오류가 발생했습니다.</string>파일 경로 요약:
- 수정 필요: app/src/main/java/com/texthip/thip/ui/group/search/viewmodel/GroupSearchViewModel.kt
- 새/수정 리소스: res/values/strings.xml
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/ui/group/search/viewmodel/GroupSearchViewModel.kt
around lines 75-76, replace the hardcoded error message with a
StringResourceProvider: add a StringResourceProvider parameter to the ViewModel
constructor (and propagate DI where this ViewModel is created), replace the
hardcoded string with
stringResourceProvider.getString(R.string.error_load_recent_searches), and add
the string resource <string name="error_load_recent_searches">최근 검색어를 불러오는 중 오류가
발생했습니다.</string> to res/values/strings.xml.
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (2)
app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt (2)
58-63: 에러를 사용자에게 노출하지 않고 즉시 소거함에러 메시지를 표시하지 않고
clearError()만 호출하여 사용자가 문제를 인지할 수 없습니다. 과거 커밋에 대한 동일 지적과 본질적으로 같은 문제입니다.토스트로 최소 피드백을 제공하는 간단한 수정 예시:
- LaunchedEffect(uiState.errorMessage) { - uiState.errorMessage?.let { message -> - viewModel.clearError() - } - } + LaunchedEffect(uiState.errorMessage) { + uiState.errorMessage?.let { message -> + android.widget.Toast + .makeText(androidx.compose.ui.platform.LocalContext.current, message, android.widget.Toast.LENGTH_SHORT) + .show() + viewModel.clearError() + } + }참고: 위 변경을 적용하려면 다음 import가 필요합니다(파일 상단).
import android.widget.Toastimport androidx.compose.ui.platform.LocalContextSnackbar 기반 UX가 필요하시면 Scaffold + SnackbarHostState 템플릿도 제공 가능합니다.
68-75: createGroup onError가 no-op — 실패 피드백 연결 필요실패 콜백이 비어 있어 오류 시 사용자 피드백이 없습니다. 최소한 토스트/스낵바 노출을 권장합니다.
간단한 수정 예시:
- viewModel.createGroup( + viewModel.createGroup( onSuccess = { roomId -> onGroupCreated(roomId) }, - onError = { } + onError = { message -> + android.widget.Toast + .makeText(androidx.compose.ui.platform.LocalContext.current, message, android.widget.Toast.LENGTH_SHORT) + .show() + } )중복 노출을 피하려면 위 onError로만 노출하고 LaunchedEffect에서는 자동 소거만 수행하거나(표시 제외), 반대로 onError에서 상태만 업데이트하고 LaunchedEffect에서 단일 채널(Snackbar/Toast)로 일괄 노출하도록 일원화하세요.
🧹 Nitpick comments (9)
app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomSecretRoomRequest.kt (1)
8-10: 비공개방 비밀번호 요청 모델 추가는 적절 — 입력 검증/보안 주석 보완 제안요청 모델 자체는 간결하고 직관적입니다. 다만 비밀번호 특성상 로깅 금지 및 최소 요건(예: 숫자 4자리)을 명시하는 KDoc 주석을 추가하면 유지보수성이 좋아집니다. 또한 서버 스키마가 엄격하다면 유효성 검증은 Repository 계층에서 선제적으로 수행하는 것을 권장합니다.
app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomSecretRoomResponse.kt (1)
9-10: 기본값 제거로 역직렬화 이슈를 조기 탐지현재 기본값(Boolean=false, Int=0)이 설정되어 있어, 서버 응답의 필드 누락/타입 변경이 런타임에서 조용히 숨겨질 수 있습니다. 기본값을 제거하여 불일치를 조기에 드러내는 편이 안전합니다.
아래처럼 기본값을 제거하는 것을 권장합니다:
data class RoomSecretRoomResponse( - @SerialName("matched") val matched: Boolean = false, - @SerialName("roomId") val roomId: Int = 0 + @SerialName("matched") val matched: Boolean, + @SerialName("roomId") val roomId: Int )app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt (2)
87-96: 문자열 기반 type 파라미터를 타입 안전하게 개선
joinOrCancelRoom(roomId, type: String)은 오타/미지정 값 위험이 있습니다. 서버 스키마가 제한적이면 enum/sealed class로 한정하는 것이 안전합니다.예시:
enum class RoomJoinType(val value: String) { JOIN("join"), CANCEL("cancel"); } // 사용 suspend fun joinOrCancelRoom(roomId: Int, type: RoomJoinType): Result<String> = runCatching { val request = RoomJoinRequest(type = type.value) // ... }
97-106: 비밀번호 입력 API 호출 전 간단한 입력 검증 추가 제안UI에서 4자리 숫자 제한을 두었더라도, Repository에서 최소 길이 체크를 한 번 더 하면 불필요한 네트워크 호출을 줄일 수 있습니다(특히 외부 호출자가 직접 Repository를 사용하는 경우).
간단한 가드 추가:
suspend fun postParticipateSecretRoom(roomId: Int, password: String): Result<RoomSecretRoomResponse> = runCatching { - val request = RoomSecretRoomRequest(password = password) + require(password.length >= 4) { "비밀번호는 최소 4자리여야 합니다." } + val request = RoomSecretRoomRequest(password = password) val response = roomsService.postParticipateSecretRoom(roomId, request) .handleBaseResponse() .getOrThrow() ?: throw NoSuchElementException("비밀번호 입력 응답을 받을 수 없습니다.") response }app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomUnlockScreen.kt (5)
80-80: 초기화 이펙트는 roomId 변경에도 반응하도록 키 변경화면이 재사용되며 roomId가 바뀌는 경우를 대비해 Unit 대신 roomId를 키로 사용하세요.
적용 diff:
- LaunchedEffect(Unit) { + LaunchedEffect(roomId) {
131-141: 입력 중 로딩/에러 상태에서는 입력 무시요청 중 혹은 에러 표시 중에는 입력을 무시하여 불필요한 상태 변화를 줄이는 것이 안전합니다.
적용 diff:
- onValueChange = { input -> - if (input.length <= 1 && input.all { it.isDigit() }) { + onValueChange = { input -> + if (uiState.isLoading || showError) return@onValueChange + if (input.length <= 1 && input.all { it.isDigit() }) {
166-173: 서버 에러 메시지가 있으면 우선 노출서버에서 내려오는 구체적 오류 메시지가 있다면 기본 문자열보다 이를 우선 노출하는 것이 UX에 유리합니다.
적용 diff:
- Text( - text = stringResource(R.string.group_secret_screen_error_comment), + Text( + text = uiState.errorMessage ?: stringResource(R.string.group_secret_screen_error_comment),확인 요청: GroupRoomUnlockUiState에 errorMessage(String?)가 존재하는지 확인 바랍니다. 존재하지 않으면 해당 필드 추가가 필요합니다.
43-47: Preview가 Hilt ViewModel 기본값 때문에 깨질 수 있음@Preview에서 hiltViewModel() 호출은 실패합니다. 프리뷰 전용 Content 컴포저블로 상태를 분리해 해결하는 것을 권장합니다.
제안:
- 화면을 Stateful/Stateless로 분리: Screen은 VM 주입 및 상태 수집만 담당, Content는 UI만 담당.
- Preview는 Content를 사용해 목 상태를 주입.
예시(추가 코드):
@Composable private fun GroupRoomUnlockContent( uiState: GroupRoomUnlockUiState, password: Array<String>, showError: Boolean, focusRequesters: List<FocusRequester>, onValueChange: (index: Int, input: String) -> Unit, onBackspace: (index: Int) -> Unit, onBackClick: () -> Unit, ) { // 현재 Column/Row/Text/SingleDigitBox UI 부분을 이 함수로 옮깁니다. } @Preview(showBackground = true) @Composable fun GroupRoomUnlockScreenPreview() { ThipTheme { val fakeUiState = GroupRoomUnlockUiState(isLoading = false, passwordMatched = null, errorMessage = null) GroupRoomUnlockContent( uiState = fakeUiState, password = arrayOf("", "", "", ""), showError = false, focusRequesters = List(4) { FocusRequester() }, onValueChange = { _, _ -> }, onBackspace = { _ -> }, onBackClick = {}, ) } }원하시면 위 분리 작업에 맞춘 정확한 diff를 생성해 드리겠습니다.
Also applies to: 185-190
49-50: 입력/에러 상태의 구성 변경에 대비한 상태 보존회전 등 구성 변경 시 입력값 유지가 필요하면 rememberSaveable 사용을 고려하세요. 배열은 String 배열이므로 저장 가능합니다.
적용 diff:
- var password by remember { mutableStateOf(arrayOf("", "", "", "")) } - var showError by remember { mutableStateOf(false) } + var password by rememberSaveable { mutableStateOf(arrayOf("", "", "", "")) } + var showError by rememberSaveable { mutableStateOf(false) }참고: 민감하지 않은 4자리 입력이며, 보안 요구사항에 따라 회전 시 초기화를 원하시면 현재 구현을 유지하세요.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomSecretRoomRequest.kt(1 hunks)app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomSecretRoomResponse.kt(1 hunks)app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt(1 hunks)app/src/main/java/com/texthip/thip/data/service/RoomsService.kt(2 hunks)app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt(6 hunks)app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomUnlockScreen.kt(5 hunks)app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomUnlockViewModel.kt(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- app/src/main/java/com/texthip/thip/data/service/RoomsService.kt
🚧 Files skipped from review as they are similar to previous changes (1)
- app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomUnlockViewModel.kt
🔇 Additional comments (6)
app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt (2)
52-52: roomId를 콜백으로 전달하는 설계 👍생성 성공 시 상위 네비게이션으로 roomId를 전달하는 형태는 화면 전환/딥링크 연계를 간결하게 합니다.
256-260: 도서 검색 바텀시트 prop 확장 연계 OKsearchResults/isLoading/isSearching/onSearch 연결이 자연스럽습니다. UI 상태 흐름과 일관됩니다.
app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt (4)
55-63: 장르 → API 카테고리 매핑 처리 적절선택/기본 장르를 통해 카테고리 엔드포인트를 조회하는 흐름이 명확합니다. 예외/기본값 처리도 괜찮습니다.
108-115: 모집 마감 API 연동 흐름 명확공통 패턴(handleBaseResponse→getOrThrow→null 검사)이 일관적입니다.
43-52: 닉네임 캐시 갱신 — 응답 스키마 검증 완료JoinedRoomListResponse의 최상위 필드로 nickname이 정의되어 있어 현재 userDataManager.cacheUserName 호출은 올바릅니다.
- 확인 파일: app/src/main/java/com/texthip/thip/data/model/rooms/response/JoinedRoomListResponse.kt — @SerialName("nickname") val nickname: String (라인 ~10)
변경 불필요.
31-41: 확인 완료 — UserDataManager.getUserName은 동기(메모리 캐시) 접근입니다getUserName은 suspend가 아니며 cachedUserName ?: "사용자"를 반환하므로 I/O/블로킹 위험이 없습니다. Repository로 노출한 작업은 적절합니다.
- app/src/main/java/com/texthip/thip/data/manager/UserDataManager.kt — fun getUserName(): String { return cachedUserName ?: "사용자" } (동기, 메모리 캐시)
- app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt — fun getUserName(): Result = runCatching { userDataManager.getUserName() } (메인스레드에서 호출해도 안전)
| import androidx.compose.runtime.Composable | ||
| import androidx.compose.runtime.DisposableEffect | ||
| import androidx.compose.runtime.LaunchedEffect | ||
| import androidx.compose.runtime.collectAsState |
There was a problem hiding this comment.
🛠️ Refactor suggestion
수명 주기 인지형 수집으로 변경 권장 (collectAsStateWithLifecycle)
현재 Flow 수집이 lifecycle 비인지형이라 화면이 비가시/비활성 상태에서도 수집이 지속될 수 있습니다. Lifecycle-aware로 교체해 불필요한 수집 및 잠재적 리소스 낭비를 방지하세요.
적용 diff:
- 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
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomUnlockScreen.kt
around lines 16 and 48, replace the non-lifecycle-aware Flow collection import
and usage with the lifecycle-aware API: change the import
androidx.compose.runtime.collectAsState to
androidx.lifecycle.compose.collectAsStateWithLifecycle (or add the
lifecycle-compose dependency if missing) and replace any .collectAsState(...)
calls at/around line 48 with .collectAsStateWithLifecycle(...) so the Flow is
collected with the component lifecycle.
app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomUnlockScreen.kt
Show resolved
Hide resolved
app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomUnlockScreen.kt
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (5)
app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomUnlockScreen.kt (3)
16-16: 수명 주기 인지형 수집으로 교체 권장 (collectAsStateWithLifecycle) — 이전 코멘트 재확인현재 Flow 수집이 lifecycle 비인지형입니다. 화면 비가시/비활성 상태에서도 수집될 수 있으므로 lifecycle-aware API로 교체하는 것이 안전합니다.
아래 diff 적용을 권장합니다.
- import androidx.compose.runtime.collectAsState + import androidx.lifecycle.compose.collectAsStateWithLifecycle- val uiState by viewModel.uiState.collectAsState() + val uiState by viewModel.uiState.collectAsStateWithLifecycle()추가로, 필요한 경우 lifecycle-runtime-compose 의존성이 설정되어 있는지 확인하세요 (예: androidx.lifecycle:lifecycle-runtime-compose).
Also applies to: 48-48
56-58: 중복/경합 호출 가드(L1) 잘 추가됨isLoading 가드를 통해 4자리 입력 완료 시 중복 API 호출이 방지됩니다. 이전 피드백을 적절히 반영하셨습니다.
61-68: 성공 시 키보드 숨김 + 이벤트 소진 처리 적절키보드를 즉시 숨기고 ViewModel 상태를 리셋한 뒤 내비게이션을 트리거하는 순서가 안전합니다. 재구성 타이밍에 따른 중복 내비게이션을 잘 방지합니다.
app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookSearchBottomSheet.kt (2)
53-60: Empty 상태 계산이 초기 진입 케이스를 누락합니다 — displayBooks 기반으로 통합하세요.검색어가 비어 있고 탭별 목록도 비어있는 초기 진입 상황에서 Empty UI가 표시되지 않습니다. displayBooks를 기준으로 빈 상태를 계산하면 모든 케이스(초기/검색 결과 없음)를 일관되게 처리할 수 있습니다. 과거 코멘트와 유사한 맥락입니다.
다음과 같이 변수명을 의미 있게 변경하고 조건을 통합하는 것을 권장합니다.
- val showNoSearchResultsError = searchText.isNotEmpty() && displayBooks.isEmpty() && !isSearching + // 검색어 유무와 관계없이 표시 대상이 비어있고, 로딩/검색 중이 아닐 때 Empty 노출 + val showEmptyContent = displayBooks.isEmpty() && !isSearching && !isLoading그리고 분기에서도 동일 변수명을 사용하세요:
- if (showNoSearchResultsError) { + if (showEmptyContent) { EmptyBookSheetContent(onRequestBook) } else {Also applies to: 83-86
98-107: 검색 중(isSearching)에도 로딩 스피너를 노출하고, 높이를 리스트 영역(320.dp)과 일치시키세요.현재는 isLoading일 때만 스피너가 보이고, 높이가 지정되지 않아 레이아웃 점프가 발생할 수 있습니다. 검색 디바운스 중에도 UI 피드백이 있도록 isSearching을 함께 고려하고, 리스트 영역과 동일 높이를 지정해 안정적인 레이아웃을 유지하는 것을 권장합니다. 이 중 높이 지정은 과거에 적용되었던 개선 사항이 재회귀로 보입니다.
- when { - isLoading -> { - Box( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.CenterHorizontally), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } - } + when { + isLoading || isSearching -> { + Box( + modifier = Modifier + .fillMaxWidth() + .height(320.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + }
🧹 Nitpick comments (6)
app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomUnlockScreen.kt (3)
43-44: roomId 기본값 0은 실서버 호출 리스크 — 기본값 제거 권장roomId에 기본값(0)이 있으면 호출부 누락 시 0번 룸으로 API가 호출될 수 있습니다. 네비게이션 파이프라인에서 필수 인자를 강제하는 편이 안전합니다.
아래와 같이 기본값을 제거하고, 호출부(프리뷰 포함)에서 명시적으로 전달하도록 변경을 권장합니다.
-fun GroupRoomUnlockScreen( - roomId: Int = 0, - viewModel: GroupRoomUnlockViewModel = hiltViewModel(), +fun GroupRoomUnlockScreen( + roomId: Int, + viewModel: GroupRoomUnlockViewModel = hiltViewModel(),또한 Preview 호출부도 함께 업데이트해야 합니다(아래 별도 코멘트에 diff 포함).
185-193: Preview에서 hiltViewModel() 기본값 사용 시 프리뷰 크래시 가능 — UI 전용 프리뷰로 분리 또는 스텁 주입 권장Preview는 Hilt/Nav 백스택 컨텍스트가 없어 hiltViewModel() 기본값으로 인해 종종 크래시합니다. 대응 옵션:
- 옵션 A(권장): UI-only 컴포저블로 분리(상태/콜백만 받는 stateless 컴포저블) 후 Preview는 해당 컴포저블을 직접 호출.
- 옵션 B: Preview에서 스텁 ViewModel을 명시적으로 주입.
- 옵션 C: LocalInspectionMode로 분기해 프리뷰일 때는 스텁을 사용.
최소 변경으로는 Preview에서 명시적으로 roomId와 스텁/가짜 VM을 넘기도록 합니다(스텁 구현은 별도 필요).
ThipTheme { GroupRoomUnlockScreen( - onBackClick = {}, - onSuccessNavigation = {} + roomId = 123, + onBackClick = {}, + onSuccessNavigation = {} ) }또는 UI-only 버전으로 분리하는 예시는 아래처럼 적용할 수 있습니다(참고용, 파일 외부 변경):
@Composable private fun GroupRoomUnlockContent( uiState: GroupRoomUnlockUiState, password: Array<String>, showError: Boolean, /* ... 필요한 콜백들 ... */ ) { // 현재 Column/Row/Text/SingleDigitBox 부분을 이쪽으로 이동 } // 실제 화면 @Composable fun GroupRoomUnlockScreen( roomId: Int, viewModel: GroupRoomUnlockViewModel = hiltViewModel(), onBackClick: () -> Unit = {}, onSuccessNavigation: () -> Unit = {} ) { // 기존 로직 유지하고 마지막에 GroupRoomUnlockContent(...) 호출 } // Preview에서는 더미 상태로 Content만 호출 @Preview @Composable fun GroupRoomUnlockScreenPreview() { ThipTheme { GroupRoomUnlockContent( uiState = GroupRoomUnlockUiState(/* 더미 값 */), password = arrayOf("", "", "", ""), showError = false, /* ... */ ) } }
54-59: 조건문 단순화(Nit): password.all 체크는 중복 가능성onValueChange에서 이미 한 자리 숫자만 허용하므로 아래 조건은 간소화할 수 있습니다.
- if (!uiState.isLoading && fullPassword.length == 4 && password.all { it.length == 1 }) { + if (!uiState.isLoading && fullPassword.length == 4) { viewModel.checkPassword(roomId, fullPassword) }가독성 측면의 제안이며 필수는 아닙니다.
app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookSearchBottomSheet.kt (1)
71-79: 검색 트리거 중복 가능성(onValueChange와 onSearch 모두에서 onSearch 호출).현재 타이핑 시 매 입력마다 onSearch가 호출되고(디바운싱 처리됨), 동시에 아이콘 클릭/IME Search에서도 동일 쿼리로 onSearch가 한 번 더 호출될 수 있습니다. UX상 문제는 없지만 불필요한 이중 트리거를 줄이려면 아래처럼 onSearch를 비워 중복 호출을 방지하는 방법을 고려해보세요.
- onValueChange = { - searchText = it - onSearch(it) - }, - onSearch = { onSearch(searchText) }, + onValueChange = { + searchText = it + onSearch(it) // 디바운싱되어 실시간 검색 트리거 + }, + onSearch = {}, // 중복 트리거 방지app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt (2)
37-41: 매직 스트링 제거: "SAVED"/"JOINING"을 상수로 승격하세요.하드코딩된 문자열은 오타·변경에 취약합니다. companion object에 상수로 정의하고 사용하면 안전합니다.
companion object { private val DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy.MM.dd") + private const val BOOKS_SAVED = "SAVED" + private const val BOOKS_JOINING = "JOINING" } ... - val savedBooksResult = bookRepository.getBooks("SAVED") + val savedBooksResult = bookRepository.getBooks(BOOKS_SAVED) ... - val groupBooksResult = bookRepository.getBooks("JOINING") + val groupBooksResult = bookRepository.getBooks(BOOKS_JOINING)Also applies to: 88-106
49-55: 장르 로딩 실패 케이스 미처리 — 최소한의 실패 처리 추가 권장.네트워크 실패 시 UI가 이전 상태를 유지해 원인 파악이 어렵습니다. 빈 리스트로 초기화하거나 에러 메시지를 기록해 두면 디버깅과 UX에 도움됩니다.
private fun loadGenres() { viewModelScope.launch { - roomsRepository.getGenres() - .onSuccess { genresList -> - updateState { it.copy(genres = genresList) } - } + roomsRepository.getGenres() + .onSuccess { genresList -> + updateState { it.copy(genres = genresList) } + } + .onFailure { + // 필요 시 errorMessage 설정도 고려 + updateState { it.copy(genres = emptyList()) } + } } }
📜 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 (5)
app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookSearchBottomSheet.kt(3 hunks)app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt(9 hunks)app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt(13 hunks)app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomUnlockScreen.kt(4 hunks)app/src/main/res/values/strings.xml(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- app/src/main/res/values/strings.xml
- app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt
🧰 Additional context used
🧬 Code Graph Analysis (2)
app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookSearchBottomSheet.kt (5)
app/src/main/java/com/texthip/thip/ui/common/bottomsheet/CustomBottomSheet.kt (1)
CustomBottomSheet(44-129)app/src/main/java/com/texthip/thip/ui/common/forms/SearchBookTextField.kt (1)
SearchBookTextField(38-112)app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupEmptyBookSheetContent.kt (1)
EmptyBookSheetContent(22-55)app/src/main/java/com/texthip/thip/ui/common/header/HeaderMenuBarTab.kt (1)
HeaderMenuBarTab(23-69)app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookListWithScrollbar.kt (1)
GroupBookListWithScrollbar(24-62)
app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt (4)
app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt (1)
updateState(28-30)app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt (1)
updateState(27-29)app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt (1)
updateState(26-28)app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt (1)
updateState(33-35)
🔇 Additional comments (7)
app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomUnlockScreen.kt (3)
81-85: initializeState는 멱등(idempotent) 특성 보장 필요화면 최초 진입에서만 실행되지만, 재진입/재생성 시에도 안전하도록 initializeState가 멱등적으로 동작하는지 확인이 필요합니다(예: 이전 실패 상태나 로딩 플래그가 의도치 않게 남지 않도록).
89-95: 화면 종료 시 키보드 숨김 처리 LGTMDisposableEffect로 onDispose에서 키보드를 숨기는 처리는 적절합니다. 화면 전환/백스택 이동 시 잔여 키보드 노출 문제가 방지됩니다.
145-151: Backspace UX 향상 LGTM현재 박스가 비어있을 때 이전 박스로 포커스를 이동하며 지우는 로직이 자연스러운 입력 경험을 제공합니다.
app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookSearchBottomSheet.kt (1)
86-96: 검색어 입력 시 탭 숨김 처리 로직 적절 — LGTM.검색 모드와 탭 전환이 충돌하지 않도록 명확히 분리되어 있습니다.
app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt (3)
133-175: 검색 디바운스/취소 로직 깨끗합니다 — LGTM.Job 취소로 최신 쿼리 우선 처리, 300ms 디바운스, 성공/실패/예외에서 isSearching false 정리까지 일관되어 있습니다.
215-267: 방 생성 플로우의 성공/실패 처리와 로딩 관리 적절 — LGTM.유효성 검사, 요청 생성, 로딩 상태 토글, 지역화된 에러 메시지 처리까지 깔끔합니다.
115-122: 검증 완료 — DTO 필드명과 ViewModel 매핑이 일치합니다.간단히 확인한 결과, 매핑은 DTO 스키마와 일치하여 런타임 필드 불일치는 없습니다.
- app/src/main/java/com/texthip/thip/data/model/book/response/BookListResponse.kt
- data class BookSavedResponse: bookTitle, authorName, bookImageUrl, isbn
- app/src/main/java/com/texthip/thip/data/model/book/response/BookSearchResponse.kt
- data class BookSearchItem: title, imageUrl, authorName, isbn
- app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt
- BookSavedResponse.toBookData() → bookTitle→title, bookImageUrl→imageUrl, authorName→author, isbn→isbn (lines ~115–122)
- BookSearchItem.toBookData() → title→title, imageUrl→imageUrl, authorName→author, isbn→isbn (lines ~124–131)
결론: 원래 코멘트에서 지적한 필드명 불일치는 없으므로 조치 불필요합니다.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (4)
app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt (4)
48-49: SavedStateHandle 키에 짧은 주석 추가 제안키 용도를 바로 파악할 수 있도록 간단한 주석을 덧붙이면 유지보수성이 좋아집니다.
아래와 같이 주석을 추가하는 것을 제안합니다.
+// RoomUnlock -> Recruit 간 '참여 승인' 신호 전달에 사용하는 SavedStateHandle 키 +// true가 설정되면 Recruit 화면에서 참여 로직을 트리거한 뒤 false로 되돌립니다. private const val PARTICIPATION_APPROVED_KEY = "participation_approved_key"
195-195: 일관된 스코프 보장: hiltViewModel(backStackEntry) 사용 권장중첩 그래프/다중 백스택 시 스코핑 명확성을 위해 backStackEntry를 명시적으로 넘기는 것을 권장합니다. 아래와 같이 변경하면 다른 곳(GroupNote 등)과도 스타일이 일치합니다.
- val viewModel: GroupRoomRecruitViewModel = hiltViewModel() + val viewModel: GroupRoomRecruitViewModel = hiltViewModel(backStackEntry)
197-199: 수명 주기 안전한 Flow 수집으로 교체 권장UI 재개 상태를 고려해
collectAsStateWithLifecycle()사용을 권장합니다. 이미 라이프사이클 확장이 import되어 있습니다.- val participationApproved by backStackEntry.savedStateHandle - .getStateFlow(PARTICIPATION_APPROVED_KEY, false) - .collectAsState() + val participationApproved by backStackEntry.savedStateHandle + .getStateFlow(PARTICIPATION_APPROVED_KEY, false) + .collectAsStateWithLifecycle()
220-228: 주석과 실제 동작 일치화 (가독성)코드는 이전 엔트리 존재 여부로 분기하지만, 주석은 "MakeRoom에서 바로 온 경우"라고 되어 있어 오독 소지가 있습니다. 주석을 실제 조건과 일치시키는 것을 제안합니다.
- // MakeRoom에서 바로 온 경우를 확인하여 Group 홈으로 이동 + // 이전 백스택 엔트리가 있으면 일반 뒤로 가기, 없으면 Group 홈으로 이동 val canGoBack = navController.previousBackStackEntry != null if (canGoBack) { navigateBack() } else { - // 백스택이 비어있으면 Group 홈으로 이동 (방금 생성된 방의 경우) + // 이전 엔트리가 없으면 Group 홈으로 이동 navController.popBackStack(MainTabRoutes.Group, false) }
📜 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 (2)
app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt(2 hunks)app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt(6 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt
🧰 Additional context used
🧬 Code Graph Analysis (1)
app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt (1)
app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomUnlockScreen.kt (1)
GroupRoomUnlockScreen(41-183)
🔇 Additional comments (4)
app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt (4)
26-27: 신규 import 사용 적절 (Unlock/Recruit/BookDetail 네비게이션 연동 OK)모두 실제 사용 라인과 일치하며 불필요한 import 없음.
Also applies to: 32-32, 41-41
103-106: MakeRoom → Recruit 전환 로직 구성 적절popUpTo<GroupRoutes.MakeRoom>(inclusive = true)로 MakeRoom을 백스택에서 정리하면서 Recruit로 전환하는 흐름이 올바르게 구현되었습니다.
229-237: 신규 네비게이션 훅 연결 적절 (도서 상세/비번 입력/참여중 화면)핵심 액션(도서 상세, 비밀번호 입력, 참여중 화면 이동) 진입점이 명확하게 분리되어 있고, 라우트 파라미터(roomId, isbn) 전달도 올바릅니다.
241-259: 비밀번호 성공 신호 전달 패턴 적절RoomUnlock → Recruit로 SavedStateHandle을 통한 원자적 신호 전달(true 설정 후 Recruit에서 소비 및 false 초기화) 흐름이 깔끔합니다. 이전 엔트리 null 안전 처리도 반영되어 있습니다.
| onGroupCreated = { roomId -> | ||
| // 생성된 방의 모집 화면으로 이동하고 백스택 제거 | ||
| navController.navigateToGroupRecruit(roomId) | ||
| // 백스택에서 MakeRoomWithBook 화면 제거 | ||
| navController.popBackStack<GroupRoutes.MakeRoomWithBook>(inclusive = true) | ||
| } |
There was a problem hiding this comment.
치명적: MakeRoomWithBook → Recruit 전환 시 popBackStack로 Recruit까지 함께 제거되는 버그
현재 구현은 Recruit로 이동한 직후 popBackStack<GroupRoutes.MakeRoomWithBook>(inclusive = true)를 호출하여 Recruit(방금 푸시된 화면)까지 함께 팝됩니다. 결과적으로 Recruit에 머물지 못하고 MakeRoomWithBook 이전 화면으로 돌아가게 됩니다.
navigate 시 popUpTo를 사용해 MakeRoomWithBook만 제거하도록 수정하세요.
아래 패치 적용을 권장합니다.
- onGroupCreated = { roomId ->
- // 생성된 방의 모집 화면으로 이동하고 백스택 제거
- navController.navigateToGroupRecruit(roomId)
- // 백스택에서 MakeRoomWithBook 화면 제거
- navController.popBackStack<GroupRoutes.MakeRoomWithBook>(inclusive = true)
- }
+ onGroupCreated = { roomId ->
+ navController.navigate(GroupRoutes.Recruit(roomId)) {
+ popUpTo<GroupRoutes.MakeRoomWithBook> { inclusive = true }
+ }
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| onGroupCreated = { roomId -> | |
| // 생성된 방의 모집 화면으로 이동하고 백스택 제거 | |
| navController.navigateToGroupRecruit(roomId) | |
| // 백스택에서 MakeRoomWithBook 화면 제거 | |
| navController.popBackStack<GroupRoutes.MakeRoomWithBook>(inclusive = true) | |
| } | |
| onGroupCreated = { roomId -> | |
| navController.navigate(GroupRoutes.Recruit(roomId)) { | |
| popUpTo<GroupRoutes.MakeRoomWithBook> { inclusive = true } | |
| } | |
| } |
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt
around lines 131 to 136, the current flow navigates to the Recruit screen then
calls popBackStack<GroupRoutes.MakeRoomWithBook>(inclusive = true), which also
pops the just-pushed Recruit screen; change this to navigate using
navController.navigateToGroupRecruit(roomId) with a popUpTo targeting
GroupRoutes.MakeRoomWithBook and inclusive = true (so only MakeRoomWithBook is
removed) and remove the subsequent popBackStack call so the Recruit screen
remains on top of the stack.
➕ 이슈 링크
🔎 작업 내용
📸 스크린샷
😢 해결하지 못한 과제
[] 아직 모임방 검색은 연결되어있지 않습니다
📢 리뷰어들에게
Summary by CodeRabbit
신기능
개선사항
로컬라이제이션