Skip to content

Conversation

@starshape7
Copy link
Collaborator

@starshape7 starshape7 commented Jan 7, 2026

Related issue 🛠

Work Description 📝

  • recruiting & preferLocation study api 연동 및 ui 구현 완료
  • tabBar 사이즈 조정
  • home screen -> registerStudy route 연결

Screenshot 📸

image image image image image image image image image image image image

Uncompleted Tasks 😅

  • tab 사이 간격 및 indicator 사이즈 조정

PR Point 📌

트러블 슈팅 💥

Summary by CodeRabbit

릴리스 노트

  • 새 기능

    • 내 지역 기반 스터디 검색·필터링 화면 및 선호 지역 저장 추가
    • 지역 필터 선택 UI(지역명 통합표시) 및 선호 지역 적용 기능 추가
    • 지역별 스터디 전용 필터 화면(정렬/모집상태/활동비/테마) 추가
  • 개선 사항

    • 스터디 목록 표시 확장(새 필드, 프로필 이미지·조회수 반영) 및 반응형 레이아웃 강화
    • 페이징 로드(무한스크롤) 및 필터 적용/정렬 흐름 개선
  • 기타

    • 선택 가능 지역 수 상향(최대 10개)

✏️ Tip: You can customize this high-level summary in your review settings.

@starshape7 starshape7 linked an issue Jan 7, 2026 that may be closed by this pull request
7 tasks
@coderabbitai
Copy link

coderabbitai bot commented Jan 7, 2026

Walkthrough

위치 기반 스터디 검색/필터를 위한 광범위한 개편: LocationRow 확장, Study API·DTO 재구성(페이지네이션 포함), 선호 지역 저장 API/DTO 추가, 필터/네비게이션/뷰모델 재설계 및 관련 UI 컴포넌트 타입 변경을 포함합니다.

Changes

