Conversation
Walkthrough스터디 등록 화면 구현 및 상태 관리 기능을 추가합니다. 위치 선택 바텀시트, 다양한 선택 컴포넌트, 입력 필드를 포함한 UI 계층을 확장하고 ViewModel을 통한 폼 데이터 및 검증 로직을 구현합니다. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant RegisterStudyRoute as RegisterStudyRoute
participant RegisterStudyScreen as RegisterStudyScreen
participant ViewModel as RegisterStudyViewModel
participant State as UI State<br/>(uiState)
participant SideEffect as SideEffect<br/>(SharedFlow)
User->>RegisterStudyRoute: 스터디 등록 화면 진입
RegisterStudyRoute->>ViewModel: hiltViewModel() 초기화
ViewModel->>ViewModel: loadLocationData()
ViewModel->>State: 초기 상태 설정
RegisterStudyRoute->>RegisterStudyScreen: uiState, 콜백 전달
User->>RegisterStudyScreen: 스터디명 입력
RegisterStudyScreen->>ViewModel: onStudyNameChange(name)
ViewModel->>State: studyName 업데이트
User->>RegisterStudyScreen: 테마 선택
RegisterStudyScreen->>ViewModel: onCategorySelect(themes)
ViewModel->>State: studyThemes 업데이트
User->>RegisterStudyScreen: 활동 유형 선택
RegisterStudyScreen->>ViewModel: onActivityTypeSelect(type)
ViewModel->>State: activityType 업데이트
User->>RegisterStudyScreen: 지역 검색
RegisterStudyScreen->>ViewModel: onLocationQueryChange(query)
ViewModel->>ViewModel: searchLocation(query) [debounced]
ViewModel->>State: locationResults 업데이트
User->>RegisterStudyScreen: 지역 추가
RegisterStudyScreen->>ViewModel: addSelectedRegion(region)
ViewModel->>State: selectedRegions 업데이트 (max 3)
User->>RegisterStudyScreen: 인원 수 선택
RegisterStudyScreen->>ViewModel: onMemberCountChange(count)
ViewModel->>State: memberCount 업데이트
User->>RegisterStudyScreen: 요금 정보 입력
RegisterStudyScreen->>ViewModel: onFeeInfoChange(hasFee, amount)
ViewModel->>State: hasFee, feeAmount 업데이트
User->>RegisterStudyScreen: 성향 선택
RegisterStudyScreen->>ViewModel: onPersonalityChange(categoryIdx, value)
ViewModel->>State: preferences[categoryIdx] 업데이트
User->>RegisterStudyScreen: 설명 입력
RegisterStudyScreen->>ViewModel: onDescriptionChange(desc)
ViewModel->>State: description 업데이트
User->>RegisterStudyScreen: 제출 클릭
RegisterStudyScreen->>ViewModel: isStepValid(currentPage) 확인
alt 유효한 경우
RegisterStudyScreen->>ViewModel: submit()
ViewModel->>SideEffect: NavigateToHome 발행
SideEffect->>RegisterStudyRoute: side effect 수신
RegisterStudyRoute->>User: 홈 화면으로 이동
else 무효한 경우
RegisterStudyScreen->>User: 다음 페이지로 진행 또는 유지
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes 주의 깊게 검토해야 할 영역:
Possibly related PRs
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)
Tip 📝 Customizable high-level summaries are now available in beta!You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.
Example instruction:
Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later. 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.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyScreen.kt (1)
80-100: 탭 인덱싱 오프셋 버그 및 필터 미구현으로 인한 동작 오류 확인코드베이스 검증 결과, 리뷰 코멘트의 지적이 모두 확인되었습니다.
1. 탭 인덱싱 버그 (line 90 확인됨)
현재 코드:
val tabs: List<String> = remember(selected) { listOf("전체") + selected } // line 58 var selectedTab by remember { mutableStateOf(0) } // line 57 val currentLocation = selected.getOrNull(selectedTab) // line 90
selectedTab == 0일 때는 "전체" 탭이지만,selected.getOrNull(0)으로 첫 번째 지역을 반환하게 됩니다.- 결과적으로 "전체" 탭 선택 시에도 첫 번째 선택 지역으로 필터가 적용되는 버그가 발생합니다.
2. 필터 로직 미구현 (lines 93-100 확인됨)
val studiesForUi = if (currentLocation.isNullOrBlank()) { studiesAll } else { studiesAll.filter { item -> false // ← 항상 false 반환 } }지역을 선택하면 무조건 빈 리스트가 반환됩니다.
3. 추가 정보: StudyResult에 지역 필드 부재
StudyResult데이터 클래스에는location,address등의 지역 관련 필드가 없습니다. 필터 구현 전에StudyResult에 지역 필드를 먼저 추가해야 합니다.
수정 필요 사항:
- val currentLocation = selected.getOrNull(selectedTab) - // 🔧 여기서 실제 StudyResult의 지역 필드명으로 필터하세요 (예: item.location / item.address 등) - val studiesForUi = if (currentLocation.isNullOrBlank()) { - studiesAll - } else { - studiesAll.filter { item -> - // 예시) item.location?.contains(currentLocation) == true - // 필드명이 다르면 위 라인만 고치면 됨 - false - } - } + val currentLocation = if (selectedTab == 0) null else selected.getOrNull(selectedTab - 1) + + val studiesForUi = if (currentLocation.isNullOrBlank()) { + studiesAll + } else { + studiesForUi.filter { item -> + // TODO: StudyResult에 지역 필드 추가 후 실제 필터 조건 구현 + // item.location?.contains(currentLocation) == true + } + }
🧹 Nitpick comments (15)
core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/button/SocialLoginButton.kt (1)
1-190: SocialLoginButton 패키지 이동 구조는 적절합니다 (색 사용 일관성은 한 번 정리 제안)버튼 전용 패키지로 이동한 건 컴포넌트 구조화 측면에서 좋아 보입니다. 다만 NaverStartButton에서는
contentColor = White, NaverLoginButton에서는SpotTheme.colors.White를 쓰고 있어 색 접근 방식이 섞여 있습니다.디자인 시스템 관점에서 둘 중 하나로만 통일해 두면(예: 전부
SpotTheme.colors.White) 이후 유지보수가 조금 더 수월할 것 같습니다.feature/study/src/main/java/com/umcspot/spot/study/register/component/SelectionChip.kt (1)
22-54: SelectionChip에 접근성 semantics 추가를 권장합니다
noRippleClickable만 사용하고 있어 시각적/터치 동작은 좋지만, 스크린 리더 입장에선 버튼/선택 상태라는 정보가 노출되지 않습니다.아래처럼
Role.Button과selectedsemantics를 추가해 두면 토글형 컴포넌트로 더 잘 인식될 수 있습니다.@@ fun SelectionChip( text: String, isSelected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier ) { - Box( - modifier = modifier + Box( + modifier = modifier + .semantics { + role = Role.Button + selected = isSelected + } .height(screenHeightDp(35.dp)) @@ .clip(RoundedCornerShape(6.dp)) .noRippleClickable(onClick = onClick),위 변경 시
import androidx.compose.ui.semantics.Role/semantics,selected임포트만 추가해 주시면 됩니다.feature/study/src/main/java/com/umcspot/spot/study/register/component/PriceTextField.kt (1)
28-79: PriceTextField에서 숫자 이외 문자 필터링을 권장합니다
KeyboardType.Number만으로는 사용자가-,.또는 공백/문자를 입력·붙여넣기 할 수 있어서, 금액 파싱 쪽에서 별도 검증이 필요합니다.컴포넌트 단에서 숫자만 허용하도록 한 번 걸러 주면 이후 로직이 단순해집니다.
- BasicTextField( - value = value, - onValueChange = onValueChange, + BasicTextField( + value = value, + onValueChange = { newValue -> + // 숫자만 허용 (예: "1,000" 붙여넣기 시 "1000"으로 정규화) + onValueChange(newValue.filter { it.isDigit() }) + },추가로, 나중에 필요하다면 최소/최대 길이(자리 수) 제한도 함께 고려해 볼 수 있을 것 같습니다.
core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/button/ImageTextButton.kt (1)
46-201: MultiButton 구현은 안정적으로 보이며, 긴 텍스트용 ellipsis 정도만 추가를 제안드립니다
semantics { role = Role.Button; selected = checked }와MutableInteractionSource+ pressed 색상 처리까지 잘 연결되어 있어서 토글 버튼으로 동작은 안정적으로 보입니다.다만
Text가maxLines = 1만 설정되어 있고overflow가 없어, 기획 변경 등으로 라벨이 길어질 경우 잘려 보일 수 있습니다. 방어적으로 아래처럼 ellipsis만 추가해 두면 향후 변경에도 안전합니다.- Text( - text = text, - style = SpotTheme.typography.h4, - color = colors.text, - maxLines = 1 - ) + Text( + text = text, + style = SpotTheme.typography.h4, + color = colors.text, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + )(위 변경 시
import androidx.compose.ui.text.style.TextOverflow임포트가 필요합니다.)core/designsystem/src/main/res/drawable/ic_location.xml (1)
1-8: 아이콘 색상 하드코딩 (선택 사항)벡터 자체는 문제 없어 보입니다. 다만
android:fillColor="#337BFF"처럼 색을 직접 하드코딩하면 다크 모드/브랜드 색 변경 시 재사용성이 조금 떨어질 수 있습니다.
나중에 테마 확장을 고려한다면@color/…리소스로 분리해 두는 것도 한 번 생각해볼 만합니다.feature/study/src/main/java/com/umcspot/spot/study/register/component/FeeInputSection.kt (1)
12-48: 수강료 선택/입력 분리가 명확하게 잘 되어 있습니다 (소소한 제안 포함)
hasFee: Boolean?로 “미선택 / 없음 / 있음” 3가지 상태를 표현하고, 선택이 발생한 이후에는onFeeTypeChange(Boolean)으로만 true/false를 전달하는 구조가 직관적입니다.hasFee == true일 때만PriceTextField를 보여주는 것도 UX 측면에서 자연스럽습니다.사소한 수준이지만, 나중에 다국어 지원이나 텍스트 재사용을 고려하신다면
"없음","있음"문자열은string.xml로 빼 두는 것도 좋을 것 같습니다.feature/study/src/main/java/com/umcspot/spot/study/register/component/BinaryChoiceRow.kt (1)
17-54: BinaryChoiceRow 추상화가 재사용성 측면에서 좋습니다 (타입 표현은 선택 사항)
- 좌/우 옵션을
SelectionChip두 개와 중앙VerticalDivider로 구성해서 디자인 요구사항을 잘 캡쳐한 것 같습니다.selectedIndex: Int?로 “미선택(null) / 왼쪽(0) / 오른쪽(1)” 상태를 표현한 것도 간단하고 이해하기 쉽습니다.추가로, 나중에 선택 상태가 더 복잡해질 여지가 있다면
enum class나sealed class로 선택 상태를 표현하는 것도 타입 세이프티 측면에서 고려해볼 만합니다만, 지금 단계에서는 Int?도 충분해 보입니다.core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/bottomsheet/LocationBottomSheet.kt (1)
331-375: SelectedChips에서 칩 전체를 제거 액션으로 쓸지 고민 여지가 있습니다 (선택 사항)지금 구현은:
AssistChip(onClick = {})로 칩 자체 클릭은 아무 동작이 없고,trailingIcon의Modifier.clickable { onRemove(name) }로만 삭제가 동작합니다.디자인 요구사항에 따라 그대로 두셔도 문제는 없지만,
사용자 입장에서는 “칩 전체를 탭해도 제거가 될 것”이라고 기대할 수 있어서, 나중에 여유가 되면 아래 정도는 한 번 고민해볼 만합니다.
- 칩 전체 클릭(
onClick)에서도onRemove(name)을 호출하게 조정, 혹은- 최소한 a11y 관점에서 삭제 액션이 명확히 읽히도록 contentDescription이나 TalkBack 문구를 튜닝.
지금 단계에서는 선택적으로 보완해도 될 수준이라고 생각합니다.
feature/study/src/main/java/com/umcspot/spot/study/register/component/SelectedRegionsSection.kt (1)
31-95: 지역 선택 섹션 전반 구조는 좋고, 간격 헬퍼만 한 번 확인해 보면 좋겠습니다
SelectedRegionsSection이ImmutableList+ 콜백만 받는 순수 컴포저블로 잘 설계되어 있어서 상태 관리 관점에서 깔끔합니다.- 다만
RegionItem의 패딩에서horizontal = screenHeightDp(10.dp)를 사용하고 있는데, 의도한 것이 아니라면 가로 방향에는screenWidthDp를 쓰는 게 더 자연스러워 보입니다(세로 방향에는 이미screenHeightDp를 사용 중이라 일관성이 맞습니다).- 나중에 다국어 대응을 고려하면
"지역","삭제"같은contentDescription과"지역 추가"텍스트도 string 리소스로 분리해 두면 유지보수에 도움이 될 것 같습니다.feature/study/src/main/java/com/umcspot/spot/study/register/screen/StudyIntroduceScreen.kt (1)
44-153: 소개 화면의 이미지 선택 상태를 상위 상태(RegisterStudyState)와 맞추면 이후 확장에 더 유리합니다
StudyIntroduceScreen은description은 파라미터로 주입받아 외부 상태와 잘 연결되어 있지만, 이미지 선택 여부는isImageSelected로 로컬에서만 관리하고 있습니다.- 같은 PR 에서
RegisterStudyState에studyImageUri: String?가 추가된 것을 보면, 실제 이미지 피커 연동 시에는 이 화면도 URI(또는 선택 여부 Boolean)를 인자로 받아 렌더링만 담당하는 구조가 더 자연해 보입니다.- 지금은 UI 목업 단계로 충분해 보이지만, 추후 연동 시에는
isImageSelected를 제거하고studyImageUri기반으로:
- URI 존재 여부에 따라 placeholder vs 이미지 렌더링,
- 필요 시 상위에서 선택/삭제 이벤트를 처리
하는 방향으로 리팩터링을 고려해 보시면 좋겠습니다.feature/study/src/main/java/com/umcspot/spot/study/register/model/RegisterStudyState.kt (1)
7-35: 상태 구조는 명확하고, preference 계열은 도메인 타입으로 감싸면 더 읽기 좋아질 것 같습니다
RegisterStudyState에 필요한 필드들이 한 곳에 잘 모여 있고, 기본값도 지정되어 있어 ViewModel 에서 사용하기 편해 보입니다.- 다만
networkingPreference,goalDurationPreference,discussionPreference등 여러 preference 가 모두Int?로 선언돼 있어, 나중에 1/2/3 값이 각각 무엇을 의미하는지 추적하기가 어려울 수 있습니다.- 여유가 생기면 각 preference 를
enum class나@JvmInline value class같은 도메인 전용 타입으로 감싸 두면, 호출부 가독성과 타입 안정성이 모두 좋아질 것 같습니다.feature/study/src/main/java/com/umcspot/spot/study/register/component/MemberCountSelector.kt (1)
38-156: 인원 선택 드롭다운은 기본 동작은 좋고, 닫힘 동작·옵션 정의를 나중에 다듬어도 좋겠습니다
- 현재는
expanded플래그만으로 열림/닫힘을 제어해서, 셀렉터 바깥을 탭해도 드롭다운이 닫히지 않는 상태입니다. UX 를 생각하면DropdownMenu/ExposedDropdownMenuBox등을 활용해 바깥 클릭이나 뒤로가기 시 닫히도록 개선해 볼 여지가 있습니다.memberOptions가persistentListOf(2, 3, 4, 5)로 고정돼 있는데, 기획 변경 가능성이 있다면 상수로 분리하거나 파라미터로 받아서 재사용성을 높이는 것도 고려해 볼 수 있습니다.- 그 외 레이아웃, 타이포그래피, 색상 사용은 기존 디자인 시스템과 잘 맞아 떨어지는 구현으로 보입니다.
feature/study/src/main/java/com/umcspot/spot/study/register/RegisterStudyScreen.kt (3)
65-72: sideEffect 수집용 LaunchedEffect 키를 단순화하면 더 명확해집니다.현재
LaunchedEffect(viewModel.sideEffect)안에서 다시viewModel.sideEffect.collectLatest { ... }를 호출하고 있어, 키와 실제 수집 대상이 동일한 참조를 가리키는 구조입니다. 기능상 문제는 없지만,LaunchedEffect(Unit)또는LaunchedEffect(viewModel)정도로 키를 단순화하면 “뷰모델 수명 동안 한 번 collect 한다”는 의도가 더 분명해질 것 같습니다.
74-78: Route 레벨modifier와 배경 지정 책임을 정리하면 레이아웃 구조가 더 깔끔해집니다.지금은 Route의
Column이 항상Modifier.fillMaxSize().background(white)를 사용하고, 전달받은modifier는 내부RegisterStudyScreen에만 넘기고 있습니다. 상위에서 레이아웃을 제어해야 할 일이 있다면, Line 74–78에서modifier를 연결해modifier.fillMaxSize().background(...)처럼 사용하고, 내부RegisterStudyScreen쪽에서는 배경 중복을 제거하거나 패딩만 담당하도록 역할을 나누면 구조 이해와 재사용성이 조금 더 좋아질 것 같습니다.Also applies to: 87-94, 118-143
159-197: 마지막 단계의onIntroduceValid콜백은 현재 사용되지 않아 시그니처를 단순화할 수 있습니다.페이지 3에서
StudyIntroduceScreen에onIntroduceValid = { }를 넘기고 있는데, 해당 화면 내부에서는LaunchedEffect(description)로 항상 콜백을 호출하는 구조라 현재는 아무 효과가 없는 상태입니다. 실제로 유효성 상태를 ViewModel 쪽에서 별도로 쓰지 않는다면onIntroduceValid파라미터를 제거해 양쪽 시그니처를 단순화하거나, 반대로 유효성 플래그를 ViewModel에 전달하는 용도로 다시 연결하는 쪽 중 하나로 정리하면 이후 유지보수 시 혼동을 줄일 수 있을 것 같습니다.isStepValid가 이미 description 기반 검증을 하고 있으니, 지금 로직만으로도 기능적으로는 충분해 보입니다.Also applies to: 203-218
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (27)
core/designsystem/build.gradle.kts(1 hunks)core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/bottomsheet/LocationBottomSheet.kt(1 hunks)core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/button/ImageTextButton.kt(4 hunks)core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/button/SocialLoginButton.kt(1 hunks)core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/study/section/ActivityThemeSection.kt(1 hunks)core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/study/section/ActivityTypeSection.kt(1 hunks)core/designsystem/src/main/java/com/umcspot/spot/designsystem/theme/Color.kt(6 hunks)core/designsystem/src/main/res/drawable/dismiss.xml(1 hunks)core/designsystem/src/main/res/drawable/ic_location.xml(1 hunks)feature/main/src/main/java/com/umcspot/spot/landing/LandingScreen.kt(1 hunks)feature/main/src/main/java/com/umcspot/spot/main/MainScreen.kt(2 hunks)feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationBottomSheet.kt(0 hunks)feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyScreen.kt(2 hunks)feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyFilterScreen.kt(5 hunks)feature/study/src/main/java/com/umcspot/spot/study/register/RegisterStudyScreen.kt(3 hunks)feature/study/src/main/java/com/umcspot/spot/study/register/RegisterStudyViewModel.kt(1 hunks)feature/study/src/main/java/com/umcspot/spot/study/register/component/BinaryChoiceRow.kt(1 hunks)feature/study/src/main/java/com/umcspot/spot/study/register/component/FeeInputSection.kt(1 hunks)feature/study/src/main/java/com/umcspot/spot/study/register/component/MemberCountSelector.kt(1 hunks)feature/study/src/main/java/com/umcspot/spot/study/register/component/PriceTextField.kt(1 hunks)feature/study/src/main/java/com/umcspot/spot/study/register/component/SelectedRegionsSection.kt(1 hunks)feature/study/src/main/java/com/umcspot/spot/study/register/component/SelectionChip.kt(1 hunks)feature/study/src/main/java/com/umcspot/spot/study/register/model/RegisterStudyState.kt(1 hunks)feature/study/src/main/java/com/umcspot/spot/study/register/screen/StudyCategoryScreen.kt(1 hunks)feature/study/src/main/java/com/umcspot/spot/study/register/screen/StudyInfoScreen.kt(1 hunks)feature/study/src/main/java/com/umcspot/spot/study/register/screen/StudyIntroduceScreen.kt(1 hunks)feature/study/src/main/java/com/umcspot/spot/study/register/screen/StudyPlaceScreen.kt(1 hunks)
💤 Files with no reviewable changes (1)
- feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationBottomSheet.kt
🧰 Additional context used
🧬 Code graph analysis (18)
feature/study/src/main/java/com/umcspot/spot/study/register/component/FeeInputSection.kt (3)
core/ui/src/main/java/com/umcspot/spot/ui/extension/DimensionExt.kt (1)
screenWidthDp(18-23)feature/study/src/main/java/com/umcspot/spot/study/register/component/SelectionChip.kt (1)
SelectionChip(22-54)feature/study/src/main/java/com/umcspot/spot/study/register/component/PriceTextField.kt (1)
PriceTextField(28-79)
feature/study/src/main/java/com/umcspot/spot/study/register/component/PriceTextField.kt (1)
core/ui/src/main/java/com/umcspot/spot/ui/extension/DimensionExt.kt (2)
screenHeightDp(11-16)screenWidthDp(18-23)
feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyScreen.kt (1)
core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/bottomsheet/LocationBottomSheet.kt (1)
LocationBottomSheet(79-328)
feature/study/src/main/java/com/umcspot/spot/study/register/screen/StudyCategoryScreen.kt (2)
core/ui/src/main/java/com/umcspot/spot/ui/extension/DimensionExt.kt (1)
screenHeightDp(11-16)feature/study/src/main/java/com/umcspot/spot/study/register/component/StudyNameTextField.kt (1)
StudyNameTextField(36-108)
core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/study/section/ActivityThemeSection.kt (2)
core/ui/src/main/java/com/umcspot/spot/ui/extension/DimensionExt.kt (2)
screenWidthDp(18-23)screenHeightDp(11-16)core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/button/ImageTextButton.kt (1)
MultiButton(93-171)
feature/study/src/main/java/com/umcspot/spot/study/register/screen/StudyIntroduceScreen.kt (1)
core/ui/src/main/java/com/umcspot/spot/ui/extension/DimensionExt.kt (2)
screenHeightDp(11-16)screenWidthDp(18-23)
feature/study/src/main/java/com/umcspot/spot/study/register/component/SelectionChip.kt (1)
core/ui/src/main/java/com/umcspot/spot/ui/extension/DimensionExt.kt (1)
screenHeightDp(11-16)
core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/bottomsheet/LocationBottomSheet.kt (1)
core/ui/src/main/java/com/umcspot/spot/ui/extension/DimensionExt.kt (2)
screenHeightDp(11-16)screenWidthDp(18-23)
feature/study/src/main/java/com/umcspot/spot/study/register/screen/StudyInfoScreen.kt (5)
core/ui/src/main/java/com/umcspot/spot/ui/extension/DimensionExt.kt (1)
screenHeightDp(11-16)feature/study/src/main/java/com/umcspot/spot/study/register/component/MemberCountSelector.kt (1)
MemberCountSelector(38-157)feature/study/src/main/java/com/umcspot/spot/study/register/component/FeeInputSection.kt (1)
FeeInputSection(12-49)feature/study/src/main/java/com/umcspot/spot/study/register/RegisterStudyViewModel.kt (2)
onFeeInfoChange(129-131)onPersonalityChange(133-144)feature/study/src/main/java/com/umcspot/spot/study/register/component/BinaryChoiceRow.kt (1)
BinaryChoiceRow(17-55)
feature/study/src/main/java/com/umcspot/spot/study/register/component/BinaryChoiceRow.kt (2)
feature/study/src/main/java/com/umcspot/spot/study/register/component/SelectionChip.kt (1)
SelectionChip(22-54)core/ui/src/main/java/com/umcspot/spot/ui/extension/DimensionExt.kt (2)
screenWidthDp(18-23)screenHeightDp(11-16)
core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/study/section/ActivityTypeSection.kt (2)
core/ui/src/main/java/com/umcspot/spot/ui/extension/DimensionExt.kt (1)
screenWidthDp(18-23)core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/button/ImageTextButton.kt (1)
MultiButton(93-171)
feature/study/src/main/java/com/umcspot/spot/study/register/component/MemberCountSelector.kt (2)
core/ui/src/main/java/com/umcspot/spot/ui/extension/DimensionExt.kt (2)
screenHeightDp(11-16)screenWidthDp(18-23)feature/study/src/main/java/com/umcspot/spot/study/register/RegisterStudyViewModel.kt (1)
onMemberCountChange(125-127)
feature/study/src/main/java/com/umcspot/spot/study/register/RegisterStudyViewModel.kt (1)
core/common/src/main/java/com/umcspot/spot/common/location/Location.kt (1)
searchLocations(54-63)
core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/button/ImageTextButton.kt (2)
core/designsystem/src/main/java/com/umcspot/spot/designsystem/shapes/Shapes.kt (1)
ShapeBox(148-170)core/ui/src/main/java/com/umcspot/spot/ui/extension/DimensionExt.kt (2)
screenWidthDp(18-23)screenHeightDp(11-16)
feature/study/src/main/java/com/umcspot/spot/study/register/screen/StudyPlaceScreen.kt (4)
core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/bottomsheet/LocationBottomSheet.kt (1)
LocationBottomSheet(79-328)core/ui/src/main/java/com/umcspot/spot/ui/extension/DimensionExt.kt (1)
screenHeightDp(11-16)core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/study/section/ActivityTypeSection.kt (1)
ActivityTypeSection(14-40)feature/study/src/main/java/com/umcspot/spot/study/register/component/SelectedRegionsSection.kt (1)
SelectedRegionsSection(31-53)
feature/study/src/main/java/com/umcspot/spot/study/register/RegisterStudyScreen.kt (8)
core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/appBar/AppBar.kt (1)
BackTopBar(113-143)feature/study/src/main/java/com/umcspot/spot/study/register/screen/StudyCategoryScreen.kt (1)
StudyCategoryScreen(20-68)feature/study/src/main/java/com/umcspot/spot/study/register/screen/StudyPlaceScreen.kt (1)
StudyPlaceScreen(22-76)feature/study/src/main/java/com/umcspot/spot/study/register/screen/StudyInfoScreen.kt (1)
StudyInfoScreen(22-104)feature/study/src/main/java/com/umcspot/spot/study/register/screen/StudyIntroduceScreen.kt (1)
StudyIntroduceScreen(43-154)core/ui/src/main/java/com/umcspot/spot/ui/extension/DimensionExt.kt (1)
screenHeightDp(11-16)feature/study/src/main/java/com/umcspot/spot/study/register/RegisterStudyViewModel.kt (1)
isStepValid(150-171)core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/button/SpotActivationButton.kt (1)
SpotActivationButton(24-57)
feature/study/src/main/java/com/umcspot/spot/study/register/component/SelectedRegionsSection.kt (1)
core/ui/src/main/java/com/umcspot/spot/ui/extension/DimensionExt.kt (2)
screenHeightDp(11-16)screenWidthDp(18-23)
feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyFilterScreen.kt (1)
core/ui/src/main/java/com/umcspot/spot/ui/extension/DimensionExt.kt (1)
screenHeightDp(11-16)
🔇 Additional comments (12)
feature/main/src/main/java/com/umcspot/spot/landing/LandingScreen.kt (1)
26-27: 디자인 시스템 리팩토링에 따른 깔끔한 import 경로 업데이트입니다.버튼 컴포넌트들이 새로운 디자인 시스템 구조로 이동하면서 import 경로가 적절히 업데이트되었습니다. 두 버튼 모두 코드에서 정상적으로 사용되고 있으며(83, 86번 라인), 변경사항이 명확합니다.
feature/main/src/main/java/com/umcspot/spot/main/MainScreen.kt (1)
81-81: LGTM!trailing comma를 유지한 것은 Kotlin의 권장 스타일을 따르고 있어 좋습니다.
core/designsystem/build.gradle.kts (1)
13-17: core.common 의존성 추가 방향 괜찮습니다디자인 시스템에서 공통 모듈을 사용하는 구조로 자연스럽고, 기존 도메인 의존성과도 충돌될 부분은 없어 보입니다. 이 변경만으로는 추가 조치 필요 없어 보입니다.
core/designsystem/src/main/res/drawable/dismiss.xml (1)
2-8: dismiss 아이콘 리소스 사이즈/색상 변경 확인20dp/20 viewport로 맞추고 색상도 G500 계열로 통일된 것 같아 디자인 시스템 관점에서 일관성이 좋아 보입니다. 경로나 속성 상으로는 문제 없어 보입니다.
core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/study/section/ActivityTypeSection.kt (1)
14-40: ActivityTypeSection 구성/상태 관리 구조 괜찮습니다
ActivityType.entries를 돌면서MultiButton으로 선택 상태를 표현하고,onSelect(type)만 넘겨서 상태는 상위에서 관리하도록 분리한 구조가 깔끔합니다.spacing도
screenWidthDp(14.dp)로 Figma 비율을 따라가고 있어 디자인 시스템과 잘 맞는 구현으로 보입니다.core/designsystem/src/main/java/com/umcspot/spot/designsystem/theme/Color.kt (1)
67-213: SpotColors.default 필드 추가 및 연결 일관성 좋습니다
SpotColors에default필드를 추가하고,copy(),update(),SpotDayColors()까지 모두 업데이트되어 컬러 상태 객체 일관성이 잘 유지되고 있습니다.
val SpotColors.Default: Color get() = default형태로 기존 Bxxx/Gxxx와 동일한 접근 패턴을 맞춘 것도 좋아 보이고, 현재 코드만 보면 추가 수정 필요해 보이진 않습니다.feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyScreen.kt (1)
154-164: LocationBottomSheet 연동 구조는 자연스럽습니다
showSheet,query,results,selected를 모두viewmodel의StateFlow와 묶어두고,onAddSelected/onRemoveSelected에viewmodel::add/remove를 그대로 넘기는 구조가 깔끔합니다.
시트 열기/닫기(showSheet토글) 책임도 화면 쪽에서만 가지고 있어서, 나중에 다른 화면에서 재사용하기에도 좋아 보입니다.core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/bottomsheet/LocationBottomSheet.kt (1)
79-327: 바텀시트 애니메이션·키보드·선택 제한 로직이 잘 정리되어 있습니다
visible가드 →Dialog→Animatable로 슬라이드 인/아웃을 처리하는 흐름이 깔끔합니다.animateAndDismiss()에서 포커스를 블러하고 키보드를 먼저 내린 뒤 애니메이션 후onDismiss()를 호출해 주는 것도 실제 사용감을 잘 고려한 구조 같습니다.- 최대 3개까지 선택, 초과 시 리스트 항목을
enabled = false+ 회색 텍스트로 표현하는 부분도 UX 상 명확합니다.- 검색 필드 trailing 아이콘으로 포커스를 토글하는 동작과
onFocusChanged에서 키보드를 제어하는 것도 잘 맞물립니다.현재 구조 기준으로 큰 기능 이슈는 없어 보입니다.
feature/study/src/main/java/com/umcspot/spot/study/register/RegisterStudyScreen.kt (4)
16-24: lifecycle‑aware 상태 수집과 DI 구성이 잘 되어 있습니다.
hiltViewModel()으로RegisterStudyViewModel을 주입하고,collectAsStateWithLifecycle()로uiState를 수집하는 구조가 생명주기 안전하고, 화면 재구성에도 안정적으로 동작할 것으로 보입니다.RegisterStudyState를 단일 진실 소스로 내려보내는 방향도 이후 API 연동·검증 로직 확장에 적합합니다.Also applies to: 28-29, 36-38, 50-51
53-64: 단계별 뒤로가기 처리 UX가 자연스럽습니다.
handleBackPress에서 먼저pagerState.currentPage를 확인해 스텝 > 0일 때는 이전 페이지로 애니메이션 이동, 0일 때만onBackClick()을 호출하는 구조가 잘 잡혀 있습니다.BackHandler와BackTopBar가 같은 핸들러를 공유해 하드웨어·툴바 뒤로가기가 일관된 동작을 하는 점도 좋습니다.Also applies to: 81-85
95-111: 카테고리/장소/정보 단계의 상태 전달 및 선택 로직이 의도에 맞게 잘 구성되어 있습니다.
onThemeSelect에서 현재 선택 목록을 복사해 토글하고, 최대 3개까지만 추가되도록 제약을 두는 로직이 명확합니다. 각 Step 화면으로는uiState의 필드를 명시적으로 풀어서 내려주고 있어(예: 선택된 지역, 활동비 정보, 성향 preference 리스트 등) 각 화면이 필요한 데이터만 알게 되는 점도 좋습니다.Also applies to: 159-192
203-209: 단계별 유효성 검사와 활성화 버튼 연동이 일관성 있게 잘 구현되어 있습니다.
isStepValid(pagerState.currentPage)로 현재 스텝의 검증 결과를 계산하고, 이를SpotActivationButton의isEnabled에 그대로 연결해 시각적 상태와 클릭 가능 여부가 일치하도록 한 점이 좋습니다. 뷰모델의isStepValid구현(필수 필드·성향 선택·설명 입력 등)도 UX 요구사항에 맞게 세밀하게 나뉘어 있어 추후 변경에도 대응하기 수월해 보입니다.
| if (dest?.hasRoute(RegisterStudy::class) == true) { | ||
| } | ||
| else if (navigator.showBackTopBar()) { | ||
| val title = when { | ||
| dest?.hasRoute(Alert::class) == true -> "알림" | ||
| dest?.hasRoute(AppliedAlert::class) == true -> "신청한 알림" | ||
| dest?.hasRoute(RecruitingFilter::class) == true -> "모집중인 스터디" | ||
| else -> "" | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
빈 if 블록을 제거하고 조건을 반전시켜 가독성을 개선하세요.
RegisterStudy 라우트에서 상단바를 표시하지 않는 로직은 올바르지만, 빈 if 블록은 코드 스멜입니다. 조건을 반전시켜 더 명확한 의도를 표현할 수 있습니다.
다음 diff를 적용하여 리팩터링하세요:
- if (dest?.hasRoute(RegisterStudy::class) == true) {
- }
- else if (navigator.showBackTopBar()) {
+ if (dest?.hasRoute(RegisterStudy::class) != true && navigator.showBackTopBar()) {
val title = when {
dest?.hasRoute(Alert::class) == true -> "알림"
dest?.hasRoute(AppliedAlert::class) == true -> "신청한 알림"
dest?.hasRoute(RecruitingFilter::class) == true -> "모집중인 스터디"
else -> ""
}
BackTopBar(
title = title,
onBackClick = { navController.popBackStack() },
modifier = Modifier.statusBarsPadding()
)
} else {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (dest?.hasRoute(RegisterStudy::class) == true) { | |
| } | |
| else if (navigator.showBackTopBar()) { | |
| val title = when { | |
| dest?.hasRoute(Alert::class) == true -> "알림" | |
| dest?.hasRoute(AppliedAlert::class) == true -> "신청한 알림" | |
| dest?.hasRoute(RecruitingFilter::class) == true -> "모집중인 스터디" | |
| else -> "" | |
| } | |
| if (dest?.hasRoute(RegisterStudy::class) != true && navigator.showBackTopBar()) { | |
| val title = when { | |
| dest?.hasRoute(Alert::class) == true -> "알림" | |
| dest?.hasRoute(AppliedAlert::class) == true -> "신청한 알림" | |
| dest?.hasRoute(RecruitingFilter::class) == true -> "모집중인 스터디" | |
| else -> "" | |
| } |
🤖 Prompt for AI Agents
In feature/main/src/main/java/com/umcspot/spot/main/MainScreen.kt around lines
47 to 55, there's an empty if (dest?.hasRoute(RegisterStudy::class) == true)
block which is a code smell; invert the condition so you check if
dest?.hasRoute(RegisterStudy::class) != true (or == false) and only then
evaluate navigator.showBackTopBar() and the title when showing the top bar,
removing the empty branch; ensure the logic and title when
Alert/AppliedAlert/RecruitingFilter routes are handled inside the
non-RegisterStudy path.
| val choiceLabels = persistentListOf( | ||
| "네트워킹 중시" to "목표/규율 중시", | ||
| "단기 목표" to "장기 목표", | ||
| "개인 학습 + 함께 토론형" to "공동 학습 + 동시 진행형", | ||
| "학습형" to "토론형", | ||
| "가볍게 + 유연하게" to "규칙적인 + 계획적인" | ||
| ) | ||
|
|
||
| choiceLabels.forEachIndexed { index, (left, right) -> | ||
| BinaryChoiceRow( | ||
| leftText = left, | ||
| rightText = right, | ||
| selectedIndex = preferences[index], | ||
| onSelect = { value -> onPersonalityChange(index, value) } | ||
| ) | ||
| if (index < choiceLabels.lastIndex) { | ||
| Spacer(modifier = Modifier.height(screenHeightDp(12.dp))) | ||
| } | ||
| } |
There was a problem hiding this comment.
preferences 인덱싱 안정성 보강 필요
preferences[index] 접근은 넘겨받은 리스트 길이를 전제하고 있어서, 호출 측에서 요소 수가 모자라면 곧바로 IndexOutOfBoundsException이 터집니다. 공개 컴포저블이라면 방어적으로 getOrNull 등을 써서 NPE 없이 처리하거나, 최소한 require(preferences.size == choiceLabels.size)로 계약을 명시해 주세요. 예시는 다음과 같습니다.
- choiceLabels.forEachIndexed { index, (left, right) ->
- BinaryChoiceRow(
- leftText = left,
- rightText = right,
- selectedIndex = preferences[index],
- onSelect = { value -> onPersonalityChange(index, value) }
- )
+ choiceLabels.forEachIndexed { index, (left, right) ->
+ val selection = preferences.getOrNull(index)
+ BinaryChoiceRow(
+ leftText = left,
+ rightText = right,
+ selectedIndex = selection,
+ onSelect = { value -> onPersonalityChange(index, value) }
+ )
}이렇게 하면 리스트 길이가 달라도 안전하게 동작합니다.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| val choiceLabels = persistentListOf( | |
| "네트워킹 중시" to "목표/규율 중시", | |
| "단기 목표" to "장기 목표", | |
| "개인 학습 + 함께 토론형" to "공동 학습 + 동시 진행형", | |
| "학습형" to "토론형", | |
| "가볍게 + 유연하게" to "규칙적인 + 계획적인" | |
| ) | |
| choiceLabels.forEachIndexed { index, (left, right) -> | |
| BinaryChoiceRow( | |
| leftText = left, | |
| rightText = right, | |
| selectedIndex = preferences[index], | |
| onSelect = { value -> onPersonalityChange(index, value) } | |
| ) | |
| if (index < choiceLabels.lastIndex) { | |
| Spacer(modifier = Modifier.height(screenHeightDp(12.dp))) | |
| } | |
| } | |
| val choiceLabels = persistentListOf( | |
| "네트워킹 중시" to "목표/규율 중시", | |
| "단기 목표" to "장기 목표", | |
| "개인 학습 + 함께 토론형" to "공동 학습 + 동시 진행형", | |
| "학습형" to "토론형", | |
| "가볍게 + 유연하게" to "규칙적인 + 계획적인" | |
| ) | |
| choiceLabels.forEachIndexed { index, (left, right) -> | |
| val selection = preferences.getOrNull(index) | |
| BinaryChoiceRow( | |
| leftText = left, | |
| rightText = right, | |
| selectedIndex = selection, | |
| onSelect = { value -> onPersonalityChange(index, value) } | |
| ) | |
| if (index < choiceLabels.lastIndex) { | |
| Spacer(modifier = Modifier.height(screenHeightDp(12.dp))) | |
| } | |
| } |
🤖 Prompt for AI Agents
In
feature/study/src/main/java/com/umcspot/spot/study/register/screen/StudyInfoScreen.kt
around lines 82 to 100, the code directly indexes preferences[index] which can
throw IndexOutOfBoundsException if the passed list is shorter than choiceLabels;
either enforce the contract at the start with require(preferences.size ==
choiceLabels.size) to fail fast, or make the access defensive by using
preferences.getOrNull(index) and mapping null to a safe default (e.g., 0 or an
explicit unselected state) before passing to BinaryChoiceRow; apply one of these
fixes and ensure onSelect still updates the backing state consistently.
| AnimatedVisibility(visible = activityType == ActivityType.OFFLINE && selectedRegions.isNotEmpty()) { | ||
| Column { | ||
| Spacer(modifier = Modifier.height(screenHeightDp(20.dp))) | ||
| SelectedRegionsSection( | ||
| selectedRegions = selectedRegions, | ||
| onRemoveClick = onRemoveSelected, | ||
| onAddClick = onSheetOpen | ||
| ) | ||
| } | ||
| } |
There was a problem hiding this comment.
오프라인 선택 후 처음에는 지역 추가 버튼이 아예 보이지 않는 상태입니다
AnimatedVisibility의visible조건이activityType == ActivityType.OFFLINE && selectedRegions.isNotEmpty()로 되어 있어, 오프라인을 선택해도 아직 지역이 하나도 없으면SelectedRegionsSection이 전혀 렌더링되지 않습니다.- 이 경우 사용자는
SelectedRegionsSection내부의AddRegionButton을 누를 수 없고, 결과적으로onSheetOpen을 호출할 경로가 없어서 지역을 추가할 수 없습니다. - 오프라인 여부만으로 섹션을 노출하고, 섹션 내부에서 이미
selectedRegions.size < 3조건으로 버튼 노출을 제어하고 있으므로, 아래처럼 조건을 단순화하는 편이 자연해 보입니다.
- AnimatedVisibility(visible = activityType == ActivityType.OFFLINE && selectedRegions.isNotEmpty()) {
+ AnimatedVisibility(visible = activityType == ActivityType.OFFLINE) {
Column {
Spacer(modifier = Modifier.height(screenHeightDp(20.dp)))
SelectedRegionsSection(
selectedRegions = selectedRegions,
onRemoveClick = onRemoveSelected,
onAddClick = onSheetOpen
)
}
}📝 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.
| AnimatedVisibility(visible = activityType == ActivityType.OFFLINE && selectedRegions.isNotEmpty()) { | |
| Column { | |
| Spacer(modifier = Modifier.height(screenHeightDp(20.dp))) | |
| SelectedRegionsSection( | |
| selectedRegions = selectedRegions, | |
| onRemoveClick = onRemoveSelected, | |
| onAddClick = onSheetOpen | |
| ) | |
| } | |
| } | |
| AnimatedVisibility(visible = activityType == ActivityType.OFFLINE) { | |
| Column { | |
| Spacer(modifier = Modifier.height(screenHeightDp(20.dp))) | |
| SelectedRegionsSection( | |
| selectedRegions = selectedRegions, | |
| onRemoveClick = onRemoveSelected, | |
| onAddClick = onSheetOpen | |
| ) | |
| } | |
| } |
🤖 Prompt for AI Agents
In
feature/study/src/main/java/com/umcspot/spot/study/register/screen/StudyPlaceScreen.kt
around lines 65 to 74, the AnimatedVisibility currently only shows
SelectedRegionsSection when activityType == ActivityType.OFFLINE &&
selectedRegions.isNotEmpty(), preventing the AddRegionButton from ever appearing
for a newly selected offline activity; change the visible condition to only
check activityType == ActivityType.OFFLINE so the section renders immediately
when Offline is selected, rely on SelectedRegionsSection's internal
selectedRegions.size < 3 logic to show/hide the AddRegionButton, and keep the
AnimatedVisibility wrapper as-is to preserve the animation behavior.
Related issue 🛠
Work Description 📝
Screenshot 📸
Screen_recording_20251123_183704.mp4
Uncompleted Tasks 😅
PR Point 📌
스터디 등록하기 단순 화면 구현 및 기존 디자인 시스템 일부 수정
기존에 있던 버튼 컴포넌트 수정
트러블 슈팅 💥
갑작스럽게 바뀐 것이 많아 추후 다시 정리해보겠습니다.
Summary by CodeRabbit
릴리스 노트
새로운 기능
리팩토링
✏️ Tip: You can customize this high-level summary in your review settings.