-
Notifications
You must be signed in to change notification settings - Fork 3
[REFACTOR] 모임방 생성화면 책 선택 수정, Dialog 팝업 구현 #151
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Conversation
Walkthrough모임 생성 플로우에서 그룹 책 탭을 제거하고 확인 대화상자 흐름을 도입했으며, 날짜 범위 검증·디바운스, 스크롤바(LazyListState) 오버로드 추가, 관련 뷰모델·UI 상태 정리 및 문자열 리소스가 추가되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant Screen as GroupMakeRoomScreen
participant VM as GroupMakeRoomViewModel
participant Dialog as DialogPopup
participant API
User->>Screen: 모임 생성 버튼 클릭
Screen->>VM: onToggleConfirmDialog(true)
VM->>VM: showConfirmDialog = true
Screen->>Dialog: 다이얼로그 표시 (confirm/cancel texts)
alt 사용자가 확인
User->>Dialog: confirm
Dialog->>Screen: onConfirm
Screen->>VM: onToggleConfirmDialog(false)
Screen->>VM: onCreateGroup()
VM->>API: 모임 생성 요청
API-->>VM: 응답
else 사용자가 취소
User->>Dialog: cancel
Dialog->>Screen: onCancel
Screen->>VM: onToggleConfirmDialog(false)
end
sequenceDiagram
actor User
participant Picker as GroupRoomDurationPicker
participant Utils as WheelPickerUtils
participant VM as GroupMakeRoomViewModel
User->>Picker: 시작/종료일 선택
Picker->>Picker: isPickerTouched = true
note right of Picker: 100ms 디바운스 시작
loop debounce delay
end
Picker->>Utils: validateDateRange(start, end, min, max)
Utils-->>Picker: (validatedStart, validatedEnd)
Picker->>Picker: recruitmentDays 계산
Picker->>VM: onDateRangeSelected(validatedStart, validatedEnd)
Picker->>Picker: 모집기간 UI 표시
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro Disabled knowledge base sources:
📒 Files selected for processing (8)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (3)
🧰 Additional context used🧠 Learnings (1)📚 Learning: 2025-08-13T05:42:13.580ZApplied to files:
🧬 Code graph analysis (1)app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt (1)
🔇 Additional comments (2)
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. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 12
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
app/src/main/java/com/texthip/thip/utils/auth/AuthInterceptor.kt (1)
36-41: OkHttp 인터셉터에서 runBlocking 사용은 위험합니다.OkHttp 인터셉터는 OkHttp의 스레드 풀에서 실행되는데,
runBlocking을 사용하면 해당 스레드가 블로킹되어 성능 저하 및 스레드 풀 고갈 위험이 있습니다. 특히 다수의 요청이 동시에 인증 오류를 반환하는 경우 심각한 병목이 발생할 수 있습니다.다음 중 하나의 방법으로 개선하는 것을 권장합니다:
방법 1: TokenManager에 동기 메서드 추가 (권장)
TokenManager에 동기 버전의 메서드를 추가:
// TokenManager에 추가 fun clearTokensSync() { // 동기적으로 토큰 클리어 }if (shouldClearAuth) { - runBlocking { - tokenManager.clearTokens() - authStateManager.triggerTokenExpired() - } + tokenManager.clearTokensSync() + authStateManager.triggerTokenExpiredSync() }방법 2: GlobalScope 사용 (fire-and-forget 방식)
if (shouldClearAuth) { - runBlocking { + GlobalScope.launch(Dispatchers.IO) { tokenManager.clearTokens() authStateManager.triggerTokenExpired() } }참고: 이 문제는 기존의 lines 21-22에도 동일하게 적용됩니다.
app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomUiState.kt (1)
36-41: 90일 기준 불일치 확인됨 — 상수화를 통한 일관성 통일 필수검증 결과, 다음 두 위치에서 모임 기간 상한선이 일치하지 않음을 확인했습니다.
- GroupMakeRoomUiState.kt:39:
daysBetween in 1..90(최대 90일 허용)- GroupRoomDurationPicker.kt:96:
isOverLimit = daysBetween > 91(91일 초과시만 에러)91일 입력 시 발생하는 문제:
- UI 검증: 91 > 91 = false → 에러 미표시, 입력 진행 가능
- 제출 검증: 91 in 1..90 = false → 제출 실패
권장 수정 사항:
GroupRoomConstraints.kt신규 파일 생성 (제안된 경로 동일)GroupMakeRoomUiState.kt에서 상수 참조로 변경GroupRoomDurationPicker.kt의isOverLimit조건을daysBetween > GroupRoomConstraints.MAX_DURATION_DAYS또는daysBetween >= 91로 수정하여 검증 로직 통일
🧹 Nitpick comments (11)
app/src/main/java/com/texthip/thip/ui/common/modal/ScrollbarUtil.kt (2)
61-61: 고정된 스크롤바 높이가 컨텐츠 비율을 반영하지 않습니다.
scrollbarHeight = size.height / 8f로 고정되어 있어, 실제 컨텐츠 양과 뷰포트 크기의 비율을 반영하지 못합니다. 일반적인 스크롤바 구현에서는 썸 높이가(뷰포트 높이 / 전체 컨텐츠 높이) * 뷰포트 높이로 계산됩니다.다음과 같이 개선할 수 있습니다:
- val scrollbarHeight = size.height / 8f + val thumbHeightRatio = (viewportHeight / estimatedTotalHeight.toFloat()).coerceIn(0.1f, 1f) + val scrollbarHeight = size.height * thumbHeightRatio
84-97: 두 오버로드 간 스크롤바 그리기 로직이 중복됩니다.
ScrollState와LazyListState오버로드 모두 동일한drawRoundRect호출을 사용하여 트랙과 썸을 그립니다. 이 로직을 별도의 private 함수로 추출하면 코드 중복을 제거하고 유지보수성이 향상됩니다.다음과 같이 리팩토링할 수 있습니다:
private fun DrawScope.drawScrollbar( trackThickness: Dp, thumbThickness: Dp, trackColor: Color, thumbColor: Color, scrollbarOffsetY: Float, scrollbarHeight: Float ) { // 전체 고정 바 drawRoundRect( color = trackColor, topLeft = Offset(x = size.width - trackThickness.toPx(), y = 0f), size = Size(trackThickness.toPx(), size.height), cornerRadius = CornerRadius(trackThickness.toPx() / 2) ) // 핸들 바 drawRoundRect( color = thumbColor, topLeft = Offset(x = size.width - thumbThickness.toPx(), y = scrollbarOffsetY), size = Size(thumbThickness.toPx(), scrollbarHeight), cornerRadius = CornerRadius(thumbThickness.toPx() / 2) ) }그런 다음 두 오버로드에서 이 함수를 호출하면 됩니다.
app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupDatePicker.kt (1)
114-114: 방어적 코딩이 적용되었으나 실질적으로는 불필요합니다.
days.maxOrNull() ?: 1을 사용하여 null 안전성을 추가했습니다.days리스트는(1..lengthOfMonth()).toList()로 생성되므로 항상 최소 1개 이상의 요소를 포함하여maxOrNull()이 null을 반환할 수 없습니다.현재 코드도 문제없지만, 더 명확하게 하려면
days.max()를 그대로 사용하거나, 주석으로 방어적 코딩 의도를 명시하는 것을 고려할 수 있습니다.app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomUiState.kt (1)
45-47: 비밀번호 형식 검증 보완 제안(숫자 4자리 제한)현재 길이(4)만 확인하여 영문/기호도 통과합니다. 숫자 4자리만 허용하려면 아래처럼 보완하는 것을 권장합니다.
- val isPasswordValid: Boolean - get() = !isPrivate || password.length == 4 + val isPasswordValid: Boolean + get() = !isPrivate || (password.length == 4 && password.all { it.isDigit() })app/src/main/java/com/texthip/thip/ui/common/modal/DialogPopup.kt (1)
37-43: 고정 폭 320.dp → 반응형 제약으로 변경 권장작은/큰 화면 대응을 위해 fillMaxWidth + widthIn(max)를 권장합니다. 현재 고정 폭은 태블릿/분할 화면에서 부자연스러울 수 있습니다.
적용 diff:
@@ - .width(320.dp) + .fillMaxWidth() + .widthIn(max = 320.dp) .background( color = colors.DarkGrey, shape = RoundedCornerShape(12.dp) ) .padding(20.dp)필요 시 import:
+import androidx.compose.foundation.layout.widthInapp/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt (3)
166-171: GenreChipRow에 width(12.dp) 적용은 오타/의도 오해 가능성 높음Row 너비를 12dp로 제한하면 칩이 잘리거나 배치가 비정상일 수 있습니다. 전체 폭 사용을 권장합니다.
제안 diff:
- GenreChipRow( - modifier = Modifier.width(12.dp), + GenreChipRow( + modifier = Modifier.fillMaxWidth(), genres = uiState.genres.toDisplayStrings(), selectedIndex = uiState.selectedGenreIndex, onSelect = onSelectGenre, horizontalArrangement = Arrangement.Center )
126-130: blur 조건이 많으면 저사양 기기에서 성능 저하 가능스크림(반투명 배경)만으로도 강조가 충분한 경우가 많습니다. blur 강도를 낮추거나 조건을 축소하는 방안을 고려해 주세요.
54-55: DATE_FORMATTER 로케일/중복 관리고정 패턴 대신 Locale에 맞춘 형식(또는 공용 유틸의 포맷터)을 재사용하면 일관성과 i18n 대응이 좋아집니다. utils에 이미 포맷터가 있다면 그쪽으로 통합 권장.
app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookSearchBottomSheet.kt (3)
31-31: 테마 색상 접근 방식 일관화 + 인디케이터 가독성 유지colors alias와 ThipTheme.colors를 혼용하면 가독성이 떨어집니다. 한 가지 방식으로 통일하는 것을 권장합니다. 아래처럼 alias import를 제거하고 ThipTheme.colors를 사용해 주세요.
- import com.texthip.thip.ui.theme.ThipTheme.colors- CircularProgressIndicator( - color = colors.White - ) + CircularProgressIndicator( + color = ThipTheme.colors.White + )추가로 로딩 시 수직 중앙정렬을 의도하셨다면 Box에 동일한 높이 정책을 부여해야 합니다(현재는 가로 중앙 정렬만 적용). 예시:
Box( modifier = Modifier .fillMaxWidth() .weight(1f) // <- 추가 (ColumnScope) .align(Alignment.CenterHorizontally), contentAlignment = Alignment.Center ) { CircularProgressIndicator(color = ThipTheme.colors.White) }Also applies to: 125-127
132-136: 로딩/목록 레이아웃의 높이/패딩 일관성 확보목록 분기에는 weight(1f)와 좌우 padding을 주셨지만, 로딩 분기(Box)에는 없어 상·하 위치가 달라 보일 수 있습니다. 상단 입력 영역 고정, 하단 콘텐츠 영역(로딩/목록 동일)로 레이아웃을 통일하면 시각적 점프가 줄어듭니다.
간단히는 로딩 Box에도 동일한 weight/패딩을 부여하거나, 아래처럼 공통 컨테이너로 감싸세요:
val contentModifier = Modifier .weight(1f) .padding(horizontal = 20.dp) if (isLoading) { Box(Modifier.fillMaxWidth().then(contentModifier), contentAlignment = Alignment.Center) { ... } } else { Column(Modifier.fillMaxWidth().then(contentModifier)) { ... } }
53-54: API 추가 사항 정리: showGroupBooksTab 기본값 문서화 및 사용처 검증기획상 모임 생성 플로우에서는 그룹 책 탭을 숨겨야 하므로, 해당 화면 호출부에서 showGroupBooksTab=false가 실제로 적용되는지 확인해 주세요. 또한 파라미터 의도를 KDoc으로 명시하면 재사용 시 혼동을 줄일 수 있습니다. 프리뷰에도 false 케이스를 하나 추가하는 것을 권장합니다.
프리뷰 예시:
@Preview(showBackground = true) @Composable fun PreviewBookSearchBottomSheet_SavedOnly() { ThipTheme { GroupBookSearchBottomSheet( onDismiss = {}, onBookSelect = {}, onRequestBook = {}, savedBooks = dummySavedBooks, groupBooks = emptyList(), showGroupBooksTab = false ) } }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (15)
app/src/main/java/com/texthip/thip/ui/common/modal/DialogPopup.kt(2 hunks)app/src/main/java/com/texthip/thip/ui/common/modal/ScrollbarUtil.kt(2 hunks)app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookListWithScrollbar.kt(4 hunks)app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookSearchBottomSheet.kt(5 hunks)app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupDatePicker.kt(4 hunks)app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupMemberLimitPicker.kt(1 hunks)app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupRoomDurationPicker.kt(7 hunks)app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupWheelPicker.kt(2 hunks)app/src/main/java/com/texthip/thip/ui/group/makeroom/mock/GroupMakeRoomRequest.kt(0 hunks)app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt(10 hunks)app/src/main/java/com/texthip/thip/ui/group/makeroom/util/WheelPickerDisplayUtils.kt(2 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(8 hunks)app/src/main/java/com/texthip/thip/utils/auth/AuthInterceptor.kt(2 hunks)app/src/main/res/values/strings.xml(2 hunks)
💤 Files with no reviewable changes (1)
- app/src/main/java/com/texthip/thip/ui/group/makeroom/mock/GroupMakeRoomRequest.kt
🧰 Additional context used
🧬 Code graph analysis (3)
app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt (2)
app/src/main/java/com/texthip/thip/ui/common/topappbar/InputTopAppBar.kt (1)
InputTopAppBar(25-69)app/src/main/java/com/texthip/thip/ui/common/modal/DialogPopup.kt (1)
DialogPopup(25-78)
app/src/main/java/com/texthip/thip/ui/common/modal/DialogPopup.kt (1)
app/src/main/java/com/texthip/thip/ui/common/buttons/ActionMediumButton.kt (1)
ActionMediumButton(31-80)
app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt (5)
app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt (1)
updateState(41-43)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/viewmodel/GroupViewModel.kt (1)
updateState(30-32)app/src/main/java/com/texthip/thip/ui/search/viewmodel/BookDetailViewModel.kt (1)
updateState(47-49)app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt (1)
updateState(26-28)
🔇 Additional comments (15)
app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupWheelPicker.kt (3)
71-75: 포매팅 개선 승인 - 가독성 향상람다 함수의 다중 라인 포매팅으로 코드 가독성이 향상되었습니다.
remember의존성 명시가 올바르고 클로저 변수 참조도 적절합니다.
77-81: 포매팅 개선 승인 - 일관된 스타일 유지
normalizeOffset람다의 다중 라인 포매팅이 이전getCircularIndex와 일관된 스타일을 유지합니다. 의존성 명시가 정확하고 함수 호출이 명확합니다.
83-87: 포매팅 개선 승인 - 일관된 코드 스타일
offsetToIndex람다의 포매팅도 이전 두 람다와 일관된 다중 라인 스타일을 유지합니다. 모든remember초기화가 통일된 형식으로 개선되어 코드 일관성이 향상되었습니다.app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupMemberLimitPicker.kt (1)
31-31: 범위 변경은 올바르며 상태 검증 계층으로 보호됩니다.
memberLimit상태의 검증자가 이미memberLimit in 2..30범위를 강제하고 있어서, 이 변경은 안전합니다. 선택기 범위(2..30)가 상태 검증자와 일치하며, 기본값(30)도 유효합니다.setMemberLimit()호출 시 상태 검증자가 1 이하의 값을 거부하므로 추가적인 확인이 불필요합니다.app/src/main/java/com/texthip/thip/ui/common/modal/ScrollbarUtil.kt (1)
28-42: 주석 추가로 가독성이 향상되었습니다.한글 주석이 추가되어 스크롤바의 트랙과 썸 렌더링 로직을 이해하기 쉬워졌습니다.
app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookListWithScrollbar.kt (1)
58-61: 리뷰 의견 철회됨 – 제약 조건이 적절히 제공됩니다.
GroupBookListWithScrollbar의 모든 production 사용처는ModalBottomSheetLayout내에서.fillMaxHeight(0.8f)로 제약된 부모 컨테이너에 중첩되어 있습니다. 부모Column의.weight(1f)수정자가 높이 제약을 전달하므로,fillMaxHeight()사용은 안전합니다.app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupDatePicker.kt (1)
43-49:remember블록 추가로 성능 최적화되었습니다.years와 days 계산을
remember블록으로 감싸고 적절한 의존성을 지정하여 불필요한 재계산을 방지했습니다. 이는 컴포지션 성능을 향상시킵니다.app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomUiState.kt (1)
32-34: 확인 다이얼로그 상태 추가, 방향성 적합
showConfirmDialog도입으로 VM의 토글 API와 일관됩니다. UI 제어에 적합해 보입니다.app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt (1)
328-330: 확인 다이얼로그 토글 API 추가, 일관성 양호UIState와 연결되는 단순 토글로 역할이 명확합니다. 성공/실패 콜백에서 다이얼로그를 닫는 정책만 화면단과 합의해 주세요.
app/src/main/res/values/strings.xml (3)
34-35: 버튼 공통 라벨 추가 OK
cancel,confirm추가로 다이얼로그 컴포넌트 재사용성이 좋아집니다.
309-309: 모집 기간 메시지 자리표시자 점검
%d일수 포맷은 GroupRoomDurationPicker에서ChronoUnit.DAYS로 전달되어 일관적입니다. 0일(내일 시작) 케이스도 자연스럽게 표시되는지 한 번만 실제 UI로 확인 부탁드립니다.
311-313: 생성 확인 메시지 포맷 인자 개수/형식 확인
%1$s~%4$s총 4개 문자열이 필요합니다. 실제 사용처에서 로케일 포맷(예:yyyy.MM.dd)로 렌더링된 날짜 문자열을 전달하는지 확인해 주세요. 인자 누락 시 런타임 예외가 발생합니다.app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupRoomDurationPicker.kt (2)
68-76: 공용 유틸을 통한 날짜 자동 보정 👍
validateDateRange로 보정 후 변경이 없을 때만 콜백하는 흐름이 깔끔합니다.
60-66: 모집기간 디바운스 계산, UX 향상에 적합입력 중 깜빡임을 줄여 가독성이 좋아집니다. 이 상태는 초기화/재선택 시 잘 리셋되고 있습니다.
초기 진입 직후 1초 내 빠른 조작에서도 기대대로 갱신되는지 실제 기기에서 한 번만 확인 부탁드립니다.
app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt (1)
75-83: 확인 다이얼로그 뒤에 생성 트리거 이동 — 잘 반영됨생성 호출을 onToggleConfirmDialog 경유로 변경해 사용자 확인 단계를 추가한 점 좋습니다.
| confirmText: String = stringResource(R.string.yes), | ||
| cancelText: String = stringResource(R.string.no), |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
| val averageItemHeight = if (layoutInfo.visibleItemsInfo.isNotEmpty()) { | ||
| layoutInfo.visibleItemsInfo.map { it.size }.average().toFloat() | ||
| } else { | ||
| 100f | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
평균 아이템 높이 계산이 이질적인 리스트에서 부정확할 수 있습니다.
현재 구현은 표시되는 아이템들의 평균 높이를 사용하여 전체 컨텐츠 높이를 추정합니다. 아이템 크기가 크게 다를 경우 스크롤바 위치가 부정확해질 수 있습니다. 또한 fallback 값 100f는 임의로 선택된 값입니다.
이 컴포넌트가 크기가 균일한 아이템 리스트에서만 사용되는지 확인하세요:
🏁 Script executed:
#!/bin/bash
# drawVerticalScrollbar(LazyListState) 사용처 확인
rg -nP --type=kotlin -C10 'drawVerticalScrollbar\s*\(\s*\w+\s*:\s*LazyListState'Length of output: 0
검증을 위해 더 광범위한 코드베이스 검색이 필요합니다.
---
🏁 Script executed:
```shell
#!/bin/bash
# drawVerticalScrollbar 함수 정의 찾기
rg -nP --type=kotlin 'fun\s+drawVerticalScrollbar' -A 15
Length of output: 0
🏁 Script executed:
#!/bin/bash
# drawVerticalScrollbar 함수의 모든 호출처 검색
rg -nP --type=kotlin 'drawVerticalScrollbar\s*\(' -B 5 -A 2Length of output: 3398
🏁 Script executed:
#!/bin/bash
# ScrollbarUtil.kt 파일 전체 내용 확인
fd -t f 'ScrollbarUtil\.kt' --exec cat {}Length of output: 3756
추가 검증을 위해 실제 사용 사례를 확인하겠습니다.
---
🏁 Script executed:
```shell
#!/bin/bash
# GroupBookListWithScrollbar.kt 파일 내용 확인
fd -t f 'GroupBookListWithScrollbar\.kt' --exec cat {}
Length of output: 3464
🏁 Script executed:
#!/bin/bash
# GroupBookListWithScrollbar의 컴포지션 호출처 검색
rg -nP --type=kotlin 'GroupBookListWithScrollbar\s*\(' -B 3 -A 3Length of output: 4887
LazyListState 버전 스크롤바 위치 계산을 개선해야 합니다.
현재 구현(drawVerticalScrollbar(lazyListState))은 visibleItemsInfo에 표시되는 아이템들의 평균 높이만 사용하여 전체 컨텐츠 높이를 추정합니다. 실제 사용처인 GroupBookListWithScrollbar에서 각 아이템이 CardBookSearch(가변 높이) + Spacers(25.dp 고정)로 구성되어 있기 때문에, 비가시 영역의 아이템들이 다른 높이를 가질 경우 스크롤바 위치가 정확하지 않을 수 있습니다. 또한 fallback 값 100f는 임의적입니다.
더 정확한 스크롤 위치 계산을 위해 다음을 고려해야 합니다:
- 전체 아이템의 높이 정보를 누적하여 추적하거나
- 평균값을 더 보수적으로 계산하거나
- 스크롤 진행률 계산 로직을 재검토
🤖 Prompt for AI Agents
app/src/main/java/com/texthip/thip/ui/common/modal/ScrollbarUtil.kt around lines
64-68: the scrollbar current code estimates total content height using only the
average of visibleItemsInfo (with a hardcoded 100f fallback), which yields
inaccurate scroll positions for variable-height items; fix it by replacing the
arbitrary fallback and naive average with a conservative estimation using
measured data — compute the sum of the heights of visibleItemsInfo, derive an
average from those measured items (only if count>0), then estimate unseen items
height as average * (totalItemsCount - visibleItemsInfo.count), and use measured
sum + estimated unseen height as total content height; alternatively implement a
per-item height cache (update via onGloballyPositioned) and use the exact summed
heights when available to compute precise scrollbar position.
| val tabs = if (showGroupBooksTab) { | ||
| listOf( | ||
| stringResource(R.string.group_saved_book), | ||
| stringResource(R.string.group_book) | ||
| ) | ||
| } else { | ||
| listOf(stringResource(R.string.group_saved_book)) | ||
| } |
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupDatePicker.kt
Show resolved
Hide resolved
| // 날짜 범위 계산 | ||
| val daysBetween = ChronoUnit.DAYS.between(startDate, endDate) | ||
| val isOverLimit = daysBetween > 91 | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
최대 기간 상수화로 UiState와 기준 통일 필요
isOverLimit = daysBetween > 91는 UiState의 90일 기준과 어긋납니다. 앞서 제안한 상수를 사용해 통일해 주세요.
- val isOverLimit = daysBetween > 91
+ import com.texthip.thip.ui.group.makeroom.GroupRoomConstraints
+ val isOverLimit = daysBetween > GroupRoomConstraints.MAX_DURATION_DAYSCommittable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupRoomDurationPicker.kt
around lines 94 to 97, replace the hardcoded 91 with the shared max-duration
constant used by UiState (e.g., MAX_ROOM_DURATION_DAYS = 90) so the check uses
the same 90-day threshold; import or reference the existing constant from the
UiState/shared constants file and update the comparison to use that constant
(daysBetween > MAX_ROOM_DURATION_DAYS) to ensure both files use the same limit.
| if (uiState.isLoading) { | ||
| Box( | ||
| modifier = Modifier | ||
| .fillMaxSize() | ||
| .background(Color.Black.copy(alpha = 0.5f)), | ||
| contentAlignment = Alignment.Center | ||
| ) { | ||
| CircularProgressIndicator(color = colors.NeonGreen) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
로딩 오버레이가 터치 이벤트를 차단하지 않음 → 중복 입력/내부 클릭 가능성
배경에 색만 입힌 Box는 터치를 소비하지 않습니다. 클릭/포인터를 소비해 입력을 차단하세요.
적용 diff:
@@
- if (uiState.isLoading) {
- Box(
- modifier = Modifier
- .fillMaxSize()
- .background(Color.Black.copy(alpha = 0.5f)),
- contentAlignment = Alignment.Center
- ) {
- CircularProgressIndicator(color = colors.NeonGreen)
- }
- }
+ if (uiState.isLoading) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.Black.copy(alpha = 0.5f))
+ .clickable(
+ indication = null,
+ interactionSource = remember { MutableInteractionSource() }
+ ) { /* consume clicks while loading */ },
+ contentAlignment = Alignment.Center
+ ) {
+ CircularProgressIndicator(color = colors.NeonGreen)
+ }
+ }필요 import:
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.runtime.remember📝 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.
| if (uiState.isLoading) { | |
| Box( | |
| modifier = Modifier | |
| .fillMaxSize() | |
| .background(Color.Black.copy(alpha = 0.5f)), | |
| contentAlignment = Alignment.Center | |
| ) { | |
| CircularProgressIndicator(color = colors.NeonGreen) | |
| } | |
| } | |
| if (uiState.isLoading) { | |
| Box( | |
| modifier = Modifier | |
| .fillMaxSize() | |
| .background(Color.Black.copy(alpha = 0.5f)) | |
| .clickable( | |
| indication = null, | |
| interactionSource = remember { MutableInteractionSource() } | |
| ) { /* consume clicks while loading */ }, | |
| contentAlignment = Alignment.Center | |
| ) { | |
| CircularProgressIndicator(color = colors.NeonGreen) | |
| } | |
| } |
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt
around lines 318 to 327, the semi-transparent Box used for the loading overlay
doesn't consume touch events so underlying UI can still receive clicks; update
the Box modifier to consume pointer input (e.g. add .pointerInput(Unit) {
awaitPointerEventScope { while (true) { awaitPointerEvent() } } } or use a
non-enabled clickable) so all touch events are intercepted while loading, and
add the necessary imports (androidx.compose.ui.input.pointer.pointerInput and
androidx.compose.ui.input.pointer.awaitPointerEventScope).
| @JvmStatic | ||
| fun validateDateRange( | ||
| startDate: LocalDate, | ||
| endDate: LocalDate, | ||
| minDate: LocalDate, | ||
| maxDate: LocalDate | ||
| ): Pair<LocalDate, LocalDate> { | ||
| // 시작 날짜 유효성 검사 | ||
| val validatedStart = when { | ||
| startDate.isBefore(minDate) -> minDate | ||
| startDate.isAfter(maxDate) -> maxDate | ||
| else -> startDate | ||
| } | ||
|
|
||
| // 종료 날짜 유효성 검사 | ||
| val minEndDate = validatedStart.plusDays(1) | ||
| val validatedEnd = when { | ||
| endDate.isAfter(maxDate) -> maxDate | ||
| endDate.isBefore(minEndDate) -> minEndDate | ||
| else -> endDate | ||
| } | ||
|
|
||
| return validatedStart to validatedEnd | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
경계값 버그: start=maxDate일 때 end가 maxDate를 넘어설 수 있음
현재 validatedStart= maxDate가 되면 minEndDate = validatedStart.plusDays(1)가 maxDate+1이 되어, endDate <= maxDate인 경우에도 분기에서 minEndDate를 반환해 상한을 위반할 소지가 있습니다. 시작일 상한을 maxDate.minusDays(1)로 제한하고, 종료일은 그 최소값과 maxDate 사이로 재조정해야 합니다.
권장 수정:
fun validateDateRange(
startDate: LocalDate,
endDate: LocalDate,
minDate: LocalDate,
maxDate: LocalDate
): Pair<LocalDate, LocalDate> {
- // 시작 날짜 유효성 검사
- val validatedStart = when {
- startDate.isBefore(minDate) -> minDate
- startDate.isAfter(maxDate) -> maxDate
- else -> startDate
- }
+ // 시작일은 최소 minDate, 최대 (maxDate - 1일)로 제한
+ val maxStart = maxDate.minusDays(1)
+ val validatedStart = when {
+ startDate.isBefore(minDate) -> minDate
+ startDate.isAfter(maxStart) -> maxStart
+ else -> startDate
+ }
- // 종료 날짜 유효성 검사
- val minEndDate = validatedStart.plusDays(1)
- val validatedEnd = when {
- endDate.isAfter(maxDate) -> maxDate
- endDate.isBefore(minEndDate) -> minEndDate
- else -> endDate
- }
+ // 종료일은 최소 (validatedStart+1일), 최대 maxDate
+ val minEndDate = validatedStart.plusDays(1)
+ val validatedEnd = when {
+ endDate.isBefore(minEndDate) -> minEndDate
+ endDate.isAfter(maxDate) -> maxDate
+ else -> endDate
+ }
return validatedStart to validatedEnd
}추가로(선택), 최대 기간(예: 90일) 제한도 이 유틸에서 옵션 인자로 처리하면 화면/VM 간 기준 일원화에 도움이 됩니다.
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/ui/group/makeroom/util/WheelPickerDisplayUtils.kt
around lines 32 to 55, the current validation lets validatedStart become maxDate
which makes minEndDate = maxDate.plusDays(1) and can force validatedEnd beyond
maxDate; change the start upper bound to maxDate.minusDays(1) so validatedStart
= min(startDate, maxDate.minusDays(1)) and then clamp validatedEnd between
validatedStart.plusDays(1) and maxDate (i.e., validatedEnd = max(minEndDate,
min(endDate, maxDate))). Optionally add a parameter for a maximum span (e.g.,
maxDays) and enforce validatedEnd <= validatedStart.plusDays(maxDays) if
provided.
app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt
Show resolved
Hide resolved
| fun loadMoreSearchResults() { | ||
| val currentState = _uiState.value | ||
| if (currentState.isLoadingMoreSearchResults || | ||
| if (currentState.isLoadingMoreSearchResults || | ||
| currentState.isSearching || | ||
| currentState.isLastSearchPage || | ||
| currentState.isLastSearchPage || | ||
| currentState.searchResults.isEmpty() || | ||
| currentState.currentSearchQuery.isBlank()) { | ||
| currentState.currentSearchQuery.isBlank() | ||
| ) { | ||
| return |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
검색 ‘더보기’에서 이전 상태로 병합하는 stale merge 가능성
currentState 스냅샷을 사용해 searchResults = currentState.searchResults + newResults로 병합하면, 호출 사이에 상태가 변한 경우(예: 빠른 추가 검색, 다른 페이지 로딩 완료) 신규 항목 손실/중복이 생길 수 있습니다. 업데이트 클로저 내에서 최신 상태를 기반으로 병합하세요.
- updateState {
- it.copy(
- searchResults = currentState.searchResults + newResults,
- searchPage = response.page,
- isLastSearchPage = response.last,
- isLoadingMoreSearchResults = false
- )
- }
+ updateState { state ->
+ state.copy(
+ searchResults = state.searchResults + newResults,
+ searchPage = response.page,
+ isLastSearchPage = response.last,
+ isLoadingMoreSearchResults = false
+ )
+ }Also applies to: 262-267
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt
around lines 236-244 (and similarly 262-267), the code snapshots currentState
and then writes searchResults = currentState.searchResults + newResults, which
can lose or duplicate items if state changed between read and write; instead,
perform the merge inside an atomic state update using the stateflow's
update/compare-and-set style updater so the lambda receives the latest state and
you merge using prev.searchResults + newResults before returning prev.copy(...),
applying the same change to the other occurrence at 262-267.
| private fun isAuthError(response: Response): Boolean { | ||
| return try { | ||
| val body = response.peekBody(Long.MAX_VALUE).string() | ||
| // 에러 코드 40108 (인증 처리 중 서버 오류) 확인 | ||
| body.contains("40108") | ||
| } catch (e: Exception) { | ||
| false | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
에러 감지 로직이 취약하고 성능 문제가 있을 수 있습니다.
현재 구현의 문제점:
-
취약한 에러 감지:
body.contains("40108")은 응답 본문 어디든 "40108" 문자열이 있으면 인증 에러로 간주합니다. 이는 실제 데이터나 다른 필드에 해당 문자열이 포함된 경우 false positive를 발생시킬 수 있습니다. -
성능 이슈:
peekBody(Long.MAX_VALUE)는 매우 큰 응답의 경우 메모리와 성능에 부담을 줄 수 있습니다. -
광범위한 예외 처리: 모든 예외를 catch하고
false를 반환하면 실제 문제(예: JSON 파싱 오류)를 은폐할 수 있습니다.
JSON 응답을 파싱하여 정확한 에러 코드 필드를 확인하는 것을 권장합니다:
private fun isAuthError(response: Response): Boolean {
return try {
- val body = response.peekBody(Long.MAX_VALUE).string()
- // 에러 코드 40108 (인증 처리 중 서버 오류) 확인
- body.contains("40108")
- } catch (e: Exception) {
+ val body = response.peekBody(2048).string() // 적절한 크기로 제한
+ val jsonObject = JSONObject(body)
+ val errorCode = jsonObject.optString("code", "")
+ errorCode == "40108"
+ } catch (e: JSONException) {
+ false
+ } catch (e: IOException) {
false
}
}또는 프로젝트에서 사용 중인 JSON 라이브러리(Gson, Moshi 등)를 활용할 수 있습니다.
🤖 Prompt for AI Agents
app/src/main/java/com/texthip/thip/utils/auth/AuthInterceptor.kt lines 46-54:
the current isAuthError implementation uses peekBody(Long.MAX_VALUE) and
body.contains("40108") which causes false positives, memory/CPU issues and
swallows all exceptions; instead read a bounded peek of the response (limit to a
reasonable size, e.g., 64KB), parse the body as JSON using the project’s JSON
library (Gson/Moshi) to extract the explicit error code field (e.g., "code" or
"error.code"), compare that numeric value to 40108, and only catch and log
JSON/parsing/IO exceptions (do not silently swallow unexpected exceptions)
returning false on parse errors; ensure the code handles absent bodies/nulls
defensively.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (3)
app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt (3)
292-316: Box 오버레이 → Compose Dialog 전환, 접근성/포커스/뒤로가기 보장Dialog로 교체되어 시스템 레벨 동작이 확보되었습니다. onDismissRequest 처리도 적절합니다. 이전 리뷰 지적을 잘 반영하셨습니다.
299-304: 문자열 포맷 인자 중복(meetingStartDate 2회) — 모집 종료일을 전달해야 합니다.strings.xml의 group_create_confirm_message는 4개 인자를 기대하며, 2번째는 “모집 종료일”이어야 합니다. 현재 2번째·3번째에 meetingStartDate가 중복 전달됩니다. UiState에 recruitmentEndDate(또는 동등 필드)를 추가·관리한 뒤 아래처럼 교체하세요.
description = stringResource( R.string.group_create_confirm_message, java.time.LocalDate.now().format(DATE_FORMATTER), - uiState.meetingStartDate.format(DATE_FORMATTER), + uiState.recruitmentEndDate.format(DATE_FORMATTER), uiState.meetingStartDate.format(DATE_FORMATTER), uiState.meetingEndDate.format(DATE_FORMATTER) ),검증 스크립트(플레이스홀더 순서 확인):
#!/bin/bash # strings.xml에서 group_create_confirm_message 확인 fd -a strings.xml | while read f; do echo "== $f" rg -nC2 'name="group_create_confirm_message"' "$f" done
318-327: 로딩 오버레이가 터치를 차단하지 않습니다 → 중복 입력/오작동 위험반투명 Box는 포인터를 소비하지 않습니다. 로딩 중에는 입력을 반드시 인터셉트하세요.
@@ -import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Color +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.runtime.remember @@ - if (uiState.isLoading) { + if (uiState.isLoading) { Box( modifier = Modifier .fillMaxSize() - .background(Color.Black.copy(alpha = 0.5f)), + .background(Color.Black.copy(alpha = 0.5f)) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { /* consume clicks while loading */ }, contentAlignment = Alignment.Center ) { CircularProgressIndicator(color = colors.NeonGreen) } }
🧹 Nitpick comments (4)
app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupDatePicker.kt (3)
62-75: 예외 기반 폴백 대신 계산 기반으로 단순화하세요.정상 흐름에서 try/catch는 불필요한 오버헤드입니다. 해당 월의 길이로 안전한 day를 계산해 바로 생성하면 가독성과 성능이 개선됩니다. 월 변경 블록(라인 95-101)에도 동일 적용을 권장합니다.
+import kotlin.math.min @@ - val fallbackDate = try { - LocalDate.of(newYear, month, day) - } catch (e: Exception) { - val lastDay = LocalDate.of(newYear, month, 1).lengthOfMonth() - LocalDate.of(newYear, month, lastDay) - } + val lastDay = LocalDate.of(newYear, month, 1).lengthOfMonth() + val fallbackDate = LocalDate.of(newYear, month, min(day, lastDay))월 변경 시:
- val fallbackDate = try { - LocalDate.of(year, newMonth, day) - } catch (e: Exception) { - val lastDay = LocalDate.of(year, newMonth, 1).lengthOfMonth() - LocalDate.of(year, newMonth, lastDay) - } + val lastDay = LocalDate.of(year, newMonth, 1).lengthOfMonth() + val fallbackDate = LocalDate.of(year, newMonth, min(day, lastDay))
68-74: 클램프 로직 중복 → 헬퍼로 추출해 DRY 유지.min/maxDate 보정 when 블록이 연-월-일에 반복됩니다. private 헬퍼(예: clamp(date, min, max))로 추출하거나 기존 공용 유틸이 있다면 재사용해 단일 소스로 관리하세요. 유지보수 시 실수 가능성이 줄어듭니다.
+private fun clamp(date: LocalDate, minDate: LocalDate, maxDate: LocalDate): LocalDate = + when { + date.isBefore(minDate) -> minDate + date.isAfter(maxDate) -> maxDate + else -> date + } @@ - val validatedDate = when { - fallbackDate.isBefore(minDate) -> minDate - fallbackDate.isAfter(maxDate) -> maxDate - else -> fallbackDate - } + val validatedDate = clamp(fallbackDate, minDate, maxDate) @@ - val validatedDate = when { - fallbackDate.isBefore(minDate) -> minDate - fallbackDate.isAfter(maxDate) -> maxDate - else -> fallbackDate - } + val validatedDate = clamp(fallbackDate, minDate, maxDate) @@ - val validatedDate = when { - newDate.isBefore(minDate) -> minDate - newDate.isAfter(maxDate) -> maxDate - else -> newDate - } + val validatedDate = clamp(newDate, minDate, maxDate)Also applies to: 102-106, 130-134
43-49: 월/일 목록을 min/maxDate에 맞춰 동적 제한 권장(UX).현재는 불가능한 월/일도 노출됐다가 선택 시점에 클램프됩니다. 선택 목록 자체를 범위에 맞추면 혼란이 줄어듭니다.
- val months = remember { (1..12).toList() } - val days = remember(year, month) { - (1..LocalDate.of(year, month, 1).lengthOfMonth()).toList() - } + val months = remember(year, minDate, maxDate) { + when (year) { + minDate.year -> (minDate.monthValue..12).toList() + maxDate.year -> (1..maxDate.monthValue).toList() + else -> (1..12).toList() + } + } + val days = remember(year, month, minDate, maxDate) { + val last = LocalDate.of(year, month, 1).lengthOfMonth() + val start = if (year == minDate.year && month == minDate.monthValue) minDate.dayOfMonth else 1 + val end = if (year == maxDate.year && month == maxDate.monthValue) minDate.withYear(year).withMonth(month).let { + maxDate.dayOfMonth + } else last + (start..end).toList() + }또한 day 선택 항목을 다음처럼 보정하면 일관성이 높습니다:
- selectedItem = day.coerceAtMost(days.maxOrNull() ?: 1), + selectedItem = day.coerceIn(days.firstOrNull() ?: 1, days.lastOrNull() ?: 1),app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt (1)
166-172: 가로 중앙 정렬 의도라면 고정 width(12.dp) 제거가 필요합니다.현재 Modifier.width(12.dp)로 영역이 12dp로 제한되어 Center가 무의미합니다. 리스트 폭을 확보하세요.
- GenreChipRow( - modifier = Modifier.width(12.dp), + GenreChipRow( + modifier = Modifier.fillMaxWidth(), genres = uiState.genres.toDisplayStrings(), selectedIndex = uiState.selectedGenreIndex, onSelect = onSelectGenre, horizontalArrangement = Arrangement.Center )
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira 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/ui/group/makeroom/component/GroupDatePicker.kt(4 hunks)app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt(10 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt (2)
app/src/main/java/com/texthip/thip/ui/common/topappbar/InputTopAppBar.kt (1)
InputTopAppBar(25-69)app/src/main/java/com/texthip/thip/ui/common/modal/DialogPopup.kt (1)
DialogPopup(25-78)
🔇 Additional comments (2)
app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupDatePicker.kt (1)
126-136: 일 선택 시 안전 보정 + 콜백 전달 처리, 잘 적용됐습니다.out-of-range를 min/max로 클램프 후 onDateSelected 전달 로직이 명확합니다.
app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt (1)
139-140: 완료 버튼 → 확인 다이얼로그 트리거, 좋습니다.플로우가 명확해졌고 실수 제출을 줄일 수 있습니다.
➕ 이슈 링크
🔎 작업 내용
📸 스크린샷
😢 해결하지 못한 과제
[] TASK
📢 리뷰어들에게
Summary by CodeRabbit
릴리스 노트
새로운 기능
개선사항
버그 수정