Cohort / File(s) 변경 요약
Location 모델
core/common/src/main/java/com/umcspot/spot/common/location/Location.kt
LocationRowprovince, district, neighborhood로 분해되고 fullName 추가; 자원 파일을 region_data.tsv(UTF-8)로 로드하도록 파싱/검색 로직 변경
디자인시스템 — 위치 선택 UI
core/designsystem/.../bottomsheet/LocationBottomSheet.kt
selected/onAdd/onRemove 타입을 List<LocationRow> / (LocationRow)->Unit으로 변경; 표시·검색에 fullName 사용; max 선택 3→10; 칩 UI/레이아웃 조정
디자인시스템 — 공통 컴포넌트
core/designsystem/.../appBar/AppBar.kt, .../button/ImageTextButton.kt, .../empty/EmptyAlert.kt, .../study/StudyItem.kt, .../study/section/ActivityThemeSection.kt
반응형 크기·레이아웃 조정, interactionSource 안전 처리, StudyListItem.onClick 시그니처 변경((StudyResult)→Unit), ImmutableListList API 변경 등 UI 내부 스타일·시그니처 조정
모델 변경 — 전역/열거형
core/model/src/main/java/com/umcspot/spot/model/Global.kt
RecruitingStatus enum 추가; RecruitingStudySort, FeeRange 상수명 변경(네이밍 일관화)
Study DTO/매퍼/API 재구성
data/study/src/.../dto/response/StudyResponseDto.kt, mapper/StudyMapper.kt, service/StudyService.kt, datasource/*.kt, datasourceimpl/*.kt, repositoryimpl/StudyRepositoryImpl.kt
StudyResponseDto 구조 변경(studyList→content, hasNext/nextCursor 등), Study 필드명 변경(id,name,description 등), API 경로·쿼리 재구성(/api/studies/recruiting), getPreferLocationStudies 신규 엔드포인트/메서드 추가, 매퍼(toDomain/toDomainList)·레포지토리 시그니처 및 페이징 로직 변경
도메인 — Study 모델/인터페이스
domain/study/src/.../StudyResult.kt, repository/StudyRepository.kt
StudyResult 필드명·구조 변경, StudyResultList에 pagination 필드 추가, 저장소 인터페이스 메서드 시그니처 변경(신규 파라미터 및 regionCodes)
User — 선호 지역 기능 추가
data/user/src/.../datasource/*.kt, datasourceimpl/*.kt, service/UserService.kt, dto/request/UserPreferredRegionRequestDto.kt, dto/response/UserPreferredRegionResponseDto.kt, mapper/UserMapper.kt, repositoryimpl/UserRepositoryImpl.kt, domain/user/.../UserPreferredRegionResult.kt, domain/user/.../UserRepository.kt
선호 지역 저장/조회 API 및 DTO 추가, 매퍼(List.toPreferredRegionRequestDto 등), Repository/데이터소스·서비스 인터페이스 확장 및 UserRepositoryImpl 의존성 변경
Feature: Main 네비게이션/화면 통합
feature/main/.../MainNavHost.kt, MainNavigator.kt, MainScreen.kt, feature/main/build.gradle.kts
preferLocationStudyFilterGraph 통합·네비게이션 추가, 일부 모듈 의존성 제거
Feature: Study — PreferLocation 화면/뷰모델/내비게이션
feature/study/src/.../preferLocation/*
PreferLocation 화면·필터 화면·뷰모델 대규모 추가/재작성: ScrollPosition·페이징·selected LocationRow 관리·load/loadNextPage/applyFilter/searchLocation/syncPreferredRegions 등 주요 기능 추가; 네비게이션 그래프 변경(Hilt back-stack entry 사용)
Feature: Study — Recruiting 필터 통합 및 제거
feature/study/src/.../recruiting/*
RecruitingStudyFilterViewModel 제거 및 필터 상태를 RecruitingStudyViewModel로 통합(드래프트 기반 UI), HeaderRow/SortTypeBottomSheet 시그니처 변경, 페이징·load/loadNextPage·applyFilter 등 도입
Feature: Study — 등록(Registration) 관련 타입 변경
feature/study/src/.../register/*
selectedRegions·onAdd/onRemove 등 타입 String→LocationRow로 변경; RegisterStudyViewModel의 add/removeSelectedRegion 시그니처 및 regionCodes 매핑 로직 변경; SelectedRegionsSection·StudyPlaceScreen 시그니처 업데이트
기타 사소 변경
core/ui/.../GsonExt.kt, data/study/build.gradle.kts, feature/study/build.gradle.kts
import/의존성 정리(주석화된 의존성 추가/제거 등)

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Screen as PreferLocationStudyScreen
    participant VM as PreferLocationStudyViewModel
    participant UserRepo as UserRepository
    participant LocStore as LocationStore
    participant StudyRepo as StudyRepository

    User->>Screen: 화면 진입
    Screen->>VM: load()
    VM->>UserRepo: getUserPreferredRegion()
    UserRepo-->>VM: preferred regions (regionCodes)
    VM->>LocStore: loadLocationData()
    LocStore-->>VM: allLocations
    VM->>StudyRepo: getPreferLocationStudies(regionCodes,cursor,size,filters)
    StudyRepo-->>VM: StudyResultList (content, hasNext, nextCursor)
    VM-->>Screen: uiState 업데이트 (초기 데이터)

    User->>Screen: 스크롤 근접
    Screen->>VM: loadNextPage()
    VM->>StudyRepo: getPreferLocationStudies(cursor=nextCursor)
    StudyRepo-->>VM: 다음 페이지
    VM-->>Screen: uiState 병합 업데이트

    User->>Screen: 위치 추가/제거
    Screen->>VM: addLocation/removeLocation(LocationRow)
    VM->>VM: _selectedRegion 업데이트
    VM->>UserRepo: syncPreferredRegions(selected regionCodes)
    VM->>StudyRepo: getPreferLocationStudies(updated regionCodes)
    VM-->>Screen: uiState 갱신
Loading
sequenceDiagram
    actor User
    participant Screen as RecruitingStudyScreen
    participant VM as RecruitingStudyViewModel
    participant StudyRepo as StudyRepository

    User->>Screen: 화면 진입
    Screen->>VM: load()
    VM->>StudyRepo: getRecruitingStudies(feeCategory,categories,isOnline,sortBy,cursor,size)
    StudyRepo-->>VM: StudyResultList
    VM-->>Screen: uiState 업데이트

    User->>Screen: 필터 열기
    Screen->>Screen: RecruitingStudyFilterScreen 표시
    User->>Screen: 필터 적용
    Screen->>VM: applyFilter(status,fee,themes)
    VM->>StudyRepo: getRecruitingStudies(updated filters)
    StudyRepo-->>VM: 필터된 결과
    VM-->>Screen: uiState 업데이트
Loading

🎯 Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

🏷️ Suggested Labels

🐶 종훈, 🍒 [FEAT]

Poem

나는 토끼, 지도를 들고 폴짝—
province, district, neighborhood를 모아 노래하네.
필터를 누르면 페이지가 춤추고,
선호지역은 저장되어 다시 불러오네.
킁킁, 코드는 정리됐고 UI는 반짝인다! 🐰✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.51% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 주요 변경사항을 명확하게 설명하고 있습니다. 'Feat/#6 studyscreen구현'은 recruiting 및 preferLocation study 화면 구현이라는 주요 목표를 반영합니다.
Description check ✅ Passed PR 설명이 필수 섹션(Related issue, Work Description, Screenshot)을 포함하고 있으며, API 연동 및 UI 구현 내용을 명확히 기술하고 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a 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 (1)
core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/empty/EmptyAlert.kt (1)

55-60: h3 타이포그래피 폰트 크기 검증 필요: 의도하지 않은 시각적 변경 가능성

SpotTheme.typography.h3fontSize = 18.sp로 정의되어 있으나, 제거된 명시적 fontSize 오버라이드가 30.sp였다면 이는 40% 축소되는 중대한 시각적 변경입니다. 이 폰트 크기 감소가 의도된 것인지 확인이 필수적입니다. 의도하지 않은 변경이라면 수정이 필요합니다.

🤖 Fix all issues with AI agents
In
@core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/appBar/AppBar.kt:
- Line 87: The notification icon in AppBar.kt uses a fixed Modifier.size(24.dp)
causing inconsistent sizes across screen widths; change the modifier for the
notification Icon (the same element that currently has Modifier.size(24.dp)) to
use the responsive helper used by the search
icon—Modifier.size(screenWidthDp(24.dp))—so both icons use screenWidthDp(24.dp)
for consistent, responsive sizing.

In
@core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/button/ImageTextButton.kt:
- Around line 108-109: The nullable fallback on interactionSource is dead code
because the parameter is non-nullable; either make the parameter nullable
(change interactionSource: MutableInteractionSource to interactionSource:
MutableInteractionSource?) and keep the remember fallback for safety, or remove
the unnecessary fallback and simplify to val internalSource = interactionSource
and keep using internalSource.collectIsPressedAsState() (also update any other
usages like the later reference around the line with collectIsPressedAsState
accordingly).

In
@data/study/src/main/java/com/umcspot/spot/study/datasource/StudyDataSource.kt:
- Around line 16-24: The interface method getPreferLocationStudies in
StudyDataSource is missing the isOnline parameter and uses sortType while
StudyService expects sortBy; update the StudyDataSource signature (and any
implementing classes) to include the Boolean? isOnline parameter and rename
sortType to sortBy (or vice versa to match StudyService) so the parameter names
and types match StudyService.getPreferLocationStudies, then adjust all
implementations and callers of getPreferLocationStudies to pass the new isOnline
argument and the renamed sortBy parameter.

In
@data/study/src/main/java/com/umcspot/spot/study/dto/response/StudyResponseDto.kt:
- Around line 24-25: The DTO's id is a String but Study.toDomain() currently
does this.id.toLong(), which can throw NumberFormatException for non-numeric API
values; change the conversion to use safe parsing (e.g., this.id.toLongOrNull())
inside Study.toDomain() in StudyMapper.kt and handle the null case explicitly
(either throw a clear IllegalArgumentException/DomainMappingException with
context including the offending id, or return a nullable domain/optional result
as your domain contract requires) so malformed ids no longer crash the app.

In
@data/study/src/main/java/com/umcspot/spot/study/repositoryimpl/StudyRepositoryImpl.kt:
- Around line 31-37: The getRecommendStudies() implementation is calling
studyDataSource.getPopularStudies() instead of the recommend endpoint; update
the call inside StudyRepositoryImpl.getRecommendStudies() to invoke
studyDataSource.getRecommendStudies() (keep the runCatching/recoverCatching flow
and the fallback to setRecommendDummies()), ensuring the returned
response.result is still mapped via toDomainList().

In
@data/user/src/main/java/com/umcspot/spot/user/datasourceimpl/UserDataSourceImpl.kt:
- Around line 37-42: Remove the debug Log.d call left in setUserPreferredRegion
and eliminate the unnecessary intermediate variable res: in
UserDataSourceImpl.override suspend fun setUserPreferredRegion(regions:
List<String>): NullResultResponse remove the Log.d(...) line and return the
result of
userService.setUserPreferredRegion(regions.toPreferredRegionRequestDto())
directly; if logging is needed, replace the debug Android log with the project's
proper logging framework instead of Log.d.

In
@data/user/src/main/java/com/umcspot/spot/user/dto/request/UserPreferredRegionRequestDto.kt:
- Line 3: The file imports StudyTheme but it is unused; remove the line "import
com.umcspot.spot.model.StudyTheme" from UserPreferredRegionRequestDto.kt to
eliminate the dead import and then recompile/inspect for any other unused
imports or lint issues.

In
@data/user/src/main/java/com/umcspot/spot/user/dto/response/UserPreferredRegionResponseDto.kt:
- Line 4: UserPreferredRegionResponseDto 클래스에 불필요한 import가 남아 있습니다;
com.umcspot.spot.model.WeatherType import를 제거하여 사용되지 않는 의존성을 정리하세요
(UserPreferredRegionResponseDto 파일에서 import com.umcspot.spot.model.WeatherType
줄을 삭제).

In
@data/user/src/main/java/com/umcspot/spot/user/repositoryimpl/UserRepositoryImpl.kt:
- Around line 16-22: The fallback UserResult(name = "123") in
UserRepositoryImpl.getUserName is a hardcoded placeholder; replace the
recoverCatching block so we don't return that magic value — either propagate the
original error into the Result (e.g., remove recoverCatching so runCatching
yields a failure, or explicitly return Result.failure(it) inside the recover
handler using the exception variable `it`), or replace it with a meaningful
default sourced from config or a well-defined constant (e.g., empty name or
nullable field) instead of "123"; update references to userDataSource.getUser(),
getUserName and recoverCatching accordingly.

In
@feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyViewModel.kt:
- Around line 91-120: loadNextPage() checks _isLoadingMore but never sets it, so
set _isLoadingMore.value = true at the start of the load and ensure it is reset
to false when the request finishes or fails; modify the block in loadNextPage
(around viewModelScope.launch / runCatching) to set _isLoadingMore.value = true
before calling studyRepository.getRecruitingStudies and then set
_isLoadingMore.value = false in a finally/complete path (or in both onSuccess
and onFailure) so duplicate page loads are prevented.
- Around line 84-87: In RecruitingStudyViewModel's load() function the onFailure
block only logs the exception (Log.e("RecruitingStudyViewModel",
"loadFristError", e)) and does not update the UI state; replace the
commented-out intent with an actual update to the ViewModel's UI state (e.g.,
set the backing state to UiState.Failure using the exception message or
toString) inside the onFailure lambda so the UI observes the failure and can
react accordingly.

In
@feature/study/src/main/java/com/umcspot/spot/study/register/RegisterStudyViewModel.kt:
- Around line 195-197: Bug: the lambda parameter `it` in
currentState.selectedRegions.mapNotNull is shadowed by the inner find's `it`,
causing the comparison `it.fullName == it.fullName` to always be true and
returning the wrong location code. Fix: rename the outer lambda parameter (e.g.,
region) and the inner find parameter (e.g., location) so the comparison uses
region.fullName == location.fullName and return location.code; update the
expression using currentState.selectedRegions.mapNotNull and allLocations.find
accordingly to avoid shadowing.
🧹 Nitpick comments (19)
core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/appBar/AppBar.kt (1)

164-243: SearchTopBar에도 반응형 크기 적용을 고려해보세요.

AppBarHomeBackTopBar는 반응형 크기로 업데이트되었지만, SearchTopBar는 여전히 고정된 dp 값을 사용하고 있습니다. 디자인 시스템의 일관성을 위해 SearchTopBar에도 screenHeightDpscreenWidthDp를 적용하는 것을 권장합니다.

🔎 적용 예시

주요 변경 지점:

  • Line 182: .padding(vertical = 6.dp).padding(vertical = screenHeightDp(6.dp))
  • Line 190: .size(25.dp).size(screenWidthDp(25.dp))
  • Line 194: .width(8.dp).width(screenWidthDp(8.dp))
  • Line 207: .height(44.dp).height(screenHeightDp(44.dp))
  • Line 210: .padding(horizontal = 14.dp).padding(horizontal = screenWidthDp(14.dp))
  • Line 229: .size(36.dp).size(screenWidthDp(36.dp))
  • Line 235: .size(32.dp).size(screenWidthDp(32.dp))
core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/study/section/ActivityThemeSection.kt (1)

18-18: 미사용 import 제거 필요

ImmutableListList로 변경한 후 이 import가 더 이상 사용되지 않는 것으로 보입니다. 파일 내에서 ImmutableList를 사용하는 다른 곳이 없다면 이 import를 제거해주세요.

data/user/src/main/java/com/umcspot/spot/user/dto/response/UserPreferredRegionResponseDto.kt (1)

8-8: @OptIn(ExperimentalSerializationApi::class)을 사용하여 @SuppressLint 제거하기

@SuppressLint("UnsafeOptInUsageError")는 경고를 숨기는 방식이므로, 대신 @OptIn(ExperimentalSerializationApi::class) 어노테이션을 사용해야 합니다. 이는 kotlinx.serialization의 실험용 API를 명시적으로 opt-in하는 올바른 방식입니다.

이 패턴은 프로젝트의 27개 DTO 파일에서 사용 중입니다. 다음과 같이 수정하세요:

@OptIn(ExperimentalSerializationApi::class)
@Serializable
data class UserPreferredRegionResponseDto(...)

모듈 레벨에서 통합 설정하거나, 영향받는 모든 DTO 파일에 어노테이션을 추가하시기 바랍니다.

domain/study/src/main/java/com/umcspot/spot/study/repository/StudyRepository.kt (1)

15-32: 파라미터 네이밍 불일치가 있습니다.

getRecruitingStudies에서는 feeCategory: FeeRange?를 사용하고, getPreferLocationStudies에서는 feeRange: FeeRange?를 사용합니다. 동일한 타입에 대해 일관된 네이밍을 사용하면 API 사용자의 혼란을 줄일 수 있습니다.

🔎 네이밍 일관성 개선 제안
 suspend fun getPreferLocationStudies(
     recruitingStatus : RecruitingStatus?,
-    feeRange: FeeRange?,
+    feeCategory: FeeRange?,
     categories: List<String>?,
     sortBy: RecruitingStudySort?,
     cursor: Long?,
     size: Int,
     regionCodes : List<String>?
 ): Result<StudyResultList>
data/study/src/main/java/com/umcspot/spot/study/datasourceimpl/StudyDataSourceImpl.kt (1)

44-44: 파라미터명 불일치: sortType vs sortBy

메서드 파라미터명이 sortType으로 되어 있지만, 다른 파일들에서는 sortBy를 사용합니다. 일관성을 위해 sortBy로 변경하는 것을 권장합니다.

🔎 파라미터명 일관성 개선
 override suspend fun getPreferLocationStudies(
     recruitingStatus: RecruitingStatus?,
     feeCategory: FeeRange?,
     categories: List<String>?,
-    sortType: RecruitingStudySort?,
+    sortBy: RecruitingStudySort?,
     cursor: Long?,
     size: Int,
     regionCodes: List<String>?
 ): BaseResponse<StudyResponseDto> =
-    studyService.getPreferLocationStudies(recruitingStatus, feeCategory, categories, null, sortType, cursor, size, regionCodes)
+    studyService.getPreferLocationStudies(recruitingStatus, feeCategory, categories, null, sortBy, cursor, size, regionCodes)
data/study/src/main/java/com/umcspot/spot/study/service/StudyService.kt (1)

3-7: 사용하지 않는 import 정리

ActivityTypeStudyTheme이 import 되어 있지만 이 파일에서 사용되지 않습니다.

🔎 제안된 수정
 package com.umcspot.spot.study.service

-import com.umcspot.spot.model.ActivityType
 import com.umcspot.spot.model.FeeRange
 import com.umcspot.spot.model.RecruitingStatus
 import com.umcspot.spot.model.RecruitingStudySort
-import com.umcspot.spot.model.StudyTheme
feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyFilterScreen.kt (2)

166-166: Spacer에서 screenHeightDp 사용 불일치

일부 Spacer는 screenHeightDp를 사용하고 (Line 159, 174, 189) 일부는 사용하지 않습니다 (Line 166, 181, 197, 203). 일관된 반응형 레이아웃을 위해 통일해주세요.

🔎 제안된 수정
-                Spacer(modifier = Modifier.height(53.dp))
+                Spacer(modifier = Modifier.height(screenHeightDp(53.dp)))

Line 181, 197, 203에도 동일하게 적용해주세요.

Also applies to: 181-181, 197-197, 203-203


253-296: RecruitingStudyFilterScreen과 코드 중복

ActivityFeeSectionResetFilterText 컴포저블이 RecruitingStudyFilterScreen.kt에 이미 존재합니다 (relevant_code_snippets 참조). 공통 모듈로 추출하여 재사용하는 것을 권장합니다.

core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/bottomsheet/LocationBottomSheet.kt (1)

354-391: 칩 높이가 텍스트를 자를 수 있음

칩의 높이가 screenHeightDp(17.dp)로 고정되어 있는데, 이는 작은 디바이스에서 텍스트가 잘리거나 수직 패딩이 부족할 수 있습니다. wrapContentHeight()를 사용하거나 최소 높이를 설정하는 것을 고려해주세요.

🔎 제안된 수정
             Box(
                 modifier = Modifier
                     .wrapContentWidth()
-                    .height(screenHeightDp(17.dp))
+                    .height(screenHeightDp(23.dp))
                     .clip(SpotShapes.Hard)
feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyViewModel.kt (1)

36-36: scrollPositionvar로 노출하는 것은 캡슐화 위반

scrollPosition이 public var로 선언되어 외부에서 직접 수정이 가능합니다. ViewModel의 상태는 불변으로 관리하거나 private setter를 사용하는 것이 권장됩니다.

feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyScreen.kt (2)

238-261: indexOf 사용으로 인한 성능 문제

studies.indexOf(item)을 각 아이템마다 호출하면 O(n²) 시간 복잡도가 됩니다. itemsIndexed를 사용하여 인덱스를 직접 받는 것이 효율적입니다.

또한 Spacerpadding modifier를 사용하는 것은 올바르지 않습니다. height를 직접 사용해주세요.

🔎 제안된 수정
-        items(
-            items = studies,
-            key = { it.id }
-        ) { item ->
-            Spacer(Modifier.padding(screenHeightDp(5.dp)))
+        itemsIndexed(
+            items = studies,
+            key = { _, item -> item.id }
+        ) { index, item ->
+            Spacer(Modifier.height(screenHeightDp(5.dp)))

             StudyListItem(
                 item = item,
                 modifier = Modifier
                     .fillMaxWidth(),
                 onClick = { onItemClick(item) }
             )

-            if(studies.indexOf(item) != studies.lastIndex) {
-                Spacer(Modifier.padding(screenHeightDp(5.dp)))
+            if(index != studies.lastIndex) {
+                Spacer(Modifier.height(screenHeightDp(5.dp)))

                 HorizontalDivider(
                     modifier = Modifier
                         .fillMaxWidth(),
                     color = SpotTheme.colors.G300,
                     thickness = 1.dp
                 )
             }
         }

178-212: Loading 상태 표시 개선 가능

현재 UiState.Loading 상태에서 SpotSpinner가 전체 화면을 덮지만, RecruitingStudyScreenContent도 동시에 렌더링됩니다. 빈 리스트로 렌더링되므로 문제는 없지만, when 블록 내에서 Success 케이스만 content를 렌더링하도록 구조화하면 더 명확합니다.

feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyViewModel.kt (2)

149-152: 오류 시 UiState.Failure 사용을 고려하세요.

현재 onFailure에서 UiState.Empty로 설정하여 실제 오류 정보가 사용자에게 전달되지 않습니다. 디버깅 및 사용자 피드백을 위해 UiState.Failure를 사용하는 것이 좋습니다.

🔎 제안된 수정
             }.onFailure { e ->
                 Log.e("PreferLocationStudyViewModel", "load error", e)
-                _uiState.update { it.copy(data = UiState.Empty) }
+                _uiState.update { it.copy(data = UiState.Failure(e.message ?: "Unknown error")) }
             }

167-174: 중복 네트워크 호출 및 !! 사용 개선 필요

  1. userRepository.getUserPreferredRegion()load()에서 이미 호출되었으므로, 캐시된 _selectedRegion.value를 재사용하면 불필요한 네트워크 요청을 줄일 수 있습니다.
  2. Line 174의 currentRegionCode!!는 NPE 위험이 있습니다.
🔎 제안된 수정
         viewModelScope.launch {
             _isLoadingMore.value = true

             runCatching {
-                val preferredCodes = userRepository.getUserPreferredRegion()
-                    .getOrThrow()
-                    .regionCodes
-                    .map { it.trim() }
+                val preferredCodes = _selectedRegion.value.map { it.code }

                 val regionCodesForRequest =
-                    if (currentRegionCode.isNullOrBlank()) preferredCodes else listOf(currentRegionCode!!)
+                    if (currentRegionCode.isNullOrBlank()) preferredCodes else listOf(currentRegionCode)
feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyFilterScreen.kt (1)

75-75: 주석 처리된 코드 정리 필요

acceptEnabledtrue로 하드코딩되어 있고, 원래 로직이 주석 처리되어 있습니다. 의도적인 변경이라면 주석을 제거하고, 그렇지 않다면 원래 로직을 복원하세요.

🔎 제안된 수정
-    val acceptEnabled = true // draftActivity != null || draftFee != null || draftThemes.isNotEmpty()
+    val acceptEnabled = true
feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyScreen.kt (2)

120-132: 중복된 LaunchedEffect 블록

Lines 121-124와 126-132가 거의 동일한 로직을 수행합니다. 하나의 LaunchedEffect로 통합하는 것이 좋습니다.

🔎 제안된 수정
-    // 선택 칩 변화 시 탭 인덱스 보정
-    LaunchedEffect(selected.size) {
-        val maxIdx = (1 + selected.size) - 1
-        if (selectedTab > maxIdx) selectedTab = maxIdx
-    }
-
-    LaunchedEffect(selected.size) {
-        val maxIdx = (1 + selected.size) - 1
-        if (selectedTab > maxIdx) {
-            selectedTab = maxIdx
-            viewmodel.selectTab(selectedTab)
-        }
-    }
+    // 선택 칩 변화 시 탭 인덱스 보정
+    LaunchedEffect(selected.size) {
+        val maxIdx = selected.size // (1 + selected.size) - 1 = selected.size
+        if (selectedTab > maxIdx) {
+            selectedTab = maxIdx
+            viewmodel.selectTab(selectedTab)
+        }
+    }

290-313: indexOf 대신 itemsIndexed 사용 권장

items.indexOf(item)은 각 아이템마다 O(n) 복잡도로 전체 O(n²)가 됩니다. itemsIndexed를 사용하면 O(1)로 개선됩니다.

🔎 제안된 수정
+import androidx.compose.foundation.lazy.itemsIndexed

-    items(
-        items = items,
-        key = { it.id }
-    ) { item ->
+    itemsIndexed(
+        items = items,
+        key = { _, item -> item.id }
+    ) { index, item ->
         Spacer(Modifier.padding(screenHeightDp(5.dp)))

         StudyListItem(
             item = item,
             modifier = Modifier
                 .fillMaxWidth(),
             onClick = { onItemClick(item) }
         )

-        if(items.indexOf(item) != items.lastIndex) {
+        if (index != items.lastIndex) {
             Spacer(Modifier.padding(screenHeightDp(5.dp)))

             HorizontalDivider(
                 modifier = Modifier
                     .fillMaxWidth(),
                 color = SpotTheme.colors.G300,
                 thickness = 1.dp
             )
         }
     }
core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/study/StudyItem.kt (2)

52-52: 반응형 치수 사용의 일관성 개선 권장

대부분의 치수에 screenWidthDp/screenHeightDp를 사용하고 있으나, 여기서는 고정값 13.dp를 사용하고 있습니다. 일관성을 위해 screenWidthDp(13.dp)를 고려해보세요.

🔎 일관성을 위한 수정 제안
-            horizontalArrangement = Arrangement.spacedBy(13.dp),
+            horizontalArrangement = Arrangement.spacedBy(screenWidthDp(13.dp)),

108-108: 코드 중복: cap 함수

cap 함수가 core/model/src/main/java/com/umcspot/spot/model/DataFormatter.kt에 이미 존재합니다. 중복을 제거하고 기존 함수를 사용하는 것을 권장합니다.

🔎 중복 제거 제안

파일 상단에 import를 추가하고:

+import com.umcspot.spot.model.cap

그리고 로컬 함수를 제거:

-    fun cap(n: Int) = if (n >= 1000) "999+" else n.toString()
     val display = if (count1 != 0) "${cap(count1)} / ${cap(count2)}" else cap(count2)
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bac9159 and 22f6f0c.

⛔ Files ignored due to path filters (1)
  • core/common/src/main/assets/region_data.tsv is excluded by !**/*.tsv
