-
Notifications
You must be signed in to change notification settings - Fork 0
[Feat] PlaceMap의 하위 Fragment 제거 및 MVI 아키텍쳐 적용 #24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Conversation
기존 `Fragment`에서 `OnBackPressedCallback`을 통해 처리하던 장소 상세 미리보기(`PlaceDetailPreview`) 화면의 뒤로 가기 로직을 Compose의 `BackHandler`로 마이그레이션했습니다. 이를 통해 View 시스템에 대한 의존성을 줄이고 Compose 내부에서 상태에 따라 뒤로 가기 이벤트를 더 직관적으로 제어하도록 개선했습니다.
- **`PlaceDetailPreviewFragment.kt` 리팩토링:**
- `OnBackPressedCallback` 객체 생성 및 등록, 생명주기 관련 코드를 모두 제거했습니다.
- `LaunchedEffect`를 통해 `backPressedCallback`을 활성화하던 로직을 제거했습니다.
- `PlaceDetailPreviewScreen` 컴포저블에 `onBackPress` 콜백을 추가하여, 뒤로 가기 이벤트 발생 시 `viewModel.unselectPlace()`를 호출하도록 연결했습니다.
- **`PlaceDetailPreviewScreen.kt` 수정:**
- `BackHandler`를 도입하여 `visible` 상태가 `true`일 때만 뒤로 가기 이벤트를 가로채고 `onBackPress`를 실행하도록 구현했습니다.
- 더 이상 불필요해진 `onEmpty` 파라미터를 제거하고 `onBackPress` 파라미터를 추가했습니다.
특정 모델(`PlaceDetailUiModel`)에 종속적이던 `SelectedPlaceUiState`를 제거하고, 제네릭을 지원하는 `PlaceUiState<T>`로 대체하여 상태 관리 클래스의 범용성을 확보했습니다. 아울러 일부 컴포넌트의 패키지 위치를 정리했습니다.
- **`PlaceUiState.kt` 도입:**
- 기존 `SelectedPlaceUiState.kt`를 삭제하고, `Loading`, `Empty`, `Success<T>`, `Error` 상태를 가지는 제네릭 실드 인터페이스 `PlaceUiState<T>`를 새로 정의했습니다.
- 기존 `Success` 상태에 포함되어 있던 `isSecondary` 로직을 `PlaceUiState.Success<PlaceDetailUiModel>`의 확장 프로퍼티로 분리하여 구현했습니다.
- **ViewModel 및 UI 로직 수정:**
- `PlaceMapViewModel`에서 관리하는 `selectedPlace`의 타입을 `SelectedPlaceUiState`에서 `PlaceUiState<PlaceDetailUiModel>`로 변경했습니다.
- 이에 따라 `PlaceMapFragment`, `PlaceDetailPreviewFragment`, `PlaceDetailPreviewScreen` 등 관련 UI 컴포넌트에서 상태를 처리하는 분기문과 데이터 참조 코드를 수정했습니다.
- **`TimeTagMenu.kt` 패키지 이동:**
- `TimeTagMenu` 컴포저블의 위치를 `timeTagSpinner/component` 패키지에서 `placeMap/component`로 이동하여 구조를 단순화했습니다.
- **테스트 코드 수정:**
- `PlaceMapViewModelTest`의 검증 로직을 `PlaceUiState`를 사용하도록 변경했습니다.
`PlaceMapViewModel`에서 관리하던 시간 태그(`TimeTag`) 관련 데이터를 `LiveData` 및 단순 리스트에서 `StateFlow<PlaceUiState>`로 변경했습니다. 이를 통해 UI 계층에서 데이터의 로딩, 성공, 빈 상태를 일관되게 처리하도록 개선했습니다.
- **`PlaceMapViewModel.kt` 리팩토링:**
- `timeTags`와 `selectedTimeTag`를 `StateFlow<PlaceUiState>` 타입으로 변환했습니다.
- 기존에 `LiveData`를 Flow로 변환하여 사용하던 임시 프로퍼티 `selectedTimeTagFlow`를 제거했습니다.
- 데이터 로드 성공 및 실패 시 `PlaceUiState.Success` 또는 `PlaceUiState.Empty`를 방출하도록 로직을 수정했습니다.
- **Fragment 및 UI 로직 수정:**
- **`PlaceMapFragment.kt`, `PlaceListFragment.kt`**: `LiveData` 관찰 코드를 `lifecycleScope` 내에서 `StateFlow`를 수집(`collect`)하는 방식으로 변경했습니다. `PlaceUiState` 상태에 따라 지도 마커를 필터링하거나 장소 목록을 갱신하도록 분기 처리를 추가했습니다.
- **`PlaceDetailPreviewFragment.kt`**: 상세 미리보기 클릭 시, 선택된 시간 태그가 유효한 상태(`Success`)인지 확인하는 가드 절을 추가하고, 로깅 시 안전하게 데이터를 참조하도록 수정했습니다.
- **Compose 컴포넌트 수정:**
- **`TimeTagMenu.kt`**: `PlaceUiState`를 인자로 받아 상태가 `Success`일 때만 메뉴를 표시하도록 래퍼 컴포저블을 구현하고, 실제 렌더링 로직은 `TimeTagContent`로 분리했습니다.
- **`PlaceMapScreen.kt`**: 변경된 ViewModel 상태 구조에 맞춰 파라미터 타입을 `PlaceUiState`로 수정하고 하위 컴포넌트에 전달하도록 변경했습니다.
`PlaceMapViewModel`과 `PlaceListViewModel`에서 사용하던 `LiveData`와 `SingleLiveData`를 모두 제거하고, `StateFlow`와 `SharedFlow`로 전면 교체했습니다. 이를 통해 불필요한 Flow 변환 코드를 제거하고 상태 관리 방식을 통일했습니다.
- **`PlaceMapViewModel.kt` 수정:**
- UI 상태(`initialMapSetting`, `placeGeographies`, `selectedPlace`, `isExceededMaxLength`, `selectedCategories`)를 `LiveData`에서 `StateFlow`로 변경했습니다.
- 일회성 이벤트(`navigateToDetail`, `backToInitialPositionClicked`, `onMapViewClick`)를 `SingleLiveData`에서 `SharedFlow`로 변경하고 `tryEmit`을 사용하여 이벤트를 발행하도록 수정했습니다.
- 기존에 `LiveData`를 `Flow`로 변환하여 제공하던 중복 프로퍼티들(`selectedPlaceFlow`, `isExceededMaxLengthFlow`, `onMapViewClickFlow`)을 제거했습니다.
- 메뉴 아이템 재클릭 이벤트를 처리하기 위한 `onMenuItemReClick` 함수와 `SharedFlow`를 추가했습니다.
- **`PlaceListViewModel.kt` 수정:**
- 장소 목록 상태(`places`)를 `LiveData`에서 `StateFlow`로 변경했습니다.
- 기존에 `places`를 변환하여 노출하던 `placesFlow`를 제거하고, `places` 프로퍼티가 직접 `StateFlow`를 반환하도록 수정했습니다.
`PlaceMapFragment` 내부에서 `ChildFragmentManager`를 통해 관리하던 하위 Fragment들(`PlaceListFragment`, `PlaceCategoryFragment`, `PlaceDetailPreviewFragment` 등)을 제거하고, 순수 Jetpack Compose 기반의 `PlaceMapScreen`으로 통합하여 구조를 단순화했습니다.
- **`PlaceMapFragment.kt` 리팩토링:**
- 복잡한 Fragment 트랜잭션 로직을 제거하고, `ComposeView`에서 `PlaceMapScreen`을 통해 전체 UI를 구성하도록 변경했습니다.
- `PlaceMapViewModel`과 `PlaceListViewModel`의 데이터를 `collectAsStateWithLifecycle`로 구독하고, `LaunchedEffect`를 사용하여 `MapManager` 초기화, 마커 필터링, 로깅, 네비게이션 등의 사이드 이펙트를 처리하도록 재구현했습니다.
- 이미지 프리로딩(`preloadImages`) 로직을 Fragment 내부로 이동시켰습니다.
- **`PlaceMapScreen.kt` 및 컴포넌트 재구성:**
- `PlaceCategoryScreen`, `PlaceListScreen`, `PlaceDetailPreviewScreen` 등 개별 컴포저블을 조합하여 하나의 화면을 구성하도록 구조를 변경했습니다.
- `NaverMapContent`가 `NaverMap` 인스턴스를 상위로 전달하도록 수정하여, `PlaceMapFragment` 레벨에서 지도 객체를 제어할 수 있게 개선했습니다.
- 장소 상세 미리보기(`Preview`) 노출 여부에 따라 `PlaceListScreen`의 투명도를 조절하는 UI 로직을 추가했습니다.
- **`PlaceListScreen.kt` 수정:**
- BottomSheet 상태(`bottomSheetState`)에 따라 현재 위치 버튼 등 지도 컨트롤 UI의 노출 여부를 제어하는 로직을 추가했습니다.
- **기타 변경사항:**
- `PlaceDetailPreviewSecondaryScreen`에 `BackHandler`를 추가하여 뒤로가기 동작을 Compose 내에서 처리하도록 했습니다.
- `PlaceMapViewModel`의 프로퍼티 명 변경(`selectedPlaceFlow` -> `selectedPlace`)을 반영했습니다.
장소 지도(`PlaceMap`) 화면의 Compose 마이그레이션이 완료됨에 따라, 더 이상 사용하지 않는 기존 Fragment 및 XML 레이아웃 코드를 삭제했습니다. 또한, 기능별로 분산되어 있던 Compose 컴포넌트와 관련 클래스들을 `placeMap` 패키지 하위로 통합하여 구조를 단순화했습니다.
- **Legacy 코드 삭제:**
- `PlaceListFragment`, `PlaceCategoryFragment`, `PlaceDetailPreviewFragment` 등 기존 View 기반의 Fragment 클래스들을 삭제했습니다.
- `fragment_place_list.xml`, `item_place_list.xml` 등 관련 XML 레이아웃 리소스와 바인딩 코드를 제거했습니다.
- **패키지 구조 재편:**
- `placeList`, `placeCategory`, `placeDetailPreview` 등 하위 패키지에 흩어져 있던 컴포저블(`PlaceListScreen`, `PlaceCategoryScreen` 등)과 `PlaceListViewModel`을 `presentation.placeMap` 및 `presentation.placeMap.component` 패키지로 이동하여 접근성을 높였습니다.
- `PlaceMapFragment` 및 `PlaceMapScreen`에서 변경된 패키지 경로를 반영하도록 import 구문을 수정했습니다.
- **`CategoryView` 이동:**
- 장소 상세 화면(`PlaceDetailActivity`)에서 여전히 사용되는 커스텀 뷰 `CategoryView`를 `presentation.placeDetail` 패키지로 이동하고, `activity_place_detail.xml`에서 해당 뷰를 참조하도록 수정했습니다.
지도 화면(`PlaceMap`)에 MVI(Model-View-Intent) 패턴을 적용하여 단방향 데이터 흐름(UDF) 구조로 리팩토링했습니다. 개별적으로 관리되던 상태들을 하나의 UI State로 통합하고, 사용자 상호작용과 일회성 이벤트를 명확히 정의하여 코드의 유지보수성을 높였습니다.
- **MVI 아키텍처 구성요소 추가:**
- **`PlaceMapAction`:** 지도 준비, 태그 클릭, 마커 클릭 등 사용자의 의도(Intent)를 정의하는 Sealed Interface를 추가했습니다.
- **`PlaceMapEvent`:** 스낵바 표시, 화면 이동, 마커 초기화 등 일회성 부수 효과(Side Effect)를 정의하는 Sealed Interface를 추가했습니다.
- **`PlaceMapUiState`:** 기존의 분산된 `StateFlow`들을 통합 관리하기 위한 단일 상태 데이터 클래스를 도입했습니다.
- **`PlaceMapViewModel.kt` 리팩토링:**
- `onPlaceMapAction` 메서드를 통해 모든 액션을 처리하도록 로직을 중앙화했습니다.
- 개별 `StateFlow` 변수들을 `_uiState`(`PlaceMapUiState`)와 `_uiEvent`(`Channel`)로 대체했습니다.
- 로깅 및 데이터 로드 로직을 `Action` 처리 블록 내부로 이동시켰습니다.
- **UI 상태 클래스 재정의:**
- 기존 `PlaceUiState`를 삭제하고, 범용적인 로딩 상태 관리를 위한 `LoadState`로 대체했습니다.
- `PlaceListUiState` 파일명을 `ListLoadState`로 변경하고 관련 참조를 수정했습니다.
- **View 레이어 수정 (`PlaceMapFragment`, `PlaceMapScreen`):**
- `PlaceMapScreen`의 파라미터를 개별 상태 대신 `uiState`와 `onAction` 콜백으로 단순화했습니다.
- Fragment에서 `ObserveAsEvents` 유틸리티를 사용하여 `SharedFlow`/`Channel` 이벤트를 생명주기에 맞춰 안전하게 수집하도록 변경했습니다.
- Compose View 내부에서 `NaverMap`의 초기화를 돕는 `rememberNaverMap` 및 `await` 확장 함수를 추가했습니다.
- **테스트 코드 수정:**
- 변경된 상태 클래스(`LoadState`, `ListLoadState`)에 맞춰 `PlaceListViewModelTest` 및 `PlaceMapViewModelTest`의 검증 로직을 수정했습니다.
`PlaceMapViewModel`에서 통합 관리되던 단일 이벤트 채널을 UI 관련 이벤트와 지도 제어 이벤트로 분리하고, 비대해진 `PlaceMapFragment`의 로직을 전용 핸들러와 델리게이트로 위임하여 구조를 개선했습니다.
- **이벤트 및 ViewModel 분리:**
- 기존 `PlaceMapEvent`에서 지도 조작 관련 이벤트를 신규 인터페이스인 `MapControlEvent`(`InitMap`, `SetMarkerByTimeTag` 등)로 분리했습니다.
- `PlaceMapViewModel`이 UI 사이드 이펙트를 위한 `placeMapUiEvent`와 지도 제어를 위한 `mapControlUiEvent` 두 개의 Flow를 노출하도록 수정했습니다.
- **핸들러(Handler) 클래스 도입:**
- `PlaceMapFragment` 내부의 방대한 `when` 분기문을 제거하고, 역할을 분리한 전용 핸들러 클래스를 구현했습니다.
- **`MapControlEventHandler`**: `NaverMap` 및 `MapManager`를 직접 조작하는 지도 로직을 담당합니다.
- **`PlaceMapEventHandler`**: 스낵바 표시, 화면 이동(Navigation), 이미지 프리로드 등 Android UI 관련 로직을 담당합니다.
- **상태 관리 위임(Delegate) 적용:**
- `NaverMap` 객체와 `MapManager`의 상태 관리를 위해 `MapDelegate`와 `MapManagerDelegate`를 새로 추가했습니다.
- 기존 `MapState` 클래스를 삭제하고 `MapDelegate`로 대체하였으며, `PlaceMapScreen` 및 `NaverMapContent`가 이를 참조하도록 변경했습니다.
타임태그가 선택되지 않은(`EMTPY_TIME_TAG_ID`) 상태에서 필터를 초기화할 때, 모든 마커가 숨겨지는 문제를 수정했습니다.
- **`MapFilterManagerImpl.kt` 수정:**
- `clearFilter()` 메서드에서 `selectedTimeTagId`가 비어있는 경우, 타임태그 일치 여부를 검사하지 않고 모든 마커를 보이도록(`isVisible = true`) 로직을 개선했습니다.
비대해진 `PlaceMapViewModel`의 책임을 분산하기 위해 Action 처리 로직을 기능별 핸들러로 위임하고, 관련 패키지 구조를 `intent` 중심으로 재구성했습니다.
- **Action 및 Handler 세분화:**
- 단일 `PlaceMapAction`을 `SelectAction`(장소 선택 및 상호작용), `FilterAction`(필터링 및 데이터 로드), `MapEventAction`(지도 제어 이벤트)으로 분리했습니다.
- 각 Action을 전담하여 처리하는 `SelectActionHandler`, `FilterActionHandler`, `MapEventActionHandler`를 도입하여 ViewModel의 로직을 분리했습니다.
- **패키지 구조 재편:**
- `viewmodel` 패키지에 혼재되어 있던 클래스들을 `intent` 패키지 하위의 `action`, `event`, `state`로 이동하여 구조를 명확히 했습니다.
- `PlaceMapUiState`, `LoadState`, `ListLoadState` 등의 상태 클래스와 `MapControlEvent` 등의 이벤트 클래스가 해당 패키지로 이동되었습니다.
- **UI 계층 수정:**
- `PlaceMapScreen` 및 `PlaceMapFragment`에서 ViewModel로 이벤트를 전달할 때, 기존의 포괄적인 `PlaceMapAction` 대신 구체화된 Action(`SelectAction.OnPlaceClick` 등)을 사용하도록 호출부를 수정했습니다.
`PlaceMapViewModel` 내부에서 수동으로 생성하던 ActionHandler 객체들을 Metro 프레임워크를 사용한 의존성 주입 방식으로 변경했습니다. 이를 위해 별도의 핸들러 그래프를 정의하고, 뷰모델의 런타임 의존성을 그래프를 통해 주입받도록 구조를 개선했습니다.
- **`PlaceMapViewModel.kt` 수정:**
- `MapEventActionHandler`, `FilterActionHandler`, `SelectActionHandler`를 직접 인스턴스화하던 코드를 제거했습니다.
- `PlaceMapHandlerGraph.Factory`를 이용해 `handlerGraph`를 생성하고, 필요한 `Channel`, `StateFlow`, Scope 등을 주입했습니다.
- `onPlaceMapAction`에서 개별 핸들러 변수 대신 `handlerGraph`를 통해 핸들러를 호출하도록 변경했습니다.
- **ActionHandler 클래스 수정:**
- `SelectActionHandler`, `MapEventActionHandler`, `FilterActionHandler`에 `@Inject` 어노테이션을 추가했습니다.
- `FilterActionHandler`의 생성자 파라미터에 `@CachedPlaces`, `@CachedPlaceByTimeTag` 한정자(Qualifier)를 적용하여 의존성을 명확히 했습니다.
- **DI 구성요소 추가 (`di/placeMapHandler`):**
- `PlaceMapHandlerGraph`: 핸들러 인스턴스를 제공하고, 런타임 의존성을 주입받는 팩토리 인터페이스를 정의했습니다.
- `CachedPlaces`, `CachedPlaceByTimeTag`: 동일한 타입의 `StateFlow` 의존성을 구분하기 위한 Qualifier 어노테이션을 추가했습니다.
`PlaceMap` 화면의 사용자 액션(Action)과 이벤트(Event)를 처리하는 핸들러 클래스들을 `intent.handler` 패키지로 통합하고, 공통 인터페이스를 도입하여 구조를 표준화했습니다.
- **인터페이스 정의:**
- `ActionHandler<ACTION, STATE>` 및 `EventHandler<EVENT>` 인터페이스를 새로 정의하여 핸들러의 구현 규약을 통일했습니다.
- **패키지 이동 및 구현 수정:**
- 기존 `intent.action`과 `intent.event` 패키지에 있던 `SelectActionHandler`, `MapEventActionHandler`, `FilterActionHandler`, `PlaceMapEventHandler`, `MapControlEventHandler`를 `intent.handler` 패키지로 이동했습니다.
- 각 핸들러 클래스가 `ActionHandler` 또는 `EventHandler` 인터페이스를 구현(`override`)하도록 로직을 수정했습니다.
- **의존성 주입(DI) 설정:**
- `SelectActionHandler`, `MapEventActionHandler`, `FilterActionHandler`에 `@ContributesBinding(PlaceMapViewModelScope::class)`을 추가하여 의존성 주입 설정을 보강했습니다.
지도(PlaceMap) 기능 내 컴포넌트들에 산재되어 있던 하드코딩된 치수(dp)와 비율 값들을 `FestabookSpacing`의 확장 프로퍼티로 정의하고 적용하여 코드의 유지보수성을 높였습니다.
- **`Styles.kt` 추가:**
- `FestabookSpacing`의 확장 프로퍼티를 담은 `Styles.kt`를 생성했습니다.
- `previewVerticalPadding`, `placeListImageSize`, `placeListBottomSheetPeekHeight` 등 특정 컴포넌트에서 사용되는 UI 상수를 한곳에 정의했습니다.
- **`PlaceListScreen.kt` 수정:**
- `PlaceListBottomSheet`의 `peekHeight`와 `halfExpandedRatio`, 그리고 리스트 아이템의 이미지 크기를 새로 정의한 상수로 대체했습니다.
- **`PlaceDetailPreviewScreen.kt` 수정:**
- 화면의 수직 패딩과 장소 이미지 크기에 하드코딩된 값 대신 `festabookSpacing` 확장 프로퍼티를 적용했습니다.
- **기타 컴포넌트 수정:**
- `PlaceCategoryLabel.kt`: 텍스트 패딩 값을 `festabookSpacing.paddingBody1`으로 변경했습니다.
- `TimeTagMenu.kt`: 메뉴의 너비 값을 `timeTagButtonWidth` 상수로 변경했습니다.
`PlaceMapViewModel`의 비대해진 로직을 분리하기 위해 ActionHandler 패턴을 도입하고, 상태 관리 방식을 `StateFlow`로 전면 개편했습니다.
- **`PlaceMapViewModel.kt` 리팩토링:**
- `PlaceMapHandlerGraph.Factory`를 주입받아 로직을 `SelectActionHandler`, `FilterActionHandler`, `MapEventActionHandler`로 위임하도록 구조를 변경했습니다.
- 기존 `LiveData` 기반의 상태 관리를 `StateFlow`(`uiState`)와 `Channel`(`mapControlUiEvent`, `placeMapUiEvent`)로 전환했습니다.
- **ActionHandler 테스트 코드 추가:**
- `SelectActionHandlerTest`: 플레이스 선택/해제, 상세 정보 조회, 타임태그 클릭 등의 로직 테스트를 작성했습니다.
- `FilterActionHandlerTest`: 카테고리 및 타임태그 기반의 장소 필터링 로직 테스트를 작성했습니다.
- `MapEventActionHandlerTest`: 지도 초기화, 초기 위치 복귀, 드래그 이벤트 처리 로직 테스트를 작성했습니다.
- **테스트 및 유틸리티 정비:**
- `PlaceMapViewModelTest`: 핸들러 위임 동작 및 `StateFlow` 상태 변화를 검증하도록 테스트를 수정했습니다.
- `PlaceListViewModelTest`를 삭제하고, 관련 픽스처 및 테스트 파일들을 `placeList`에서 `placeMap` 패키지로 이동했습니다.
- Flow 기반 테스트를 지원하기 위한 `observeEvent` 확장 함수(`FlowExtensions.kt`)를 추가했습니다.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the 📝 WalkthroughWalkthroughPlaceMapFragment refactored from Fragment-based composition with multiple child fragments to a unified Compose-based UI. PlaceMapViewModel migrated to reactive state management using StateFlow, action/event channels, and a DI-powered handler graph. Old UI state types (PlaceListUiState, SelectedPlaceUiState) replaced with LoadState and ListLoadState. New action/handler architecture introduced for filtering, map events, and place selection. Listener interfaces reorganized to subpackage. Changes
Sequence Diagram(s)sequenceDiagram
participant User as User/UI
participant Screen as PlaceMapScreen
participant ViewModel as PlaceMapViewModel
participant Handlers as Handler Graph<br/>(Filter/Map/Select)
participant State as StateFlow<br/>PlaceMapUiState
participant Channels as Event Channels<br/>(MapControl/PlaceMap)
User->>Screen: Triggers Action<br/>(e.g., OnPlaceClick)
Screen->>ViewModel: onPlaceMapAction(action)
ViewModel->>Handlers: Routes to appropriate handler<br/>via handlerGraph
rect rgb(240, 248, 255)
Note over Handlers: Handler processes action
Handlers->>Handlers: Updates internal state<br/>& computes changes
Handlers->>State: Emits state update<br/>via onUpdateState callback
Handlers->>Channels: Emits events<br/>(MapControlEvent, PlaceMapEvent)
end
State-->>Screen: StateFlow observes changes
Channels-->>Screen: Event channels emit to UI
Screen->>Screen: Re-renders based on<br/>new state & events
Screen->>User: Updated UI
User->>Screen: Responds to UI Event<br/>(e.g., StartPlaceDetail)
Screen->>ViewModel: Processes event<br/>(e.g., navigate, show snackbar)
sequenceDiagram
participant VM as PlaceMapViewModel<br/>onCreate
participant Factory as PlaceMapHandlerGraph<br/>.Factory
participant Graph as PlaceMapHandlerGraph
participant Filters as FilterActionHandler
participant MapEvents as MapEventActionHandler
participant Selects as SelectActionHandler
participant Cache as Cached StateFlows<br/>(Places/TimeTag)
participant UIState as StateFlow<br/>PlaceMapUiState
VM->>VM: Collect initial data<br/>(loadAllPlaces, observeErrorEvent)
activate VM
VM->>VM: Create cached flows<br/>(cachedPlaces, cachedPlaceByTimeTag)
VM->>Factory: Create handler graph<br/>with dependencies
activate Factory
Factory->>Graph: Instantiate with channels,<br/>cached flows, callbacks
activate Graph
Graph->>Filters: Wire FilterActionHandler<br/>with update callbacks
Graph->>MapEvents: Wire MapEventActionHandler<br/>with state access
Graph->>Selects: Wire SelectActionHandler<br/>with repository & scope
Graph-->>Factory: Return PlaceMapHandlerGraph
deactivate Graph
Factory-->>VM: Return Graph instance
deactivate Factory
VM->>VM: Store handlerGraph<br/>for action dispatch
VM->>UIState: Initialize with<br/>Loading states
deactivate VM
Note over VM,Selects: Ready for user actions
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs
Suggested reviewers
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
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 |
|
File Change 너무 죄송합니다 ㅜㅜ 최대한 노력했지만 더 작은 단위로 쪼개기가 너무 애매했어요. |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 18
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
app/src/main/java/com/daedan/festabook/presentation/placeMap/component/PreviewAnimatableBox.kt (1)
45-49: Exit animation comment doesn't match implementation.The comment on line 46 describes an exit animation ("나갈 때 애니메이션"), but the code uses
snapTowhich resets values instantly without animating. This creates asymmetric behavior—smooth entry but instant disappearance.If a smooth exit animation is desired, replace
snapTowithanimateTo. If instant dismissal is intentional, update or remove the misleading comment.🔎 Proposed fix for smooth exit animation
} else { - // 나갈 때 애니메이션 (위에서 아래로 + 페이드아웃) - launch { offsetY.snapTo(120f) } - launch { alpha.snapTo(0.3f) } + // 나갈 때 애니메이션 (위에서 아래로 + 페이드아웃) + launch { offsetY.animateTo(120f, animationSpec = tween(300)) } + launch { alpha.animateTo(0.3f, animationSpec = tween(300)) } }Alternatively, if instant dismissal is preferred, update the comment:
} else { - // 나갈 때 애니메이션 (위에서 아래로 + 페이드아웃) + // 즉시 숨김 상태로 리셋 launch { offsetY.snapTo(120f) } launch { alpha.snapTo(0.3f) } }app/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceListBottomSheet.kt (1)
92-97: Pre-existing critical issue: Coroutine launched on every layout pass causes resource leak.The
scope.launchblock inside thelayoutmodifier is executed on every layout pass (triggered by size changes, recomposition, etc.), potentially launching multiple concurrent collectors for the same state. This accumulates coroutine jobs that are never cancelled, leading to a memory leak.🔎 Proposed fix: Move collection to LaunchedEffect
Remove the coroutine launch from inside the layout block (lines 92-97) and set up the collection in a separate
LaunchedEffectoutside the layout:val nestedScrollConnection = placeListBottomSheetNestedScrollConnection(bottomSheetState) + + LaunchedEffect(bottomSheetState.state) { + snapshotFlow { bottomSheetState.state.requireOffset() } + .collect { currentOffset -> + onScroll(currentOffset) + } + } Column( modifier = modifier .fillMaxSize() .layout { measurable, constraints -> val placeable = measurable.measure(constraints) // 실제 레이아웃 측정 시에만 앵커 설정 if (!isLookingAhead) { val screenHeightPx = constraints.maxHeight.toFloat() // 3가지 앵커 높이 정의 (DP) val halfExpandedOffsetPx = screenHeightPx - screenHeightPx * halfExpandedRatio val collapsedOffsetPx = with(density) { screenHeightPx - peekHeight.toPx() } val expandedOffsetPx = 0f // 화면 최상단 bottomSheetState.state.updateAnchors( newAnchors = DraggableAnchors { PlaceListBottomSheetValue.EXPANDED at expandedOffsetPx PlaceListBottomSheetValue.HALF_EXPANDED at halfExpandedOffsetPx PlaceListBottomSheetValue.COLLAPSED at collapsedOffsetPx }, newTarget = bottomSheetState.currentValue, ) - // 스크롤 되었을 때 호출하는 콜백 - scope.launch { - snapshotFlow { bottomSheetState.state.requireOffset() } - .collect { currentOffset -> - onScroll(currentOffset) - } - } }This ensures the collection lifecycle is properly managed and only one collector is active per composition.
app/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceListScreen.kt (1)
151-157:LaunchedEffect(Unit)may re-triggeronPlaceLoadon recomposition.Using
Unitas the key means this effect runs once when the composable enters composition in thePlaceLoadedstate. However, if the parent recomposes andPlaceListScreenis re-entered while still inPlaceLoaded, the effect will run again. Consider using a more stable key or tracking whether the load has been triggered.🔎 Proposed fix using a derived key
is ListLoadState.PlaceLoaded -> { - LaunchedEffect(Unit) { + LaunchedEffect(placesUiState) { scope.launch { currentOnPlaceLoad() } } }This ensures the effect only re-runs when
placesUiStateactually changes toPlaceLoadedagain.
🧹 Nitpick comments (21)
app/src/main/java/com/daedan/festabook/presentation/placeMap/component/BackToPositionButton.kt (1)
15-42: Component is well-structured.The
BackToPositionButtonimplementation is clean and follows Compose best practices. The hardcoded1.dpborder width at line 34 is acceptable as border widths are typically design constants. However, if you're aiming for full consistency with the spacing theme extraction mentioned in the PR objectives, you could consider adding aborderWidthconstant to your theme.app/src/main/java/com/daedan/festabook/FestaBookApp.kt (1)
72-76: Remove commented-out code.The commented-out conditional Timber setup logic should either be removed entirely or documented if there's a specific reason to keep it for future reference.
🔎 Proposed fix
private fun setupTimber() { plantDebugTimberTree() plantInfoTimberTree() -// if (BuildConfig.DEBUG) { -// plantDebugTimberTree() -// } else { -// plantInfoTimberTree() -// } Timber.plant(FirebaseCrashlyticsTree()) }app/src/main/java/com/daedan/festabook/presentation/placeMap/intent/handler/EventHandler.kt (1)
3-5: Consider adding KDoc for the public interface.As a public contract used by multiple handler implementations, KDoc would clarify the intent and usage pattern (e.g., that invoke is called to asynchronously handle an event).
📝 Suggested documentation
+/** + * Generic contract for asynchronous event handling. + * Implementations should process the given event and perform side effects as needed. + */ interface EventHandler<EVENT> { + /** + * Handles the given event asynchronously. + * @param event the event to process + */ suspend operator fun invoke(event: EVENT) }app/src/main/java/com/daedan/festabook/presentation/common/ObserveEvent.kt (2)
12-25: Simplify LaunchedEffect keys and reconsider withContext.Two observations:
LaunchedEffect key on
lifecycleOwner.lifecycle: Includinglifecycleas a key will restart the effect if the lifecycle owner changes, which is rare. Typically onlyflowis used as the key.Redundant
withContext(Dispatchers.Main.immediate):LaunchedEffectalready runs on the main dispatcher in Compose, andflow.collect(onEvent)invokesonEventin the collector's context (already main). ThewithContextis defensive but likely unnecessary unless the flow usesflowOn()internally.🔎 Proposed simplification
@Composable fun <T> ObserveAsEvents( flow: Flow<T>, onEvent: suspend (T) -> Unit, ) { val lifecycleOwner = LocalLifecycleOwner.current - LaunchedEffect(flow, lifecycleOwner.lifecycle) { - lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - withContext(Dispatchers.Main.immediate) { - flow.collect(onEvent) - } + LaunchedEffect(flow) { + lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + flow.collect(onEvent) } } }
12-16: Consider adding KDoc for the public Composable.This utility establishes a lifecycle-aware event observation pattern. Documenting the behavior (especially that events are only collected while STARTED) would help consumers understand when to use it versus other state collection mechanisms.
app/src/test/java/com/daedan/festabook/placeMap/PlaceLIstTestFixture.kt (1)
50-86: Consider refactoring duplicate fixture structure.
FAKE_PLACES_CATEGORY_FIXTURElargely duplicatesFAKE_PLACES(lines 12-48), differing only in the category of the second place. If category variation is the primary test concern, consider a factory function or parameterized fixture to reduce duplication.💡 Example factory approach
fun createFakePlaces(secondPlaceCategory: PlaceCategory = PlaceCategory.FOOD_TRUCK): List<Place> = listOf( Place( id = 1, category = PlaceCategory.FOOD_TRUCK, // ... other fields ), Place( id = 2, category = secondPlaceCategory, // ... other fields ), ) val FAKE_PLACES = createFakePlaces() val FAKE_PLACES_CATEGORY_FIXTURE = createFakePlaces(PlaceCategory.BAR)app/src/main/java/com/daedan/festabook/presentation/placeMap/component/Styles.kt (1)
1-25: LGTM!Extracting hardcoded dimensions into themed extension properties is a solid approach. The naming is clear, and grouping these in
Styles.ktkeeps design tokens centralized.Optional: Add a file-level KDoc summarizing the spacing constants for PlaceMap components, especially since
placeListBottomSheetHalfRatiois a ratio (Float) rather than a dimension (Dp).app/src/main/java/com/daedan/festabook/presentation/placeMap/intent/handler/ActionHandler.kt (1)
7-7: Consider renaming lambda parameter for clarity.The parameter name
beforein the function type((before: STATE) -> STATE)suggests temporal ordering, but it represents the current state being transformed. Considercurrentor simplystatefor better clarity in IDE hints and documentation.🔎 Suggested naming improvement
- val onUpdateState: ((before: STATE) -> STATE) -> Unit + val onUpdateState: ((current: STATE) -> STATE) -> Unitor simply:
- val onUpdateState: ((before: STATE) -> STATE) -> Unit + val onUpdateState: ((STATE) -> STATE) -> Unitapp/src/main/java/com/daedan/festabook/presentation/placeMap/intent/state/MapDelegate.kt (1)
23-29: Consider reordering Flow operators for clarity.The current operator order applies
distinctUntilChanged()beforefilterNotNull(), which means it compares nullable values. While this works correctly for the single-initialization use case, the more idiomatic pattern is:snapshotFlow { value } .filterNotNull() .distinctUntilChanged() .first()This ensures
distinctUntilChanged()only compares non-null NaverMap instances, making the intent clearer.🔎 Suggested operator reordering
suspend fun await(timeout: Duration = 3.seconds): NaverMap = withTimeout(timeout) { snapshotFlow { value } - .distinctUntilChanged() .filterNotNull() + .distinctUntilChanged() .first() }app/src/main/java/com/daedan/festabook/presentation/placeMap/intent/state/ListLoadState.kt (1)
12-14: Consider removingPlaceLoadedin favor ofSuccess<List<PlaceUiModel>>.
PlaceLoadedappears to duplicate the purpose ofSuccess<List<PlaceUiModel>>. Having both variants can confuse consumers about which to use and complicates exhaustivewhenexpressions.If
PlaceLoadedserves a semantic purpose (e.g., triggering specific side effects), document that distinction; otherwise, remove it to keep the API surface minimal.app/src/test/java/com/daedan/festabook/placeMap/handler/FilterActionHandlerTest.kt (1)
107-147: Duplicate test cases.These two tests (
선택된 카테고리가 부스, 주점, 푸드트럭에 해당되지 않을 때 전체 목록을 불러온다and기타 카테고리만 선택되었다면 전체 목록을 불러온다) test the same scenario with identicaltargetCategories(SMOKING_AREAandTOILET). Consider removing one or differentiating them with distinct category combinations.app/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceMapScreen.kt (2)
69-86: Consider conditional rendering instead ofalpha(0f)for performance.Using
alpha(0f)keepsPlaceListScreenin the composition tree even when hidden, potentially causing unnecessary layout and state management overhead. If the component is not visible, conditional rendering would be more efficient.🔎 Proposed approach
- PlaceListScreen( - modifier = - Modifier.alpha( - if (uiState.selectedPlace is LoadState.Empty) { - 1f - } else { - 0f - }, - ), + if (uiState.selectedPlace is LoadState.Empty) { + PlaceListScreen( + modifier = Modifier, ... - ) + ) + }However, if you need to preserve scroll state or avoid recomposition costs when toggling, the current approach is acceptable.
88-117: Redundantvisible = trueparameters.Both
PlaceDetailPreviewScreenandPlaceDetailPreviewSecondaryScreenare conditionally rendered based onuiState.isPlacePreviewVisibleanduiState.isPlaceSecondaryPreviewVisible, making thevisible = trueparameter redundant. Consider removing it or relying solely on one mechanism.app/src/main/java/com/daedan/festabook/presentation/placeMap/component/NaverMapContent.kt (1)
160-168: Missing cancellation handling insuspendCancellableCoroutine.If the coroutine is cancelled before
getMapAsynccompletes, the callback will still execute and attempt to resume a cancelled continuation. Consider addinginvokeOnCancellationor usingresumewith a check.🔎 Proposed improvement
private suspend fun MapView.getMapAndRunCallback(onMapReady: (NaverMap) -> Unit = {}): NaverMap = suspendCancellableCoroutine { continuation -> getMapAsync { map -> - onMapReady(map) - continuation.resumeWith( - Result.success(map), - ) + if (continuation.isActive) { + onMapReady(map) + continuation.resume(map) + } } }app/src/main/java/com/daedan/festabook/presentation/placeMap/intent/handler/FilterActionHandler.kt (2)
81-84: Consider makingunselectPlacea private function.The
unselectPlacefunction is private, which is correct. However,trySendon line 83 silently drops the event if the channel buffer is full. For critical UI state changes, consider logging or handling the case whentrySendfails.🔎 Proposed enhancement
private fun unselectPlace() { onUpdateState.invoke { it.copy(selectedPlace = LoadState.Empty) } - _mapControlUiEvent.trySend(MapControlEvent.UnselectMarker) + _mapControlUiEvent.trySend(MapControlEvent.UnselectMarker).also { result -> + if (result.isFailure) { + // Consider logging: marker unselect event dropped + } + } }
71-73: Fix typo in constant name:EMTPY_TIME_TAG_IDshould beEMPTY_TIME_TAG_ID.The constant
TimeTag.EMTPY_TIME_TAG_IDis misspelled (should beEMPTY). This typo is defined in the domain model and consistently used across multiple files (FilterActionHandler.kt, MapControlEventHandler.kt, MapFilterManagerImpl.kt, and others). Fixing it requires updating the constant definition in TimeTag.kt and all 8 usages throughout the presentation layer.app/src/main/java/com/daedan/festabook/presentation/placeMap/component/TimeTagMenu.kt (1)
54-73: Good state-driven pattern, but consider handling Loading/Error states explicitly.The
TimeTagMenunow correctly usesLoadStatewrappers. However, theelse -> Unitbranch silently ignoresLoadingandErrorstates. Consider whether users should see a loading indicator or error message when time tags fail to load.🔎 Optional: Add loading/error UI feedback
when (timeTagsState) { is LoadState.Success -> { if (selectedTimeTagState !is LoadState.Success) return TimeTagContent( title = selectedTimeTagState.value.name, timeTags = timeTagsState.value, modifier = modifier, onTimeTagClick = onTimeTagClick, ) } - - else -> Unit + is LoadState.Loading -> { + // Optional: Show shimmer or placeholder + } + is LoadState.Error -> { + // Optional: Show error indicator + } + is LoadState.Empty -> Unit }app/src/main/java/com/daedan/festabook/presentation/placeMap/intent/handler/SelectActionHandler.kt (2)
113-117: Parameter naming:itemshould beerrororthrowable.The
onFailurecallback receives aThrowable, but the parameter is nameditem, which is misleading.🔎 Proposed fix
- }.onFailure { item -> + }.onFailure { error -> onUpdateState.invoke { - it.copy(selectedPlace = LoadState.Error(item)) + it.copy(selectedPlace = LoadState.Error(error)) } }
27-36: Handler-to-handler dependency creates tight coupling.
SelectActionHandlerdepends directly onFilterActionHandler(line 30) to callupdatePlacesByTimeTag. While this works, it creates tight coupling between handlers. Consider extracting shared logic into a common service or using events for cross-handler communication.app/src/main/java/com/daedan/festabook/presentation/placeMap/PlaceMapViewModel.kt (2)
52-62: Channel overflow strategy may silently drop events.Both
_placeMapUiEventand_mapControlUiEventuseBufferOverflow.DROP_OLDEST, which silently discards events when the buffer is full. For UI events likeShowErrorSnackBaror map control commands, consider usingDROP_LATEST(drop new events) or increasing buffer capacity to avoid losing important events.🔎 Alternative with explicit capacity
private val _placeMapUiEvent = Channel<PlaceMapEvent>( + capacity = Channel.BUFFERED, onBufferOverflow = BufferOverflow.DROP_OLDEST, )
186-200: Redundant nestedlaunch.The inner
launchat line 189 is unnecessary since the outerlaunchalready provides the coroutine scope. This adds minor overhead and reduces readability.🔎 Proposed simplification
@OptIn(FlowPreview::class) private fun observeErrorEvent() { viewModelScope.launch { - launch { - uiState - .map { it.hasAnyError } - .distinctUntilChanged() - .filterIsInstance<LoadState.Error>() - .debounce(1000) - .collect { - _placeMapUiEvent.send(PlaceMapEvent.ShowErrorSnackBar(it)) - } - } + uiState + .map { it.hasAnyError } + .distinctUntilChanged() + .filterIsInstance<LoadState.Error>() + .debounce(1000) + .collect { + _placeMapUiEvent.send(PlaceMapEvent.ShowErrorSnackBar(it)) + } } }
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (77)
app/src/main/java/com/daedan/festabook/FestaBookApp.ktapp/src/main/java/com/daedan/festabook/di/mapManager/MapManagerBindings.ktapp/src/main/java/com/daedan/festabook/di/placeMapHandler/CachedPlaceByTimeTag.ktapp/src/main/java/com/daedan/festabook/di/placeMapHandler/CachedPlaces.ktapp/src/main/java/com/daedan/festabook/di/placeMapHandler/PlaceMapHandlerGraph.ktapp/src/main/java/com/daedan/festabook/di/placeMapHandler/PlaceMapViewModelScope.ktapp/src/main/java/com/daedan/festabook/presentation/common/ObserveEvent.ktapp/src/main/java/com/daedan/festabook/presentation/placeDetail/CategoryView.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/PlaceMapFragment.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/PlaceMapViewModel.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/component/BackToPositionButton.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/component/CurrentLocationButton.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/component/NaverMapContent.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/component/OffsetDependentLayout.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceCategoryLabel.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceCategoryScreen.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceDetailPreviewScreen.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceDetailPreviewSecondaryScreen.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceListBottomSheet.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceListBottomSheetState.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceListScreen.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceMapScreen.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/component/PreviewAnimatableBox.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/component/Styles.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/component/TimeTagMenu.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/intent/action/FilterAction.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/intent/action/MapEventAction.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/intent/action/PlaceMapAction.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/intent/action/SelectAction.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/intent/event/MapControlEvent.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/intent/event/PlaceMapEvent.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/intent/handler/ActionHandler.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/intent/handler/EventHandler.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/intent/handler/FilterActionHandler.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/intent/handler/MapControlEventHandler.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/intent/handler/MapEventActionHandler.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/intent/handler/PlaceMapEventHandler.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/intent/handler/SelectActionHandler.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/intent/state/ListLoadState.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/intent/state/LoadState.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/intent/state/MapDelegate.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/intent/state/MapManagerDelegate.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/intent/state/PlaceMapUiState.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/intent/state/StateExt.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/listener/MapClickListener.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/listener/MapClickListenerImpl.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/listener/OnCameraChangeListener.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/mapManager/MapCameraManager.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/mapManager/MapManager.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/mapManager/internal/MapCameraManagerImpl.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/mapManager/internal/MapFilterManagerImpl.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/mapManager/internal/MapMarkerManagerImpl.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/model/PlaceListUiState.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/model/SelectedPlaceUiState.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/placeCategory/PlaceCategoryFragment.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/PlaceDetailPreviewFragment.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/PlaceDetailPreviewSecondaryFragment.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/placeList/OnPlaceClickListener.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/placeList/PlaceListFragment.ktapp/src/main/java/com/daedan/festabook/presentation/placeMap/placeList/PlaceListViewModel.ktapp/src/main/res/layout/activity_place_detail.xmlapp/src/main/res/layout/fragment_place_category.xmlapp/src/main/res/layout/fragment_place_detail_preview.xmlapp/src/main/res/layout/fragment_place_detail_preview_secondary.xmlapp/src/main/res/layout/fragment_place_list.xmlapp/src/main/res/layout/item_place_list.xmlapp/src/main/res/layout/item_place_list_skeleton.xmlapp/src/test/java/com/daedan/festabook/FlowExtensions.ktapp/src/test/java/com/daedan/festabook/placeDetail/PlaceDetailTestFixture.ktapp/src/test/java/com/daedan/festabook/placeDetail/PlaceDetailViewModelTest.ktapp/src/test/java/com/daedan/festabook/placeList/PlaceListViewModelTest.ktapp/src/test/java/com/daedan/festabook/placeList/PlaceMapViewModelTest.ktapp/src/test/java/com/daedan/festabook/placeMap/PlaceLIstTestFixture.ktapp/src/test/java/com/daedan/festabook/placeMap/PlaceMapViewModelTest.ktapp/src/test/java/com/daedan/festabook/placeMap/handler/FilterActionHandlerTest.ktapp/src/test/java/com/daedan/festabook/placeMap/handler/MapEventActionHandlerTest.ktapp/src/test/java/com/daedan/festabook/placeMap/handler/SelectActionHandlerTest.kt
💤 Files with no reviewable changes (16)
- app/src/main/res/layout/fragment_place_detail_preview.xml
- app/src/main/java/com/daedan/festabook/presentation/placeMap/placeList/OnPlaceClickListener.kt
- app/src/main/java/com/daedan/festabook/presentation/placeMap/model/PlaceListUiState.kt
- app/src/main/java/com/daedan/festabook/presentation/placeMap/placeList/PlaceListViewModel.kt
- app/src/main/res/layout/fragment_place_detail_preview_secondary.xml
- app/src/main/java/com/daedan/festabook/presentation/placeMap/placeCategory/PlaceCategoryFragment.kt
- app/src/main/res/layout/item_place_list.xml
- app/src/test/java/com/daedan/festabook/placeList/PlaceListViewModelTest.kt
- app/src/main/java/com/daedan/festabook/presentation/placeMap/placeList/PlaceListFragment.kt
- app/src/main/java/com/daedan/festabook/presentation/placeMap/model/SelectedPlaceUiState.kt
- app/src/main/res/layout/item_place_list_skeleton.xml
- app/src/test/java/com/daedan/festabook/placeList/PlaceMapViewModelTest.kt
- app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/PlaceDetailPreviewFragment.kt
- app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/PlaceDetailPreviewSecondaryFragment.kt
- app/src/main/res/layout/fragment_place_list.xml
- app/src/main/res/layout/fragment_place_category.xml
🧰 Additional context used
🧬 Code graph analysis (6)
app/src/test/java/com/daedan/festabook/placeMap/handler/FilterActionHandlerTest.kt (2)
app/src/test/java/com/daedan/festabook/FlowExtensions.kt (1)
observeEvent(11-19)app/src/main/java/com/daedan/festabook/di/placeMapHandler/PlaceMapHandlerGraph.kt (1)
filterActionHandler(18-38)
app/src/main/java/com/daedan/festabook/presentation/placeMap/PlaceMapFragment.kt (3)
app/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceListBottomSheetState.kt (1)
rememberPlaceListBottomSheetState(63-75)app/src/main/java/com/daedan/festabook/presentation/common/ObserveEvent.kt (1)
ObserveAsEvents(12-25)app/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceMapScreen.kt (1)
PlaceMapScreen(23-121)
app/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceMapScreen.kt (7)
app/src/main/java/com/daedan/festabook/presentation/placeMap/component/NaverMapContent.kt (1)
NaverMapContent(26-46)app/src/main/java/com/daedan/festabook/presentation/placeMap/component/TimeTagMenu.kt (1)
TimeTagMenu(52-73)app/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceCategoryScreen.kt (1)
PlaceCategoryScreen(30-83)app/src/main/java/com/daedan/festabook/presentation/placeMap/component/NaverMapLogo.kt (1)
NaverMapLogo(10-18)app/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceListScreen.kt (1)
PlaceListScreen(51-161)app/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceDetailPreviewScreen.kt (1)
PlaceDetailPreviewScreen(33-62)app/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceDetailPreviewSecondaryScreen.kt (1)
PlaceDetailPreviewSecondaryScreen(31-92)
app/src/test/java/com/daedan/festabook/placeMap/PlaceMapViewModelTest.kt (1)
app/src/test/java/com/daedan/festabook/FlowExtensions.kt (1)
observeEvent(11-19)
app/src/test/java/com/daedan/festabook/placeMap/handler/MapEventActionHandlerTest.kt (1)
app/src/test/java/com/daedan/festabook/FlowExtensions.kt (1)
observeEvent(11-19)
app/src/test/java/com/daedan/festabook/placeMap/handler/SelectActionHandlerTest.kt (1)
app/src/test/java/com/daedan/festabook/FlowExtensions.kt (1)
observeEvent(11-19)
🔇 Additional comments (66)
app/src/main/java/com/daedan/festabook/di/mapManager/MapManagerBindings.kt (1)
4-5: LGTM! Clean package reorganization.The import paths have been correctly updated to reflect the new
listenersubpackage structure. The DI binding logic at line 28 remains consistent with these changes.app/src/main/java/com/daedan/festabook/presentation/placeDetail/CategoryView.kt (1)
1-1: LGTM! Package refactoring aligns with usage context.The package move from
placeMap.placeListtoplaceDetailcorrectly reflects where this component is actually used (in the place detail screen). The refactoring maintains the import ofPlaceCategoryUiModelfrom theplaceMap.modelpackage, which appears intentional for keeping shared models in their original location.app/src/main/res/layout/activity_place_detail.xml (1)
79-79: LGTM! XML reference correctly updated.The CategoryView reference has been properly updated to reflect the new package location (
com.daedan.festabook.presentation.placeDetail.CategoryView), maintaining consistency with the refactored Kotlin class.app/src/main/java/com/daedan/festabook/presentation/placeMap/component/BackToPositionButton.kt (1)
1-1: LGTM! Package reorganization aligns with component consolidation.The package move from
placeList.componenttoplaceMap.componentis consistent with the broader refactoring to consolidate PlaceMap-related components under a unified namespace.app/src/main/java/com/daedan/festabook/presentation/placeMap/component/PreviewAnimatableBox.kt (1)
1-1: Package consolidation looks good.The move to the flatter
placeMap.componentpackage aligns with the PR's goal of reorganizing and consolidating UI components.app/src/main/java/com/daedan/festabook/presentation/placeMap/component/CurrentLocationButton.kt (1)
1-1: Package reorganization is complete.The package refactor from
placeMap.placeList.componenttoplaceMap.componentaligns with the PR's goal of consolidating Compose components into a unified namespace, and all references have been properly updated across the codebase.app/src/main/java/com/daedan/festabook/presentation/placeMap/component/OffsetDependentLayout.kt (1)
1-1: Package consolidation is complete—no broken imports.All usages of OffsetDependentLayout have been updated. The component is only used in PlaceListScreen.kt, which is in the same consolidated package (com.daedan.festabook.presentation.placeMap.component), so no explicit imports are needed. No lingering references to the old package path exist.
app/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceListBottomSheet.kt (1)
1-1: LGTM! Package consolidation aligns with refactoring goals.The package rename from
placeMap.placeList.componenttoplaceMap.componentappropriately reflects the removal of nested fragment structure and consolidates components into a unified package.app/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceListBottomSheetState.kt (1)
1-1: LGTM! Package consolidation aligns with the component-based architecture.The package move from
placeMap.placeList.componenttoplaceMap.componentconsolidates UI components under a unified namespace, which simplifies the structure and aligns with the broader Compose refactor.app/src/main/java/com/daedan/festabook/di/placeMapHandler/CachedPlaces.kt (1)
1-6: LGTM! Standard DI qualifier for disambiguating cached place dependencies.The
@CachedPlacesqualifier follows proper Metro DI conventions and will help distinguish between different StateFlow instances in the handler graph.app/src/main/java/com/daedan/festabook/di/placeMapHandler/PlaceMapViewModelScope.kt (1)
1-3: LGTM! Proper scope marker implementation for DI.The abstract class with private constructor correctly implements the scope marker pattern, ensuring it can only be used as a type-level scope indicator in the DI graph.
app/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceCategoryScreen.kt (1)
1-1: LGTM! Package consolidation consistent with component reorganization.Moving from
placeMap.placeCategory.componenttoplaceMap.componentaligns with the unified component namespace established in this refactor.app/src/main/java/com/daedan/festabook/presentation/placeMap/listener/OnCameraChangeListener.kt (1)
1-1: LGTM! Listener interfaces properly organized into dedicated subpackage.Moving camera listener to the
placeMap.listenersubpackage improves code organization by grouping related listener interfaces together.app/src/test/java/com/daedan/festabook/placeMap/PlaceLIstTestFixture.kt (1)
163-178: LGTM!The
FAKE_INITIAL_MAP_SETTINGfixture is well-structured and provides clear test data for map initialization scenarios.app/src/main/java/com/daedan/festabook/presentation/placeMap/mapManager/MapCameraManager.kt (1)
3-3: LGTM!Clean package reorganization—moving
OnCameraChangeListenerto thelistenersubpackage improves structure.app/src/test/java/com/daedan/festabook/placeDetail/PlaceDetailViewModelTest.kt (1)
7-7: LGTM! Test fixture import updated correctly.The import path change aligns with the test fixture reorganization to the placeMap package. The test logic remains unchanged and continues to validate PlaceDetailViewModel behavior correctly.
app/src/main/java/com/daedan/festabook/presentation/placeMap/mapManager/internal/MapMarkerManagerImpl.kt (1)
4-4: LGTM! Package reorganization applied correctly.The MapClickListener import has been updated to reflect the new listener subpackage structure. No functional changes.
app/src/main/java/com/daedan/festabook/presentation/placeMap/intent/action/PlaceMapAction.kt (1)
1-3: LGTM! Clean MVI action hierarchy base.The sealed interface provides a type-safe foundation for the MVI action hierarchy, enabling the ViewModel to route FilterAction, MapEventAction, and SelectAction through a unified entry point.
app/src/main/java/com/daedan/festabook/presentation/placeMap/mapManager/internal/MapCameraManagerImpl.kt (1)
4-4: LGTM! Package reorganization applied correctly.The OnCameraChangeListener import has been updated to reflect the new listener subpackage structure. No functional changes.
app/src/main/java/com/daedan/festabook/presentation/placeMap/mapManager/MapManager.kt (1)
7-7: LGTM! Package reorganization applied correctly.The MapClickListener import has been updated to reflect the new listener subpackage structure. This completes the consistent reorganization of listener interfaces across all map manager components.
app/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceCategoryLabel.kt (1)
24-24: Good refactor to use theme-based spacing.Replacing the hard-coded
4.dpwithfestabookSpacing.paddingBody1improves maintainability and aligns with the design system standardization introduced in this PR.Also applies to: 55-55
app/src/main/java/com/daedan/festabook/di/placeMapHandler/CachedPlaceByTimeTag.kt (1)
1-6: LGTM! Clean DI qualifier implementation.The annotation is correctly defined using Metro's
@Qualifierto distinguish cached place data by time tag. This complements the existingCachedPlacesqualifier and supports the PR's handler-based DI architecture.app/src/main/java/com/daedan/festabook/presentation/placeMap/listener/MapClickListener.kt (1)
1-1: Good organizational refactor.Moving the listener interface to a dedicated
listenersubpackage improves code organization and makes the package structure more logical.app/src/main/java/com/daedan/festabook/presentation/placeMap/listener/MapClickListenerImpl.kt (1)
1-4: Excellent MVI refactor!The migration from direct ViewModel method calls (
selectPlace,unselectPlace) to action-based dispatch (onPlaceMapAction) is a clean implementation of the MVI pattern. This centralizes user interactions through a single action channel, improving testability and making the data flow more traceable.Also applies to: 16-18, 24-24
app/src/test/java/com/daedan/festabook/placeDetail/PlaceDetailTestFixture.kt (1)
9-9: No action required. The import path change tocom.daedan.festabook.placeMap.FAKE_PLACESis correct. FAKE_PLACES is defined at the new location as a non-emptyList<Place>and supports the.first()call used at line 15.app/src/main/java/com/daedan/festabook/presentation/placeMap/intent/state/MapManagerDelegate.kt (1)
8-14: Clean delegate implementation.The MapManagerDelegate follows good encapsulation practices with a public readable property and controlled mutation via
init(). The use ofmutableStateOfensures Compose reactivity.app/src/main/java/com/daedan/festabook/presentation/placeMap/intent/action/FilterAction.kt (1)
5-11: LGTM!Clean and idiomatic sealed interface definition for filter-related actions. The use of
data classforOnCategoryClick(with state) anddata objectforOnPlaceLoad(stateless) is appropriate for the MVI action pattern.app/src/main/java/com/daedan/festabook/presentation/placeMap/intent/action/MapEventAction.kt (1)
5-15: LGTM!Well-structured sealed interface for map event actions, consistent with the action pattern used elsewhere in the PR. The distinction between stateless (
data object) and stateful (data class) actions is correctly applied.app/src/main/java/com/daedan/festabook/presentation/placeMap/intent/action/SelectAction.kt (1)
7-27: LGTM!Well-structured action hierarchy covering selection, navigation, and state-related user intents. The use of domain types (
TimeTag,PlaceDetailUiModel) maintains type safety.app/src/test/java/com/daedan/festabook/placeMap/handler/SelectActionHandlerTest.kt (1)
77-101: Good test coverage for the happy path.Test correctly verifies repository interaction, UI state update, and event emission for
OnPlaceClick. The use ofobserveEventto capture async events is appropriate.app/src/main/java/com/daedan/festabook/presentation/placeMap/intent/state/StateExt.kt (1)
9-14: Potential indefinite suspension ifRis never emitted.
first()will suspend forever if the selector never produces an instance ofR. Consider adding a timeout or documenting this behavior clearly for callers.// Example: This will hang if selectedPlace never becomes Success val place: LoadState.Success<PlaceDetailUiModel> = uiState.await { it.selectedPlace }If indefinite suspension is acceptable by design, add KDoc documenting the contract. Otherwise, consider
firstOrNull()with a timeout:suspend inline fun <reified R> StateFlow<PlaceMapUiState>.await( timeout: Duration = 30.seconds, crossinline selector: (PlaceMapUiState) -> Any? ): R = withTimeout(timeout) { this@await .map { selector(it) } .distinctUntilChanged() .filterIsInstance<R>() .first() }app/src/main/java/com/daedan/festabook/presentation/placeMap/intent/state/LoadState.kt (2)
6-18: LGTM!Well-designed sealed interface with proper covariance (
out T). Usingdata objectforLoadingandEmptyensures stable equality semantics, andLoadState<Nothing>forErrorcorrectly supports covariant type projections.
20-20:SECONDARY_CATEGORIESis properly defined inPlaceCategoryUiModel.Companion.The constant is defined at
PlaceCategoryUiModel.kt:28within the companion object and is correctly referenced in the extension property. No action needed.app/src/test/java/com/daedan/festabook/placeMap/handler/FilterActionHandlerTest.kt (2)
79-105: LGTM!The test correctly validates that category selection triggers a
FilterMapByCategoryevent and updates the UI state. The assertion structure with separate state and event verification is appropriate.
149-228: LGTM!The remaining tests provide good coverage for filter release, time tag filtering, and the
OnPlaceLoadaction. The test setup properly initializes state before exercising the handler.app/src/main/java/com/daedan/festabook/presentation/placeMap/intent/event/PlaceMapEvent.kt (1)
7-27: LGTM!Clean sealed interface design for one-shot UI events. The use of
LoadState.SuccessandLoadState.Errorwrappers provides type safety and enforces correct state at the event boundary.app/src/main/java/com/daedan/festabook/presentation/placeMap/intent/handler/MapEventActionHandler.kt (1)
36-59: LGTM!The remaining action handlers are well-implemented. The logging in
OnBackToInitialPositionClickfollows a consistent pattern, and the visibility computation inOnMapDragcorrectly combines both preview states.app/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceMapScreen.kt (1)
23-30: LGTM on overall screen structure!The function signature is clean with a single
onActioncallback for all user interactions, following MVI principles. The state-driven rendering fromPlaceMapUiStateprovides a clear, predictable UI flow.app/src/main/java/com/daedan/festabook/presentation/placeMap/intent/event/MapControlEvent.kt (2)
19-23: Consider ifLoadState<TimeTag>is necessary forselectedTimeTag.
SetMarkerByTimeTagcarriesselectedTimeTag: LoadState<TimeTag>, but if this event is only emitted when a time tag is successfully selected, a plainTimeTag(orTimeTag?) might be simpler. Verify that consumers need to handle Loading/Error/Empty states for this field.
10-34: LGTM!The sealed interface provides a comprehensive set of map control events. Using
data objectfor stateless events (InitMap, BackToInitialPosition, UnselectMarker) anddata classfor events with payloads is idiomatic Kotlin.app/src/main/java/com/daedan/festabook/presentation/placeMap/component/NaverMapContent.kt (1)
36-45: LGTM!The map initialization flow is well-structured:
LaunchedEffectkeyed onmapViewensures single initialization, the suspendinggetMapAndRunCallbackinvokesonMapReady, then the delegate is initialized. Thecontentslot receives the map frommapDelegate.value.app/src/main/java/com/daedan/festabook/presentation/placeMap/intent/state/PlaceMapUiState.kt (3)
27-37: LGTM!The
hasAnyErroraggregator provides a clean way to detect the first error across multiple state fields. ConvertingListLoadState.ErrortoLoadState.Errormaintains consistency in error handling.
10-19: LGTM on state structure!The
PlaceMapUiStateprovides a comprehensive single source of truth for the place-map UI. Sensible defaults (LoadState.Loading,LoadState.Empty) and the separation betweenLoadState(single values) andListLoadState(collections) is appropriate.
21-25: No action required. TheisSecondaryproperty is correctly defined as an extension property onLoadState.Success<PlaceDetailUiModel>in LoadState.kt (line 20). The code at lines 21-25 will compile and execute correctly.Likely an incorrect or invalid review comment.
app/src/main/java/com/daedan/festabook/presentation/placeMap/intent/handler/PlaceMapEventHandler.kt (2)
13-24: Well-structured event handler with clean dependency injection.The constructor clearly separates Android-dependent callbacks from platform-agnostic dependencies. The TODO comment on line 19 appropriately flags future Compose migration work.
26-56: LGTM on event handling logic.The
whenexpression cleanly dispatches each event type. The early returns inMenuItemReClicked(line 34) andMapViewDrag(line 52) correctly guard against unnecessary operations when conditions aren't met.app/src/main/java/com/daedan/festabook/di/placeMapHandler/PlaceMapHandlerGraph.kt (1)
18-37: Clean DI graph structure using Metro's graph extension pattern.The
PlaceMapHandlerGraphproperly separates handler creation into a factory with explicit@Providesannotations. The use of qualifier annotations (@CachedPlaces,@CachedPlaceByTimeTag) correctly disambiguates the twoStateFlow<List<PlaceUiModel>>parameters.app/src/test/java/com/daedan/festabook/placeMap/PlaceMapViewModelTest.kt (1)
239-252: Good test for event emission usingobserveEventhelper.The test correctly uses the
observeEventextension function fromFlowExtensions.ktto observe theplaceMapUiEventchannel and verify theMenuItemReClickedevent is emitted.app/src/main/java/com/daedan/festabook/presentation/placeMap/component/TimeTagMenu.kt (1)
148-152: Good use of design system spacing constant.The hardcoded width value has been replaced with
festabookSpacing.timeTagButtonWidth, improving maintainability and consistency.app/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceListScreen.kt (2)
99-101: Good migration to design system constants.The bottom sheet configuration now uses
festabookSpacing.placeListBottomSheetPeekHeightandfestabookSpacing.placeListBottomSheetHalfRatioinstead of hardcoded values, improving consistency.
73-97: Conditional layout visibility based on bottom sheet state.The
OffsetDependentLayoutis now hidden when the bottom sheet isEXPANDED. This is a sensible UX improvement to avoid UI clutter when the list is fully expanded.app/src/main/java/com/daedan/festabook/presentation/placeMap/intent/handler/FilterActionHandler.kt (1)
86-97:updatePlacesByTimeTagis public but only used internally and bySelectActionHandler.This cross-handler dependency is a design choice. Consider documenting this coupling or extracting shared logic to a common utility if more handlers need this functionality in the future.
app/src/main/java/com/daedan/festabook/presentation/placeMap/intent/handler/MapControlEventHandler.kt (4)
31-41: LGTM!The
InitMaphandler correctly awaits the map, registers a location listener for analytics, and assigns the location source.
43-60: LGTM!The guard
if (mapManager == null)ensures one-time initialization, andmapManager?.setupBackToInitialPositioncorrectly re-evaluates the delegate afterinit().
66-84: LGTM!Marker setup and time-tag filtering logic handles
SuccessandEmptystates appropriately, with safe fallthrough for other states.
86-124: LGTM!Category filtering and marker selection/unselection logic is correctly implemented with null-safe
mapManageraccess.app/src/main/java/com/daedan/festabook/presentation/placeMap/PlaceMapFragment.kt (3)
93-115: Handler creation inremembermay miss recomposition-safe dependencies.
MapControlEventHandlerandPlaceMapEventHandlerare created inremember {}without keys. While this avoids recreation, some dependencies (e.g.,density,appGraph.defaultFirebaseLogger) are stable, butlocationSource,placeMapViewModel, and callback lambdas are captured by reference. If any of these change across the Fragment lifecycle, the handlers will hold stale references.Consider using
rememberUpdatedStatefor callbacks or passing stable references via DI to ensure handler instances remain valid.
117-132: LGTM!Event observation via
ObserveAsEventsand thePlaceMapScreencomposition are correctly wired with proper state collection and action dispatch.
137-140: LGTM!Menu re-click correctly dispatches the
UnSelectPlaceaction before triggering the re-click event.app/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceDetailPreviewSecondaryScreen.kt (2)
41-51: LGTM!
BackHandleris correctly scoped tovisiblestate, and click handling properly forwards the currentselectedPlace.
58-90: LGTM!The Success branch correctly renders the category icon and place title with proper theme spacing.
app/src/main/java/com/daedan/festabook/presentation/placeMap/PlaceMapViewModel.kt (2)
64-92: LGTM!Handler graph creation via factory and action routing in
onPlaceMapActionprovide clean separation of concerns. Thewhenexpression ensures exhaustive handling of allPlaceMapActionsubtypes.
146-184: LGTM!Parallel loading of organization geography and place geographies via nested
launchis appropriate. State updates correctly wrap results inLoadState.app/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceDetailPreviewScreen.kt (3)
42-50: LGTM!
BackHandlerand click handling are correctly implemented, consistent withPlaceDetailPreviewSecondaryScreen.
64-168: LGTM!
PlaceDetailPreviewContentcorrectly renders place details with consistent theme spacing and proper null handling for optional fields.
170-195: LGTM!
formattedDatehelper handles null times gracefully, and preview correctly usesLoadState.Successwith test fixtures.
...c/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceDetailPreviewScreen.kt
Show resolved
Hide resolved
...va/com/daedan/festabook/presentation/placeMap/component/PlaceDetailPreviewSecondaryScreen.kt
Show resolved
Hide resolved
...ain/java/com/daedan/festabook/presentation/placeMap/intent/handler/MapControlEventHandler.kt
Outdated
Show resolved
Hide resolved
...main/java/com/daedan/festabook/presentation/placeMap/intent/handler/MapEventActionHandler.kt
Show resolved
Hide resolved
app/src/test/java/com/daedan/festabook/placeMap/handler/SelectActionHandlerTest.kt
Show resolved
Hide resolved
app/src/test/java/com/daedan/festabook/placeMap/handler/SelectActionHandlerTest.kt
Show resolved
Hide resolved
| @Test | ||
| fun `카테고리가 기타시설일 떄에도 플레이스 상세를 선택할 수 있다`() = | ||
| runTest { | ||
| // given | ||
| coEvery { placeDetailRepository.getPlaceDetail(1) } returns | ||
| Result.success( | ||
| FAKE_ETC_PLACE_DETAIL, | ||
| ) | ||
| val eventResult = observeEvent(mapControlUiEvent.receiveAsFlow()) | ||
|
|
||
| // when | ||
| selectActionHandler(SelectAction.OnPlaceClick(1)) | ||
| advanceUntilIdle() | ||
|
|
||
| // then | ||
| val event = eventResult.await() | ||
| advanceUntilIdle() | ||
|
|
||
| val expected = LoadState.Success(FAKE_ETC_PLACE_DETAIL.toUiModel()) | ||
| val actual = uiState.value.selectedPlace | ||
| assertThat(actual).isEqualTo(expected) | ||
| assertThat(event).isEqualTo(MapControlEvent.SelectMarker(expected)) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor typo in test name.
Line 104: "떄" should be "때" (카테고리가 기타시설일 때에도).
🤖 Prompt for AI Agents
In
app/src/test/java/com/daedan/festabook/placeMap/handler/SelectActionHandlerTest.kt
around lines 103 to 125, the test function name contains a typo: change "떄" to
the correct Korean "때" in the test name declaration so the function reads `fun
\`카테고리가 기타시설일 때에도 플레이스 상세를 선택할 수 있다\`() = ...`; update the function name only
(no behavior changes) and run tests to ensure naming compiles.
| // then | ||
| coVerify(exactly = 1) { fakeHandlerGraph.filterActionHandler } | ||
| coVerify(exactly = 1) { fakeHandlerGraph.selectActionHandler } | ||
| coVerify(exactly = 1) { fakeHandlerGraph.mapEventActionHandler } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test verifies handler property access, not handler invocation.
The coVerify calls check that fakeHandlerGraph.filterActionHandler etc. were accessed (getter called), but don't verify that the handler's invoke method was actually called with the action. Consider verifying the actual handler invocation.
🔎 Proposed fix to verify handler invocation
- coVerify(exactly = 1) { fakeHandlerGraph.filterActionHandler }
- coVerify(exactly = 1) { fakeHandlerGraph.selectActionHandler }
- coVerify(exactly = 1) { fakeHandlerGraph.mapEventActionHandler }
+ val mockFilterHandler = mockk<FilterActionHandler>(relaxed = true)
+ val mockSelectHandler = mockk<SelectActionHandler>(relaxed = true)
+ val mockMapEventHandler = mockk<MapEventActionHandler>(relaxed = true)
+ every { fakeHandlerGraph.filterActionHandler } returns mockFilterHandler
+ every { fakeHandlerGraph.selectActionHandler } returns mockSelectHandler
+ every { fakeHandlerGraph.mapEventActionHandler } returns mockMapEventHandler
+ // Then verify invoke was called
+ coVerify { mockSelectHandler.invoke(SelectAction.UnSelectPlace) }
+ coVerify { mockFilterHandler.invoke(FilterAction.OnPlaceLoad) }
+ coVerify { mockMapEventHandler.invoke(MapEventAction.OnMapDrag) }Note: The mock setup would need to be moved to before ViewModel creation for this to work properly.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In app/src/test/java/com/daedan/festabook/placeMap/PlaceMapViewModelTest.kt
around lines 233 to 237, the test currently verifies only that handler
properties were accessed (getters), not that the handlers were invoked; change
the assertions to verify the handler invocation instead (e.g., coVerify that
fakeHandlerGraph.filterActionHandler.invoke(...) was called with the expected
action or any()), and do the same for selectActionHandler and
mapEventActionHandler; to make this work move or create the mock handler
instances and their stubbing before constructing the ViewModel so the ViewModel
holds the mock handlers and the coVerify checks the invoke calls rather than
property access.
주석 처리되어 있던 `setGlobalExceptionHandler()` 메서드 호출을 복구하여, 앱 실행 중 발생하는 예외를 전역적으로 처리하는 로직을 다시 활성화했습니다.
- **`FestaBookApp.kt` 수정:**
- `onCreate` 메서드 내에서 주석으로 막혀있던 `setGlobalExceptionHandler()` 호출부의 주석을 해제했습니다.
UI 컴포지션 단계에서 발생하던 부수 효과(Side Effect)를 `LaunchedEffect`로 이동시켜 안정성을 높이고, 불필요한 에러 및 빈 상태 콜백을 제거하여 코드를 간소화했습니다.
- **`PlaceListScreen.kt` 리팩토링:**
- UI 렌더링 로직(`when`) 내에서 직접 호출되던 `onPlaceLoadFinish`와 `onPlaceLoad` 콜백을 `LaunchedEffect` 블록으로 이동했습니다. 이를 통해 상태 변화(`placesUiState`)에 따라 부수 효과가 안전하게 실행되도록 개선했습니다.
- 사용하지 않는 `onError` 파라미터와 관련 호출 코드를 삭제했습니다.
- **장소 상세 미리보기 화면 수정 (`PlaceDetailPreviewScreen`, `PlaceDetailPreviewSecondaryScreen`):**
- `LoadState.Error` 및 `LoadState.Empty` 상태를 처리하기 위해 존재했던 `onError`, `onEmpty` 콜백 파라미터를 제거했습니다.
- `Success` 상태가 아닐 경우 별도의 처리 없이 빈 화면을 유지하도록 `when` 분기문을 간소화했습니다.
Flow 기반의 단위 테스트 편의성을 위해 유틸리티 함수를 개선하고, `PlaceMapViewModel`의 데이터 로딩 실패 시나리오에 대한 테스트 케이스를 추가했습니다.
- **`FlowExtensions.kt` 수정:**
- `observeEvent`에 타임아웃(`3.seconds`)을 적용하여 테스트가 무한 대기하는 현상을 방지했습니다.
- Flow에서 발생하는 여러 개의 이벤트를 리스트로 수집하여 검증할 수 있는 `observeMultipleEvent` 확장 함수를 추가했습니다.
- **`PlaceMapViewModelTest.kt` 테스트 추가:**
- 장소 데이터(`getPlaces`)나 지리 정보(`getPlaceGeographies`) 로딩 실패 시, `PlaceMapEvent.ShowErrorSnackBar` 이벤트가 정상적으로 발행되는지 확인하는 테스트 케이스 2종을 추가했습니다.
- **핸들러 테스트 리팩토링:**
- `FilterActionHandlerTest.kt` 및 `MapEventActionHandlerTest.kt`에서 수동으로 코루틴을 실행하여 이벤트를 수집하던 로직을 `observeMultipleEvent`로 대체하여 가독성을 높였습니다.
- `FilterActionHandlerTest`에서 필터 카테고리 클릭 시 `UnselectMarker` 이벤트와 `FilterMapByCategory` 이벤트가 순서대로 발생하는지 `containsExactly`를 통해 명확히 검증하도록 수정했습니다.
`ListLoadState`의 제네릭 타입을 공변적으로 변경하고, `Loading` 및 `Error` 상태를 단순화하여 상태 관리 구조를 개선했습니다. 또한, 장소 선택 시 마커 이벤트에 전달되는 데이터의 일관성을 확보했습니다.
- **`ListLoadState.kt` 리팩토링:**
- `ListLoadState` 인터페이스를 공변(`out T`)으로 변경했습니다.
- `Loading`을 `class`에서 `data object`로 변경하여 싱글톤으로 관리되도록 하고, 불필요한 제네릭 타입을 `Nothing`으로 처리했습니다.
- `Error` 상태 또한 제네릭 타입 `T`를 제거하고 `ListLoadState<Nothing>`을 구현하도록 수정했습니다.
- **`PlaceMapUiState.kt` 수정:**
- `places` 필드의 초기값을 변경된 구조에 맞춰 `ListLoadState.Loading()` 생성자 호출에서 `ListLoadState.Loading` 객체 참조로 수정했습니다.
- **`SelectActionHandler.kt` 수정:**
- 장소 상세 정보 로드 성공 시, 새로운 `LoadState.Success` 객체를 변수에 할당하여 상태 업데이트와 `MapControlEvent.SelectMarker` 이벤트 전송에 동일한 객체를 사용하도록 변경했습니다. 이를 통해 `uiState.value` 참조 시점 차이로 인한 데이터 불일치 문제를 방지했습니다.
`PlaceMapUiState`의 특정 상태 변화를 기다리는 `await` 확장 함수에 타임아웃 로직을 도입하여, 상태가 변경되지 않을 경우 무한 대기하는 문제를 방지하도록 개선했습니다.
- **`StateExt.kt` 수정:**
- `StateFlow<PlaceMapUiState>.await` 함수에 `timeout` 파라미터(기본값 3초)를 추가했습니다.
- 내부 로직을 `withTimeout` 블록으로 감싸, 지정된 시간 내에 조건에 맞는 상태가 수집되지 않으면 타임아웃이 발생하도록 변경했습니다.
`PlaceMapViewModel`의 이벤트 채널 생성 로직을 수정하고, 단위 테스트에서 코루틴 디스패처가 올바르게 해제되도록 개선했습니다. 또한 테스트 픽스처의 파일명을 적절하게 변경했습니다.
- **`PlaceMapViewModel.kt` 수정:**
- `_placeMapUiEvent`와 `_mapControlUiEvent` 채널 생성 시 설정되어 있던 `onBufferOverflow = BufferOverflow.DROP_OLDEST` 옵션을 제거하여 기본 채널 동작을 따르도록 변경했습니다.
- **테스트 코드 개선:**
- `SelectActionHandlerTest.kt` 및 `MapEventActionHandlerTest.kt`에 `@After` 어노테이션이 달린 `tearDown` 함수를 추가했습니다.
- 각 테스트 종료 시 `Dispatchers.resetMain()`을 호출하여 메인 디스패처 설정을 초기화하도록 수정했습니다.
- `PlaceLIstTestFixture.kt`의 파일명을 `PlaceMapTestFixture.kt`로 변경하여 오타를 수정하고 맥락에 맞게 네이밍을 개선했습니다.
테스트 유틸리티인 `observeEvent` 함수에서 Flow의 데이터를 수집할 때 사용하던 타임아웃 처리 방식을 변경했습니다.
- **`FlowExtensions.kt` 수정:**
- `flow.timeout(3.seconds).first()` 연산자 체인 대신, `withTimeout(3000)` 블록 내부에서 `flow.first()`를 호출하도록 로직을 수정했습니다.
#️⃣ 이슈 번호
🛠️ 작업 내용
🙇🏻 중점 리뷰 요청
전체 아키텍쳐
MVI 특징
Composable의 파라미터는 크게 3가지로 나눌 수 있습니다.
주로 on~으로 시작하는 사용자 행동 (Action)
컴포저블의 상태 (UiState)
사이드 이펙트 (Event)
기존 MVVM에서는 컴포저블의 상태를 추상화 할 수 있었습니다. (산발된 LiveData를 하나의 UiState로 관리)
하지만 사이드 사용자 행동이 많아지면 그만큼 ViewModel의 메서드가 늘어나는 문제가 있었고,
사이드 이펙트 또한 추상화가 어려운 문제가 있었습니다.
(정확히는 UiState처럼 하나의 상태로 통합 관리하게 된다면
Composable이 ViewModel의 메서드를 직접 호출하기 때문에
Composable - ViewModel method의 결합이 높아지는 문제가 있습니다)
하지만 MVI의 경우 Composable은 어떤 액션 이 일어났는지 보고하면
ViewModel 내부에서 상태를 변경, 혹은 사이드 이펙트를 발생시키는 방식으로 동작합니다.
이를 통해 더욱 안정적으로 UiState 상태 관리가 가능합니다.
하나의 창구 (UiEvent, UiState)에서 상태 관리 및, 사이드 이벤트 발생 -> 단일진실 공급 원칙 준수 -> 더욱 안정적으로 UiState 상태 관리 가능
현재 아키텍쳐 특징
MVI의 예제를 보시면 ViewModel에서 Action을 받아 상태를 변경하고 있는 경우가 많습니다.
하지만 만약 Action이 20개, 30개가 된다면 어떻게 될까요?
ViewModel은 걷잡을 수 없이 뚱뚱해집니다.
보통 ViewModel이 뚱뚱해지는 경우 UseCase로 추상화를 하지만 한계가 있습니다.
UseCase는 복잡한 메서드를 추상화하는데 특화되어 있으나, 간단하지만 수많은 로직을 추상화하기는 어렵습니다.
이를 통해 저는 Handler 라는, 사용자 액션 처리를 담당하는 별도의 Delegate 객체를 두어, 분산시키는 방식을 적용하였습니다.
sideEffect를 발송, uiState를 업데이트 하기 때문에 Handler는 UseCase와 다르게 Presentation Layer입니다.
ViewModel은 초기 UiState 업데이트,
모든 Handler에서 반복되는 사이드 이펙트 발송 (observeErrorEvent)
의 역할만 남기도록 추상화했습니다.
당부
MVI로 리팩토링하면서, 이보다 더 정확한 Ui 상태를 관리법은 없는 것 같다는 느낌을 받았습니다.
기존 MVVM에는 수많은 라이브데이터를 별도의 ViewModel 메서드로 관리하고,
Android API (ViewHolder, OnBackPressDispatcher 등)에서 상태 업데이트 로직이 산발되어 있어
요구사항이 복잡해질수록 점점 스파게티 코드가 되어간다는 느낌을 받았습니다.
MVI는 이런 문제를 타파할 수 있는 확실한 해결책이라고 생각합니다.
MVI는 러닝 커브가 높습니다. 현재 코드가 이해가 굉장히 어려울 것으로 예상됩니다.
리뷰하실 때 MVI 관련하여 모르는 지식, 충돌하는 지식이 있으면 언제든지 코멘트 달아주세요.
서로 함께 성장합시다 :)
📸 이미지 첨부 (Optional)
Summary by CodeRabbit
New Features
Bug Fixes
Refactor
✏️ Tip: You can customize this high-level summary in your review settings.