📒 Files selected for processing (47)
  • core/common/src/main/java/com/umcspot/spot/common/location/Location.kt
  • core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/appBar/AppBar.kt
  • core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/bottomsheet/LocationBottomSheet.kt
  • core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/button/ImageTextButton.kt
  • core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/empty/EmptyAlert.kt
  • core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/study/StudyItem.kt
  • core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/study/section/ActivityThemeSection.kt
  • core/model/src/main/java/com/umcspot/spot/model/Global.kt
  • core/ui/src/main/java/com/umcspot/spot/ui/extension/GsonExt.kt
  • data/study/build.gradle.kts
  • data/study/src/main/java/com/umcspot/spot/study/datasource/StudyDataSource.kt
  • data/study/src/main/java/com/umcspot/spot/study/datasourceimpl/StudyDataSourceImpl.kt
  • data/study/src/main/java/com/umcspot/spot/study/dto/response/StudyResponseDto.kt
  • data/study/src/main/java/com/umcspot/spot/study/mapper/StudyMapper.kt
  • data/study/src/main/java/com/umcspot/spot/study/repositoryimpl/StudyRepositoryImpl.kt
  • data/study/src/main/java/com/umcspot/spot/study/service/StudyService.kt
  • data/user/src/main/java/com/umcspot/spot/user/datasource/UserDataSource.kt
  • data/user/src/main/java/com/umcspot/spot/user/datasourceimpl/UserDataSourceImpl.kt
  • data/user/src/main/java/com/umcspot/spot/user/dto/request/UserPreferredRegionRequestDto.kt
  • data/user/src/main/java/com/umcspot/spot/user/dto/response/UserPreferredRegionResponseDto.kt
  • data/user/src/main/java/com/umcspot/spot/user/mapper/UserMapper.kt
  • data/user/src/main/java/com/umcspot/spot/user/repositoryimpl/UserRepositoryImpl.kt
  • data/user/src/main/java/com/umcspot/spot/user/service/UserService.kt
  • domain/study/src/main/java/com/umcspot/spot/study/model/StudyResult.kt
  • domain/study/src/main/java/com/umcspot/spot/study/repository/StudyRepository.kt
  • domain/user/src/main/java/com/umcspot/spot/user/model/UserPreferredRegionResult.kt
  • domain/user/src/main/java/com/umcspot/spot/user/repository/UserRepository.kt
  • feature/main/build.gradle.kts
  • feature/main/src/main/java/com/umcspot/spot/main/MainNavHost.kt
  • feature/main/src/main/java/com/umcspot/spot/main/MainNavigator.kt
  • feature/main/src/main/java/com/umcspot/spot/main/MainScreen.kt
  • feature/study/build.gradle.kts
  • feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyFilterScreen.kt
  • feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyScreen.kt
  • feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyViewModel.kt
  • feature/study/src/main/java/com/umcspot/spot/study/preferLocation/navigation/PreferLocationStudyFilterNavigation.kt
  • feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyFilterScreen.kt
  • feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyFilterViewmodel.kt
  • feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyScreen.kt
  • feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyViewModel.kt
  • feature/study/src/main/java/com/umcspot/spot/study/recruiting/navigation/RecruitingStudyFilterNavigation.kt
  • feature/study/src/main/java/com/umcspot/spot/study/recruiting/navigation/RecruitingStudyNavigation.kt
  • feature/study/src/main/java/com/umcspot/spot/study/register/RegisterStudyScreen.kt
  • feature/study/src/main/java/com/umcspot/spot/study/register/RegisterStudyViewModel.kt
  • feature/study/src/main/java/com/umcspot/spot/study/register/component/SelectedRegionsSection.kt
  • feature/study/src/main/java/com/umcspot/spot/study/register/model/RegisterStudyState.kt
  • feature/study/src/main/java/com/umcspot/spot/study/register/screen/StudyPlaceScreen.kt
💤 Files with no reviewable changes (2)
  • feature/main/build.gradle.kts
  • feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyFilterViewmodel.kt
🧰 Additional context used
🧬 Code graph analysis (9)
feature/study/src/main/java/com/umcspot/spot/study/preferLocation/navigation/PreferLocationStudyFilterNavigation.kt (2)
feature/main/src/main/java/com/umcspot/spot/main/MainNavigator.kt (1)
  • navigate (64-83)
feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyFilterScreen.kt (1)
  • PreferLocationStudyFilterScreen (51-109)
feature/main/src/main/java/com/umcspot/spot/main/MainNavHost.kt (2)
feature/study/src/main/java/com/umcspot/spot/study/preferLocation/navigation/PreferLocationStudyNavigation.kt (1)
  • preferLocationStudyGraph (17-31)
feature/study/src/main/java/com/umcspot/spot/study/preferLocation/navigation/PreferLocationStudyFilterNavigation.kt (1)
  • preferLocationStudyFilterGraph (19-38)
core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/study/StudyItem.kt (3)
core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/button/TextButton.kt (1)
  • ClickSurface (203-244)
core/ui/src/main/java/com/umcspot/spot/ui/extension/DimensionExt.kt (2)
  • screenWidthDp (18-23)
  • screenHeightDp (11-16)
core/model/src/main/java/com/umcspot/spot/model/DataFormatter.kt (1)
  • cap (26-26)
feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyFilterScreen.kt (3)
feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyFilterScreen.kt (3)
  • RecruitingStudyFilterScreenContent (112-207)
  • ActivityFeeSection (231-254)
  • ResetFilterText (256-274)
core/ui/src/main/java/com/umcspot/spot/ui/extension/DimensionExt.kt (2)
  • screenHeightDp (11-16)
  • screenWidthDp (18-23)
core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/button/TextButton.kt (1)
  • TextButton (247-299)
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)
  • screenWidthDp (18-23)
  • screenHeightDp (11-16)
core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/appBar/AppBar.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/navigation/RecruitingStudyFilterNavigation.kt (1)
feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyFilterScreen.kt (1)
  • RecruitingStudyFilterScreen (55-110)
feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyFilterScreen.kt (3)
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/TextButton.kt (1)
  • TextButton (247-299)
core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/button/ImageTextButton.kt (1)
  • MultiButton (95-176)
feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyViewModel.kt (2)
feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyViewModel.kt (1)
  • load (66-89)
core/common/src/main/java/com/umcspot/spot/common/location/Location.kt (2)
  • load (25-70)
  • searchLocations (73-83)
🔇 Additional comments (67)
feature/study/build.gradle.kts (1)

11-11: 사용자 도메인 의존성 추가가 적절합니다.

domain.user 의존성 추가는 이번 PR에서 구현하는 사용자 선호 지역 기반 스터디 조회 기능(preferLocationStudy)과 사용자 지역 데이터 관리를 지원하기 위해 필요합니다. 변경 사항이 PR의 목적과 잘 부합합니다.

core/ui/src/main/java/com/umcspot/spot/ui/extension/GsonExt.kt (1)

3-3: LGTM! import 순서 정리

Gson import를 상단으로 이동한 것은 코드 정리 차원의 변경으로, 기능적 영향이 없습니다.

data/study/build.gradle.kts (1)

11-11: 주석 처리된 의존성 제거는 적절한 조치입니다.

data/study 모듈의 모든 파일을 검토한 결과, data.user 패키지를 import하거나 사용하는 코드가 없습니다. 현재 active 의존성인 domain.study와 core.ui만으로 컴파일이 완벽하게 진행됩니다. 따라서 주석 처리된 의존성을 제거하는 것이 맞습니다.

core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/empty/EmptyAlert.kt (1)

69-69: 주석 제거가 적절합니다.

코드 정리가 잘 되었습니다. content() 람다 파라미터 자체가 버튼 등을 추가하는 목적을 명확히 하므로, 인라인 주석 제거는 코드를 더 깔끔하게 만듭니다.

core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/button/ImageTextButton.kt (1)

125-125: interactionSource 대신 internalSource 사용 확인 필요

Line 109에서는 internalSource로부터 pressed 상태를 수집하지만, 여기서는 원본 interactionSource를 사용하고 있습니다. 현재는 두 변수가 동일한 객체를 참조하지만, 향후 interactionSource 파라미터가 nullable로 변경될 경우 일관성 문제가 발생할 수 있습니다.

core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/appBar/AppBar.kt (2)

54-76: 반응형 크기 적용이 잘 구현되었습니다.

AppBarHome의 대부분의 요소에 screenHeightDpscreenWidthDp가 적절하게 적용되어 화면 크기에 따른 반응형 레이아웃이 구현되었습니다.


127-144: BackTopBar 반응형 크기 적용이 일관되게 이루어졌습니다.

BackTopBar 컴포넌트의 모든 요소에서 반응형 크기가 올바르게 적용되었습니다:

  • 높이: screenHeightDp(53.dp)
  • 패딩: screenWidthDp(17.dp)
  • 아이콘: screenWidthDp(20.dp)
  • 간격: screenWidthDp(10.dp)

Line 144의 Spacer 너비는 screenWidthDp(10.dp)로 설정되어 있으며, 이는 반응형 설계 패턴에 일관되게 적용된 의도된 디자인입니다.

Likely an incorrect or invalid review comment.

core/model/src/main/java/com/umcspot/spot/model/Global.kt (3)

76-82: 새로운 RecruitingStatus enum이 잘 구현되었습니다

모집 상태를 타입 안전한 enum으로 정의한 것은 좋은 설계입니다. BEFORE 상태가 주석 처리되어 있는데, 향후 필요할 경우를 대비해 유지하는 것이 적절해 보입니다.


36-46: FeeRange enum 상수명이 명확하게 개선되었습니다

FeeRange의 상수명 변경 (예: ABOUT10KFROM_10K_TO_20K)이 가독성을 크게 향상시키며, 코드베이스에서 이전 상수명이 완전히 업데이트되었습니다. 특별한 추가 작업은 필요하지 않습니다.


20-24: RecruitingStudySort 상수명 변경은 올바르게 구현되었습니다

enum 상수명이 다음과 같이 변경되었으며, 모든 참조가 정상적으로 업데이트되었습니다:

  • LATESTRECENT
  • VIEWHITS
  • LIKELIKES

코드베이스의 모든 사용 지점(ViewModel, Screen, Repository 등)에서 새로운 상수명을 올바르게 참조하고 있으며, 구식 상수명에 대한 참조는 없습니다. UI 레이블도 유지되어 변경 없습니다.

domain/user/src/main/java/com/umcspot/spot/user/model/UserPreferredRegionResult.kt (1)

4-7: 간결하고 적절한 데이터 클래스 구현

사용자의 선호 지역 정보를 담는 불변 데이터 클래스가 잘 정의되었습니다. 구조가 명확하고 사용하기 쉽습니다.

feature/study/src/main/java/com/umcspot/spot/study/recruiting/navigation/RecruitingStudyNavigation.kt (1)

1-34: Import 정리가 적절합니다

사용하지 않는 import를 제거하고 필요한 import를 명시적으로 추가한 것은 코드 정리를 위한 좋은 실천입니다. 동작에는 영향이 없습니다.

core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/study/section/ActivityThemeSection.kt (1)

35-40: API 변경은 호환성 문제 없음

selectedThemes 파라미터 타입이 ImmutableList<StudyTheme>에서 List<StudyTheme>로 변경되었습니다. 모든 호출 지점(CheckListScreen, StudyCategoryScreen, RecruitingStudyFilterScreen, PreferLocationStudyFilterScreen)에서 호환되며, Kotlin의 타입 시스템상 ImmutableListList의 서브타입이므로 문제없이 작동합니다.

feature/study/src/main/java/com/umcspot/spot/study/register/RegisterStudyScreen.kt (2)

26-26: LocationRow 타입으로의 마이그레이션 - 좋습니다!

String 기반에서 LocationRow 객체로 전환하여 타입 안전성이 향상되었습니다.


151-152: 콜백 시그니처가 ViewModel 메서드와 올바르게 일치합니다.

onAddSelectedonRemoveSelected 콜백이 LocationRow 파라미터를 받도록 변경되었으며, ViewModel의 addSelectedRegion(84줄)과 removeSelectedRegion(97줄) 메서드도 동일한 LocationRow 타입을 받도록 구현되어 있습니다. 타입 변경이 일관성 있게 적용되었습니다.

feature/main/src/main/java/com/umcspot/spot/main/MainScreen.kt (3)

37-37: 새로운 네비게이션 라우트 import - LGTM!

Home과 PreferLocationFilter 라우트가 올바르게 import되었습니다.

Also applies to: 41-41


69-69: TopBar 타이틀 설정 - LGTM!

PreferLocationFilter 라우트에 대한 타이틀이 다른 라우트들과 일관성 있게 추가되었습니다.


110-112: Home 화면에서의 FAB 네비게이션 - LGTM!

Home 라우트에서 스터디 등록 화면으로의 네비게이션이 다른 라우트들과 일관된 패턴으로 구현되었습니다.

feature/main/src/main/java/com/umcspot/spot/main/MainNavHost.kt (3)

29-29: 필터 그래프 import 추가 - LGTM!

preferLocationStudyFilterGraph가 올바르게 import되었습니다.


94-94: 네비게이션 그래프 연결 - LGTM!

recruitingStudyFilterGraph에 navController 파라미터가 추가되었고, preferLocationStudyGraph의 onFilterClick이 올바르게 연결되었습니다. 패턴이 일관성 있게 적용되었습니다.

Also applies to: 102-102


105-109: preferLocationStudyFilterGraph 추가 - LGTM!

필터 화면의 네비게이션 그래프가 recruitingStudyFilterGraph와 일관된 패턴으로 구현되었습니다. navController를 통한 parent entry 접근과 필터 적용 시 back stack 처리가 올바르게 설정되었습니다.

feature/study/src/main/java/com/umcspot/spot/study/register/model/RegisterStudyState.kt (1)

17-17: selectedRegions 타입 변경 - LGTM!

List에서 List로의 변경은 위치 데이터의 구조화와 타입 안전성을 개선합니다. 관련된 UI 컴포넌트와 콜백들이 일관성 있게 업데이트된 것으로 확인됩니다.

data/user/src/main/java/com/umcspot/spot/user/dto/request/UserPreferredRegionRequestDto.kt (1)

7-11: DTO 구조 - LGTM!

UserPreferredRegionRequestDto가 명확하게 정의되었고 Kotlin serialization 어노테이션이 올바르게 적용되었습니다.

data/user/src/main/java/com/umcspot/spot/user/datasource/UserDataSource.kt (1)

14-15: LGTM! 인터페이스 확장이 적절합니다.

새로운 사용자 선호 지역 관련 메서드가 기존 패턴과 일관성 있게 추가되었습니다.

domain/user/src/main/java/com/umcspot/spot/user/repository/UserRepository.kt (1)

12-13: LGTM! Repository 인터페이스 설계가 우수합니다.

Result 타입을 사용한 에러 핸들링 패턴이 적절하게 적용되었습니다.

feature/study/src/main/java/com/umcspot/spot/study/register/screen/StudyPlaceScreen.kt (1)

28-34: LGTM! 타입 안정성 개선이 우수합니다.

String에서 LocationRow로의 리팩토링은 더 풍부한 위치 정보(province, district, neighborhood)를 제공하고 타입 안정성을 향상시킵니다.

data/user/src/main/java/com/umcspot/spot/user/datasourceimpl/UserDataSourceImpl.kt (1)

46-47: LGTM! 간결한 구현입니다.

getUserPreferredRegion 구현이 적절합니다.

data/user/src/main/java/com/umcspot/spot/user/service/UserService.kt (1)

29-36: API 엔드포인트 URL 불일치 확인 필요

POST 엔드포인트는 /api/members/preferred-regions를 사용하고, GET 엔드포인트는 /api/members/prefer-regions를 사용합니다. 이것이 의도된 백엔드 설계인지 확인해 주세요.

data/user/src/main/java/com/umcspot/spot/user/mapper/UserMapper.kt (1)

30-39: LGTM!

새로운 매핑 함수들이 기존 패턴을 잘 따르고 있으며, 데이터 변환 로직이 명확합니다.

core/common/src/main/java/com/umcspot/spot/common/location/Location.kt (2)

9-20: LGTM!

LocationRow의 구조가 잘 설계되었습니다. fullName computed property가 빈 값을 적절히 필터링하여 깔끔한 표시 문자열을 생성합니다.


44-64: 파싱 로직이 잘 구현되었습니다.

헤더 제거, 탭 분리, null-safe 매핑이 적절하게 처리되어 있습니다.

data/study/src/main/java/com/umcspot/spot/study/mapper/StudyMapper.kt (2)

22-33: id.toLong() 변환 시 예외 처리 검토

this.id.toLong()id가 유효한 숫자가 아닐 경우 NumberFormatException을 발생시킬 수 있습니다. 백엔드에서 항상 유효한 숫자 ID를 반환한다면 문제없지만, 방어적 코딩을 위해 toLongOrNull()과 기본값 사용을 고려해 보세요.


35-40: LGTM!

toDomainList() 함수가 페이지네이션 필드(hasNext, nextCursor)를 포함하여 올바르게 매핑하고 있습니다.

data/user/src/main/java/com/umcspot/spot/user/repositoryimpl/UserRepositoryImpl.kt (1)

35-47: LGTM!

새로운 setUserPreferredRegiongetUserPreferredRegion 메서드가 기존 패턴을 따르고 있으며, 에러 로깅도 적절하게 구현되어 있습니다.

feature/study/src/main/java/com/umcspot/spot/study/register/component/SelectedRegionsSection.kt (1)

32-54: LGTM!

String에서 LocationRow로의 마이그레이션이 깔끔하게 구현되었습니다. fullName을 사용한 표시와 LocationRow 객체를 통한 콜백 처리가 일관성 있게 적용되었습니다.

feature/study/src/main/java/com/umcspot/spot/study/register/RegisterStudyViewModel.kt (1)

84-104: LGTM!

addSelectedRegionremoveSelectedRegion 메서드가 LocationRow 타입을 사용하도록 올바르게 업데이트되었습니다.

feature/study/src/main/java/com/umcspot/spot/study/recruiting/navigation/RecruitingStudyFilterNavigation.kt (1)

25-34: LGTM! 부모 화면과 ViewModel 공유 패턴이 올바르게 구현되었습니다.

remember(navController)를 사용하여 parentEntry를 캐싱하고, hiltViewModel(parentEntry)로 부모 화면의 ViewModel을 가져오는 방식은 Hilt Navigation Compose의 권장 패턴입니다. 이를 통해 필터 화면에서 변경한 내용이 부모 Recruiting 화면에 올바르게 반영됩니다.

domain/study/src/main/java/com/umcspot/spot/study/model/StudyResult.kt (2)

5-8: 페이지네이션 필드가 적절하게 추가되었습니다.

hasNextnextCursor 필드 추가로 커서 기반 페이지네이션을 올바르게 지원합니다.


42-57: LGTM! 더미 데이터 생성기가 새 필드들을 올바르게 반영합니다.

feature/main/src/main/java/com/umcspot/spot/main/MainNavigator.kt (2)

100-108: LGTM! PreferLocation 관련 네비게이션 로직이 기존 패턴과 일관되게 추가되었습니다.

showBackTopBar, showToTopFab, showMultipleFab 조건들이 적절하게 확장되었습니다.


174-176: LGTM! 새로운 네비게이션 함수가 올바르게 추가되었습니다.

feature/study/src/main/java/com/umcspot/spot/study/preferLocation/navigation/PreferLocationStudyFilterNavigation.kt (1)

19-38: LGTM! RecruitingStudyFilterNavigation과 일관된 패턴으로 구현되었습니다.

부모 화면의 ViewModel을 공유하는 패턴이 RecruitingStudyFilterNavigation.kt와 동일하게 적용되어 코드베이스의 일관성이 유지됩니다.

data/study/src/main/java/com/umcspot/spot/study/datasourceimpl/StudyDataSourceImpl.kt (1)

40-49: isOnline 파라미터에 하드코딩된 null 값을 확인해 주세요.

studyService.getPreferLocationStudies 호출 시 isOnline 파라미터에 null이 하드코딩되어 있습니다. 위치 기반 스터디에서는 온라인/오프라인 필터가 불필요하다면 이해되지만, 의도된 동작인지 확인이 필요합니다.

studyService.getPreferLocationStudies(recruitingStatus, feeCategory, categories, null, sortType, cursor, size, regionCodes)
//                                                                              ^^^^
data/study/src/main/java/com/umcspot/spot/study/service/StudyService.kt (1)

29-49: List 쿼리 파라미터 직렬화 방식 확인 필요

categoriesregionCodes 파라미터가 List<String> 타입으로 선언되어 있습니다. Retrofit은 기본적으로 리스트를 같은 키로 반복하여 직렬화합니다 (예: categories=A&categories=B). 백엔드 API가 이 형식을 기대하는지, 아니면 콤마로 구분된 단일 값 (예: categories=A,B)을 기대하는지 확인해주세요.

콤마 구분 형식이 필요하다면, 리스트를 문자열로 변환하거나 커스텀 @Query 변환기를 사용해야 합니다.

feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyFilterScreen.kt (1)

61-68: Draft 상태 초기화 시점 문제

draftRecruitingStatus, draftFee, draftThemes가 초기 컴포지션 시점의 ViewModel 상태로만 초기화됩니다. 화면을 벗어났다가 다시 돌아올 때, rememberSaveable은 저장된 draft 값을 복원하지만 ViewModel의 실제 필터 상태와 동기화되지 않을 수 있습니다.

ViewModel 상태가 외부에서 변경될 수 있다면, LaunchedEffect를 사용하여 ViewModel 상태 변경 시 draft를 업데이트하는 것을 고려해주세요.

core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/bottomsheet/LocationBottomSheet.kt (1)

76-84: LGTM!

String 대신 LocationRow 타입을 사용하여 타입 안정성이 향상되었습니다. 선택 비교 시 code를 사용하고 표시 시 fullName/neighborhood를 사용하는 것이 적절합니다.

data/study/src/main/java/com/umcspot/spot/study/repositoryimpl/StudyRepositoryImpl.kt (1)

60-82: 일관되지 않은 에러 처리

getRecruitingStudies는 실패 시 recoverCatching으로 더미 데이터를 반환하지만, getPreferLocationStudiesonFailure로 로그만 남기고 복구하지 않습니다. 의도된 동작인지 확인해주세요.

에러 복구 동작을 통일하거나, 의도적인 차이라면 주석으로 설명해주세요.

feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyScreen.kt (1)

110-118: ON_RESUME마다 스크롤 초기화 재검토 필요

ON_RESUME 이벤트마다 리스트를 최상단으로 스크롤합니다. 필터 화면에서 돌아올 때도 스크롤 위치가 초기화되어 사용자 경험이 저하될 수 있습니다.

필터 적용 후 스크롤 초기화가 의도된 동작이라면 현재 구현이 적절하지만, 그렇지 않다면 특정 조건에서만 스크롤을 초기화하도록 수정해주세요.

feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyViewModel.kt (4)

38-47: LGTM!

ScrollPositionPreferLocationStudyUiState 데이터 클래스가 적절하게 정의되어 있습니다. 불변 값과 합리적인 기본값을 사용하고 있습니다.


82-89: 적절한 파생 상태 패턴입니다.

combine을 사용하여 isFiltered를 계산하는 방식이 적절합니다. WhileSubscribed(5_000) 사용으로 불필요한 upstream 수집을 방지합니다.


200-221: LGTM!

selectTab, applyFilter, setSort 메서드들이 깔끔하게 구현되어 있으며, 상태 업데이트 후 적절히 load()를 호출합니다.


263-274: LGTM!

syncPreferredRegions() 메서드가 사용자 선호 지역을 올바르게 저장하고 데이터를 다시 로드합니다. 오류 처리도 적절합니다.

feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyFilterScreen.kt (4)

112-207: LGTM!

RecruitingStudyFilterScreenContent가 잘 구조화되어 있습니다. 반응형 사이징을 위한 screenWidthDp/screenHeightDp 사용이 적절합니다.


209-254: LGTM!

단일 선택 패턴이 올바르게 구현되어 있습니다. 디자인 시스템 컴포넌트(MultiButton, TextButton)를 적절히 활용하고 있습니다.


276-280: LGTM!

getIconForType 함수가 간결하게 구현되어 있습니다. Kotlin의 when이 sealed/enum에 대해 exhaustive하므로 새 타입 추가 시 컴파일 오류가 발생하여 안전합니다.


66-74: 드래프트 상태 초기화 로직 재검토 필요

ViewModel의 activity, fee, themes 상태는 동기적으로 기본값(null, null, emptyList)으로 초기화되므로, 초기 컴포지션 시 비동기 로드 문제는 없습니다. 다만 rememberSaveable은 첫 컴포지션에서만 람다를 실행하므로, ViewModel 상태가 업데이트되어도 드래프트 상태는 자동으로 동기화되지 않습니다. 필터를 적용한 후 필터 화면을 다시 열면 드래프트 상태가 최신 ViewModel 상태를 반영하지 않을 수 있습니다.

feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyScreen.kt (5)

137-147: 페이지네이션 로직이 잘 구현되어 있습니다.

snapshotFlow를 사용한 lazy loading 감지가 적절합니다. 단, isLoadingMore를 key로 포함하면 로딩 상태 변경 시 effect가 재시작될 수 있으니 주의하세요.


197-250: UI 상태 분기 처리가 잘 되어 있습니다.

UiState에 따른 조건부 렌더링이 명확합니다. isNullPreferLocation에 따라 다른 Empty 상태를 표시하는 로직이 적절합니다.


317-385: LGTM!

HeaderRow가 잘 구현되어 있습니다. isFiltered 상태에 따른 시각적 피드백(아이콘 색상, 배경색 변경)이 적절합니다.


387-467: LGTM!

탭 스크롤 영역의 그라데이션 scrim 효과가 UX를 개선합니다. drawWithContent를 활용한 구현이 적절합니다.


469-540: LGTM!

SortTypeBottomSheet이 Material3 컴포넌트를 적절히 활용하여 구현되어 있습니다. 선택된 옵션에 체크 아이콘을 표시하는 UX가 좋습니다.

core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/study/StudyItem.kt (6)

46-49: ClickSurface 사용으로 일관된 클릭 인터랙션 제공

ClickSurface를 사용하여 클릭 시각적 피드백을 일관되게 제공하는 좋은 개선입니다. onClick(item)으로 아이템을 전달하는 구현도 올바릅니다.


56-56: 필드 이름 변경이 도메인 모델과 일치

studyImageprofileImageUrl, titlename, goaldescription, member/maxMembercurrentMembers/maxMembers, viewshitCount, likeslikeCount 변경이 StudyResult 도메인 모델 업데이트와 일치합니다.

Also applies to: 70-70, 76-76, 87-88, 91-91, 94-94


64-68: 레이아웃 개선으로 올바른 정렬 구현

fillMaxWidth(), 반응형 height, padding과 함께 Spacer(weight(1f))를 추가하여 통계 정보를 하단에 적절하게 배치했습니다.

Also applies to: 82-83


71-71: 타이포그래피 일관성 개선

SpotTheme.typography를 사용하여 디자인 시스템과 일관된 텍스트 스타일을 적용했습니다.

Also applies to: 77-77, 122-122


86-86: drawable 리소스 모두 정상적으로 존재함

참조된 drawable 리소스(group, eye, like_default)가 모두 core/designsystem/src/main/res/drawable/ 디렉토리에 정상적으로 존재합니다. 추가 조치는 필요하지 않습니다.


44-44: onClick 시그니처 변경이 모든 호출부에 올바르게 적용됨

onClick 시그니처가 () -> Unit에서 (StudyResult) -> Unit으로 변경되었으며, 이에 따라 RecruitingStudyScreen, PreferLocationStudyScreen, HomeScreen의 모든 호출부에서 onClick = { onItemClick(item) }으로 올바르게 업데이트되었습니다. 추가 조치가 필요하지 않습니다.

Comment on lines +108 to +109
val internalSource = interactionSource ?: remember { MutableInteractionSource() }
val isPressed by internalSource.collectIsPressedAsState()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

불필요한 null 처리 코드입니다.

Line 106에서 interactionSource 파라미터는 non-nullable 타입(MutableInteractionSource)이며 기본값을 가지고 있어 절대 null이 될 수 없습니다. 따라서 Line 108의 ?: remember { MutableInteractionSource() } 부분은 실행되지 않는 dead code입니다.

다음 중 하나를 선택하세요:

  1. 파라미터를 nullable로 변경하려는 의도였다면: interactionSource: MutableInteractionSource?로 타입 변경
  2. 단순히 중간 변수만 필요했다면: val internalSource = interactionSource로 단순화
🔎 제안하는 수정 방안

방안 1: 파라미터를 nullable로 변경

-    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+    interactionSource: MutableInteractionSource? = null,
 ) {
-    val internalSource = interactionSource ?: remember { MutableInteractionSource() }
+    val internalSource = interactionSource ?: remember { MutableInteractionSource() }
     val isPressed by internalSource.collectIsPressedAsState()

그리고 Line 125도 수정 필요:

             .clickable(
                 enabled = enabled,
-                interactionSource = interactionSource,
+                interactionSource = internalSource,
                 indication = null
             ) {

방안 2: 불필요한 코드 제거

-    val internalSource = interactionSource ?: remember { MutableInteractionSource() }
-    val isPressed by internalSource.collectIsPressedAsState()
+    val isPressed by interactionSource.collectIsPressedAsState()

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
@core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/button/ImageTextButton.kt
around lines 108 - 109, The nullable fallback on interactionSource is dead code
because the parameter is non-nullable; either make the parameter nullable
(change interactionSource: MutableInteractionSource to interactionSource:
MutableInteractionSource?) and keep the remember fallback for safety, or remove
the unnecessary fallback and simplify to val internalSource = interactionSource
and keep using internalSource.collectIsPressedAsState() (also update any other
usages like the later reference around the line with collectIsPressedAsState
accordingly).

Comment on lines +16 to 24
suspend fun getPreferLocationStudies(
recruitingStatus : RecruitingStatus?,
feeCategory: FeeRange?,
categories: List<String>?,
sortType: RecruitingStudySort?,
cursor: Long?,
size: Int,
regionCodes : List<String>?
): BaseResponse<StudyResponseDto>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

isOnline 파라미터 누락 및 파라미터명 불일치

getPreferLocationStudies에서 두 가지 문제가 있습니다:

  1. isOnline 파라미터가 StudyService (Line 44)에는 존재하지만 StudyDataSource에는 누락되어 있습니다.
  2. 파라미터명이 일관되지 않습니다: 여기서는 sortType을 사용하지만 StudyService에서는 sortBy를 사용합니다.
🔎 제안된 수정
     suspend fun getPreferLocationStudies(
         recruitingStatus : RecruitingStatus?,
         feeCategory: FeeRange?,
         categories: List<String>?,
-        sortType: RecruitingStudySort?,
+        isOnline: Boolean?,
+        sortBy: RecruitingStudySort?,
         cursor: Long?,
         size: Int,
         regionCodes : List<String>?
     ): BaseResponse<StudyResponseDto>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
suspend fun getPreferLocationStudies(
recruitingStatus : RecruitingStatus?,
feeCategory: FeeRange?,
categories: List<String>?,
sortType: RecruitingStudySort?,
cursor: Long?,
size: Int,
regionCodes : List<String>?
): BaseResponse<StudyResponseDto>
suspend fun getPreferLocationStudies(
recruitingStatus : RecruitingStatus?,
feeCategory: FeeRange?,
categories: List<String>?,
isOnline: Boolean?,
sortBy: RecruitingStudySort?,
cursor: Long?,
size: Int,
regionCodes : List<String>?
): BaseResponse<StudyResponseDto>
🤖 Prompt for AI Agents
In
@data/study/src/main/java/com/umcspot/spot/study/datasource/StudyDataSource.kt
around lines 16 - 24, The interface method getPreferLocationStudies in
StudyDataSource is missing the isOnline parameter and uses sortType while
StudyService expects sortBy; update the StudyDataSource signature (and any
implementing classes) to include the Boolean? isOnline parameter and rename
sortType to sortBy (or vice versa to match StudyService) so the parameter names
and types match StudyService.getPreferLocationStudies, then adjust all
implementations and callers of getPreferLocationStudies to pass the new isOnline
argument and the renamed sortBy parameter.

Comment on lines +24 to +25
@SerialName("id")
val id: String,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Search for StudyResult domain model definition
rg -n "class StudyResult|data class StudyResult" --type kotlin

Repository: SPOTeam/Android-V2

Length of output: 261


🏁 Script executed:

# Find Study class and toDomain() method
rg -n "class Study|data class Study" --type kotlin | head -20

Repository: SPOTeam/Android-V2

Length of output: 1802


🏁 Script executed:

# Search for the mapper or conversion logic
rg -n "fun Study.*toDomain|fun.*Study.*toDomain" --type kotlin -A 5

Repository: SPOTeam/Android-V2

Length of output: 1346


🏁 Script executed:

# Check for id field type in the codebase
rg -n "val id.*:" data/study --type kotlin | grep -E "Study|Domain"

Repository: SPOTeam/Android-V2

Length of output: 165


🏁 Script executed:

# Read StudyResult domain model
cat -n domain/study/src/main/java/com/umcspot/spot/study/model/StudyResult.kt

Repository: SPOTeam/Android-V2

Length of output: 2236


🏁 Script executed:

# Check StudyMapper more thoroughly for error handling
cat -n data/study/src/main/java/com/umcspot/spot/study/mapper/StudyMapper.kt

Repository: SPOTeam/Android-V2

Length of output: 1671


id 타입 변환에 에러 처리가 없습니다.

DTO에서 idString 타입이고 StudyResult 도메인 모델에서는 Long 타입입니다. StudyMapper.ktStudy.toDomain() 메서드에서 this.id.toLong()으로 변환하고 있지만, API가 숫자가 아닌 값을 반환하는 경우 NumberFormatException이 발생하여 앱이 크래시될 수 있습니다. 안전한 변환 로직(try-catch 또는 toLongOrNull() 활용)을 추가하세요.

🤖 Prompt for AI Agents
In
@data/study/src/main/java/com/umcspot/spot/study/dto/response/StudyResponseDto.kt
around lines 24 - 25, The DTO's id is a String but Study.toDomain() currently
does this.id.toLong(), which can throw NumberFormatException for non-numeric API
values; change the conversion to use safe parsing (e.g., this.id.toLongOrNull())
inside Study.toDomain() in StudyMapper.kt and handle the null case explicitly
(either throw a clear IllegalArgumentException/DomainMappingException with
context including the offending id, or return a nullable domain/optional result
as your domain contract requires) so malformed ids no longer crash the app.

Comment on lines 31 to 37
override suspend fun getRecommendStudies(): Result<StudyResultList> =
runCatching {
val response = studyDataSource.getRecommendStudies()
response.result.toDomain()
val response = studyDataSource.getPopularStudies()
response.result.toDomainList()
}.recoverCatching {
setRecommendDummies()
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

잘못된 데이터 소스 호출

getRecommendStudies()studyDataSource.getRecommendStudies() 대신 studyDataSource.getPopularStudies()를 호출하고 있습니다. 이로 인해 추천 스터디 대신 인기 스터디 데이터가 반환됩니다.

🔎 제안된 수정
     override suspend fun getRecommendStudies(): Result<StudyResultList> =
         runCatching {
-            val response = studyDataSource.getPopularStudies()
+            val response = studyDataSource.getRecommendStudies()
             response.result.toDomainList()
         }.recoverCatching {
             setRecommendDummies()
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
override suspend fun getRecommendStudies(): Result<StudyResultList> =
runCatching {
val response = studyDataSource.getRecommendStudies()
response.result.toDomain()
val response = studyDataSource.getPopularStudies()
response.result.toDomainList()
}.recoverCatching {
setRecommendDummies()
}
override suspend fun getRecommendStudies(): Result<StudyResultList> =
runCatching {
val response = studyDataSource.getRecommendStudies()
response.result.toDomainList()
}.recoverCatching {
setRecommendDummies()
}
🤖 Prompt for AI Agents
In
@data/study/src/main/java/com/umcspot/spot/study/repositoryimpl/StudyRepositoryImpl.kt
around lines 31 - 37, The getRecommendStudies() implementation is calling
studyDataSource.getPopularStudies() instead of the recommend endpoint; update
the call inside StudyRepositoryImpl.getRecommendStudies() to invoke
studyDataSource.getRecommendStudies() (keep the runCatching/recoverCatching flow
and the fallback to setRecommendDummies()), ensuring the returned
response.result is still mapped via toDomainList().

Comment on lines 16 to 22
override suspend fun getUserName(): Result<UserResult> =
runCatching {
val userName = userService.getUser()
val userName = userDataSource.getUser()
userName.result.toDomain()
}.recoverCatching {
UserResult(name = "123")
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

하드코딩된 폴백 값 검토 필요

recoverCatching에서 UserResult(name = "123")을 반환하는 것은 디버그용 플레이스홀더처럼 보입니다. 프로덕션 환경에서는 적절한 에러 처리나 의미 있는 기본값으로 대체하는 것이 좋습니다.

🤖 Prompt for AI Agents
In
@data/user/src/main/java/com/umcspot/spot/user/repositoryimpl/UserRepositoryImpl.kt
around lines 16 - 22, The fallback UserResult(name = "123") in
UserRepositoryImpl.getUserName is a hardcoded placeholder; replace the
recoverCatching block so we don't return that magic value — either propagate the
original error into the Result (e.g., remove recoverCatching so runCatching
yields a failure, or explicitly return Result.failure(it) inside the recover
handler using the exception variable `it`), or replace it with a meaningful
default sourced from config or a well-defined constant (e.g., empty name or
nullable field) instead of "123"; update references to userDataSource.getUser(),
getUserName and recoverCatching accordingly.

Comment on lines +84 to +87
}.onFailure { e ->
Log.e("RecruitingStudyViewModel", "loadFristError", e)
// UiState.Failure(e.message ?: e.toString())
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

실패 시 UI 상태 업데이트 누락

load() 함수에서 에러 발생 시 로그만 남기고 UI 상태를 UiState.Failure로 업데이트하지 않습니다. 주석 처리된 코드 (Line 86)가 의도된 동작인 것 같습니다.

🔎 제안된 수정
             }.onFailure { e ->
                 Log.e("RecruitingStudyViewModel", "loadFristError", e)
-//                UiState.Failure(e.message ?: e.toString())
+                _uiState.update { it.copy(studies = UiState.Failure(e.message ?: e.toString())) }
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
}.onFailure { e ->
Log.e("RecruitingStudyViewModel", "loadFristError", e)
// UiState.Failure(e.message ?: e.toString())
}
}.onFailure { e ->
Log.e("RecruitingStudyViewModel", "loadFristError", e)
_uiState.update { it.copy(studies = UiState.Failure(e.message ?: e.toString())) }
}
🤖 Prompt for AI Agents
In
@feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyViewModel.kt
around lines 84 - 87, In RecruitingStudyViewModel's load() function the
onFailure block only logs the exception (Log.e("RecruitingStudyViewModel",
"loadFristError", e)) and does not update the UI state; replace the
commented-out intent with an actual update to the ViewModel's UI state (e.g.,
set the backing state to UiState.Failure using the exception message or
toString) inside the onFailure lambda so the UI observes the failure and can
react accordingly.

Copy link

@coderabbitai coderabbitai bot left a 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

🧹 Nitpick comments (4)
feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyViewModel.kt (1)

36-36: scrollPosition을 StateFlow로 변경 고려

현재 var scrollPosition은 plain 프로퍼티입니다. 스크롤 복원 용도로만 사용한다면 괜찮지만, Compose에서 반응적으로 관찰하려면 MutableStateFlow로 변경하는 것이 좋습니다.

core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/appBar/AppBar.kt (1)

164-243: 향후 작업: SearchTopBar도 반응형 크기 조정 적용을 고려해주세요.

AppBarHomeBackTopBar는 반응형 크기 조정을 적용했지만, SearchTopBar는 여전히 고정 dp 값을 사용하고 있습니다. 일관된 반응형 디자인을 위해 향후 이 컴포넌트도 screenWidthDp/screenHeightDp를 적용하는 것을 고려해주세요.

현재 PR의 범위를 벗어나므로 나중에 처리해도 무방합니다.

feature/study/src/main/java/com/umcspot/spot/study/register/RegisterStudyViewModel.kt (2)

97-104: 리스트 필터링 간소화 가능.

toMutableList().apply { remove() } 패턴보다 filter를 사용하면 더 간결하고 함수형 스타일에 가깝습니다.

♻️ 제안하는 리팩토링
 fun removeSelectedRegion(region: LocationRow) {
     _uiState.update { currentState ->
-        val updatedRegions = currentState.selectedRegions.toMutableList().apply {
-            remove(region)
-        }
-        currentState.copy(selectedRegions = updatedRegions)
+        currentState.copy(
+            selectedRegions = currentState.selectedRegions.filter { it != region }
+        )
     }
 }

195-197: 쉐도잉 버그 수정 완료! 추가 최적화 검토 권장.

이전 리뷰에서 지적된 변수 쉐도잉 문제가 region으로 파라미터명을 변경하여 올바르게 해결되었습니다.

다만, selectedRegionsLocationRow 객체들이 이미 code 프로퍼티를 가지고 있다면, allLocations에서 다시 조회하는 과정이 불필요할 수 있습니다. 직접 code에 접근하는 것이 더 효율적입니다.

♻️ 제안하는 최적화

LocationRow가 이미 code 프로퍼티를 포함하고 있다면:

 val regionCodes = if (currentState.activityType == ActivityType.ONLINE) {
     emptyList()
 } else {
-    currentState.selectedRegions.mapNotNull { region ->
-        allLocations.find { it.fullName == region.fullName }?.code
-    }
+    currentState.selectedRegions.map { it.code }
 }

만약 selectedRegionsLocationRow 객체에 code가 없어서 조회가 필요한 경우라면, 현재 구현이 적절합니다.

다음 스크립트로 LocationRowcode 프로퍼티 존재 여부를 확인하세요:

#!/bin/bash
# Description: Check if LocationRow has a code property

# Find LocationRow data class definition and check for code property
ast-grep --pattern $'data class LocationRow(
  $$$
  code: $_
  $$$
)'

# Also search for any code property in LocationRow
rg -A 10 'data class LocationRow' --type kotlin
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 22f6f0c and d329be6.

📒 Files selected for processing (4)
  • core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/appBar/AppBar.kt
  • data/user/src/main/java/com/umcspot/spot/user/dto/response/UserPreferredRegionResponseDto.kt
  • feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyViewModel.kt
  • feature/study/src/main/java/com/umcspot/spot/study/register/RegisterStudyViewModel.kt
🧰 Additional context used
🧬 Code graph analysis (2)
core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/appBar/AppBar.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/RecruitingStudyViewModel.kt (2)
feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyViewModel.kt (1)
  • load (95-154)
core/common/src/main/java/com/umcspot/spot/common/location/Location.kt (1)
  • load (25-70)
🔇 Additional comments (11)
feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyViewModel.kt (4)

57-64: LGTM!

isFiltered 상태를 combine으로 파생하고 WhileSubscribed(5_000)으로 구독 관리하는 패턴이 적절합니다.


99-121: _isLoadingMore 상태 관리 수정 확인

이전 리뷰에서 지적된 _isLoadingMore 미설정 이슈가 수정되었습니다. Line 100에서 true, Line 120에서 false로 설정하여 중복 요청 방지 로직이 정상 작동합니다.

다만, onFailure (Line 117-119)에서 에러를 로그만 남기고 있어 사용자에게 피드백이 없습니다. 무한 스크롤에서 silent fail이 의도된 것이라면 괜찮지만, 토스트나 스낵바로 알림을 고려해 볼 수 있습니다.


131-137: LGTM!

개별 setter를 applyFilter로 통합한 것이 API 사용성과 일관성 면에서 좋습니다. 필터 변경 후 자동으로 load()를 호출하여 데이터를 갱신합니다.


84-87: 실패 시 UI 상태 업데이트 누락

onFailure 블록에서 로그만 남기고 UI 상태를 업데이트하지 않습니다. 사용자가 에러 상태를 확인하거나 재시도할 수 없습니다. 주석 처리된 Line 86을 활성화하거나 _uiState.update { it.copy(studies = UiState.Failure(...)) }로 수정이 필요합니다.

Likely an incorrect or invalid review comment.

data/user/src/main/java/com/umcspot/spot/user/dto/response/UserPreferredRegionResponseDto.kt (2)

8-15: DTO 구조가 적절합니다.

명시적인 @SerialName 매핑과 적절한 타입 선언으로 깔끔하게 구현되었습니다.


7-7: 코드베이스 전체에 걸쳐 14개의 DTO 파일에서 동일한 @SuppressLint("UnsafeOptInUsageError") 패턴이 일관되게 사용되고 있습니다. 이는 고립된 사례가 아닌 의도적인 프로젝트 전체 패턴으로 보입니다.

이 억제가 프로젝트 설정(안드로이드 린트 규칙, kotlinx.serialization 플러그인 구성, 또는 특정 버전 요구사항)으로 인해 실제로 필요한지, 아니면 더 이상 필요하지 않은지 확인하려면 프로젝트 메인테이너나 설정에 대해 잘 아는 개발자의 수동 검증이 필요합니다.

core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/appBar/AppBar.kt (4)

41-41: 반응형 높이 지원을 위한 import 추가가 적절합니다.

screenHeightDp import는 AppBar의 반응형 높이 구현에 필요합니다.


54-56: 반응형 레이아웃 적용이 적절합니다.

AppBar의 높이와 패딩에 화면 비율 기반 크기 조정을 적용하여 다양한 화면 크기에서 일관된 비율을 유지합니다.


67-89: 터치 타겟 크기를 검증해주세요.

IconButton들이 screenWidthDp(32.dp)로 명시적으로 크기가 지정되어 있습니다. Material Design 가이드라인은 최소 48.dp의 터치 타겟 크기를 권장합니다. 작은 화면에서 반응형 스케일링이 적용될 경우 터치 타겟이 권장 크기 이하로 작아질 수 있어 접근성 문제가 발생할 수 있습니다.

다양한 화면 크기에서 실제 터치 타겟 크기가 사용성 기준을 충족하는지 확인해주세요.

참고: Line 87의 알림 아이콘이 이제 screenWidthDp(24.dp)를 사용하여 검색 아이콘과 일관된 반응형 크기 조정을 적용하고 있습니다.


134-138: 터치 타겟 크기가 더 작아 접근성 문제가 우려됩니다.

뒤로 가기 버튼의 IconButton과 Icon이 모두 screenWidthDp(20.dp)로 설정되어 있습니다. 이는 AppBarHome의 아이콘들(32.dp IconButton, 24.dp Icon)보다 더 작으며, 작은 화면에서는 최소 권장 터치 타겟 크기(48.dp) 이하로 떨어질 가능성이 높습니다.

또한 IconButton과 내부 Icon의 크기가 동일하여 패딩이 없는 점도 AppBarHome의 접근 방식과 다릅니다. 의도된 디자인인지 확인하고, 터치 타겟 크기를 검증해주세요.

feature/study/src/main/java/com/umcspot/spot/study/register/RegisterStudyViewModel.kt (1)

84-95: LocationRow는 적절한 equals()/hashCode() 구현을 보유하고 있습니다.

LocationRow가 data class로 선언되어 있으므로, Kotlin이 자동으로 모든 주생성자 프로퍼티(code, province, district, neighborhood)를 기반으로 equals()hashCode()를 생성합니다. 따라서 addSelectedRegioncontains() 체크는 의도대로 동작하며 중복 방지가 정상적으로 작동합니다.

@starshape7 starshape7 merged commit 1428561 into develop Jan 10, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT/#6] Study 구현

2 participants