Skip to content

[Refactor] Callvan 필터 BottomSheet 리팩토링#1404

Open
KYM-P wants to merge 4 commits intodevelopfrom
refactor/#1403-callvan-filter
Open

[Refactor] Callvan 필터 BottomSheet 리팩토링#1404
KYM-P wants to merge 4 commits intodevelopfrom
refactor/#1403-callvan-filter

Conversation

@KYM-P
Copy link
Copy Markdown
Collaborator

@KYM-P KYM-P commented Apr 5, 2026

PR 개요

이슈 번호: #1403

PR 체크리스트

  • Code convention을 잘 지켰나요?
  • Lint check를 수행하였나요?
  • Assignees를 추가했나요?

작업사항

  • 버그 수정
  • 신규 기능
  • 코드 스타일 수정 (포맷팅 등)
  • 리팩토링 (기능 수정 X, API 수정 X)
  • 기타

작업사항의 상세한 설명

  • FilterSectionFilterDuplicateSectionisDuplicateSelectable 파라미터로 통합
  • FilterSectionItem 데이터 클래스로 섹션 정의를 반복문으로 축약
  • 필터 선택 리스트 연산(단일/복수 선택, All 토글)을 UI에서 ViewModel로 이동
  • FilterBottomSheet를 stateless로 변경 (pendingFilterState를 ViewModel에서 관리)
  • FilterBottomSheetActionsonItemClicked 단일 콜백으로 간소화
  • BottomSheet 높이를 화면의 70%로 제한
  • 내부 스크롤과 BottomSheet 드래그 제스처 충돌 해결 (nestedScroll)

논의 사항

스크린샷

추가내용

  • develop, sprint 브랜치를 향하고 있습니다
  • production 브랜치를 향하고 있습니다

🤖 Generated with Claude Code

KYM-P and others added 4 commits April 5, 2026 23:17
…composable

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@KYM-P KYM-P requested a review from a team as a code owner April 5, 2026 15:50
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 5, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9ef0b8d5-9391-4f56-a077-bc9054b29fa4

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/#1403-callvan-filter

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.

@github-actions github-actions bot added the refactor Code refactoring label Apr 5, 2026
@KYM-P KYM-P self-assigned this Apr 5, 2026
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Apr 5, 2026

@KYM-P
Copy link
Copy Markdown
Collaborator Author

KYM-P commented Apr 7, 2026

코드 리뷰 (by Claude Opus 4.6)

전반적으로 상태를 ViewModel로 끌어올리고 BottomSheet를 stateless로 전환한 방향성은 매우 좋습니다. FilterSection / FilterDuplicateSection 통합과 toggleDuplicateSelection 헬퍼 추출도 깔끔합니다. 다만 Compose 사용 패턴에서 몇 가지 짚을 점이 있습니다.


🔴 Major

1. FilterBottomSheet.ktremember(state) { ... } 가 사실상 매 recomposition마다 무효화됨

feature/callvan/.../component/FilterBottomSheet.kt L113~159 (val sections = remember(state) { listOf(...) })

state: FilterBottomSheetState 는 사용자가 필터 항목을 누를 때마다 새 인스턴스로 교체됩니다. 따라서 remember(state) 의 key 가 매번 바뀌어 sections 리스트가 매 클릭마다 새로 할당됩니다. remember 가 메모이제이션 효과를 전혀 못 내고 있으니, 다음 중 하나로 정리하는 게 좋습니다.

  • 권장: sections 정의에서 selectedItems 필드를 제거하고, state 만으로 외부에서 선택값을 매핑. 정적 메타데이터(title/items/hint)는 remember { ... } (key 없이) 또는 top-level val 로 한 번만 만들고, 선택 상태는 FilterSection 호출부에서 분기.
  • 또는 아예 remember 를 떼어내서 의도를 명확히 하기.
// top-level or remember {} (key 없이)
private val filterSectionsMeta = listOf(
    FilterSectionMeta(R.string.filter_list_list_type, persistentListOf(ListType.All, ListType.My, ListType.Joined)),
    // ...
)

// 호출부
filterSectionsMeta.forEachIndexed { index, meta ->
    FilterSection(
        title = stringResource(meta.titleRes),
        items = meta.items,
        selectedItems = state.selectedItemsFor(meta), // 또는 when 분기
        onItemClicked = actions.onItemClicked,
        hint = meta.hintRes?.let { stringResource(it) }
    )
    if (index < filterSectionsMeta.lastIndex) HorizontalDivider(...)
}

2. LocalContext.current + context.getString(...) 안티패턴

FilterBottomSheet.kt L112, L116~157

Compose에서는 stringResource(R.string.xxx) 를 사용해야 합니다. context.getString 은 다음 문제가 있습니다.

  • Configuration 변경(언어 변경 등) 시 자동 recomposition이 트리거되지 않을 수 있음
  • Preview / Compose 테스트에서 동작 보장이 약함
  • LocalContext 자체가 불필요하게 의존성 추가

FilterSectionItem.title: String@StringRes titleRes: Int 로 들고 다니다가 호출 시점에 stringResource() 로 변환하는 게 정석입니다.

3. FilterSection 의 제네릭 제거로 타입 안정성 손실

FilterBottomSheet.kt L194~ (FilterSection(... items: ImmutableList<CallvanFilterType>, selectedItems: ImmutableList<CallvanFilterType>, onItemClicked: (CallvanFilterType) -> Unit ...))

기존 <T : CallvanFilterType> 제네릭을 풀면서 FilterSectionItemselectedItems = persistentListOf(state.selectedListType) 처럼 단일 선택을 1-요소 리스트로 박싱 하게 되었습니다. 결과적으로:

  • 단일/복수 선택의 의미 차이가 타입에서 사라짐
  • ListType 칸에 SortType 을 잘못 매핑해도 컴파일러가 못 잡음
  • 매번 persistentListOf(...) 박싱 비용

FilterSectionItem 을 sealed로 (SingleSelect<T> / MultiSelect<T>) 분리하거나, 적어도 FilterSection 의 제네릭을 유지하는 편이 안전합니다.


🟡 Minor

4. HorizontalDivider 가 마지막 섹션 뒤에도 그려짐

FilterBottomSheet.kt L165 부근

if (index <= sections.lastIndex) {
    HorizontalDivider(...)
}

forEachIndexedindex 는 항상 <= lastIndex 이므로 이 조건은 항상 참입니다. 마지막 섹션 아래에도 divider 가 그려지는데, 의도라면 명시적 주석을, 아니라면 index < sections.lastIndex 로 수정해야 합니다.

5. NestedScroll: onPreScroll 미처리로 swipe-down dismiss 우선순위 문제 가능

FilterBottomSheet.kt L96~104

현재는 onPostScroll 만 정의해 "스크롤 후 남은 위쪽 잔량을 소비" 합니다. 그러나 사용자가 스크롤 가능한 영역의 중간에서 아래로 드래그하기 시작하면 onPreScroll 단계에서 BottomSheet 가 먼저 가로채 dismiss 가 트리거될 수 있습니다. 의도가 "스크롤 콘텐츠가 끝까지 올라간 뒤에만 dismiss 허용" 이라면 onPreScroll 도 함께 처리해야 일관됩니다. 실제 기기에서 빠르게 위아래로 드래그해 보면서 검증을 권장합니다.

6. onFilterItemClicked 에서 비로그인 사용자가 ListType.My/Joined 를 선택해도 즉시 피드백 없음

CallvanListViewModel.kt onFilterItemClicked / applyPendingFilter

이전에는 applyFilter 진입점에서 한 번만 검사했고, 새 구조에서도 applyPendingFilter 시점에만 로그인 다이얼로그가 뜹니다. 사용자가 My 를 누르고 다른 항목들을 만지작거린 뒤 "적용" 에서야 막히면 UX가 어색할 수 있습니다. 클릭 시점에 바로 막거나, 비로그인 상태에서 My/Joined 를 disabled 로 표시하는 방법을 고려해 보세요.


🟢 Trivial / Info

7. pendingFilterState 기본값

CallvanListScreenImpl(... pendingFilterState: FilterBottomSheetState = FilterBottomSheetState() ...) — 호출부가 항상 명시 전달하므로 default 는 Preview 외에 의미가 없습니다. 일관성을 위해 filterState 와 같이 default 를 두는 건 OK 지만, Preview 가 아닌 default 값 사용은 사실상 죽은 코드라는 점 인지하시면 좋겠습니다.

8. 잘 된 점 (칭찬)

  • toggleDuplicateSelection 로 단일/복수 토글 로직을 ViewModel 에 응집시킨 것 👍
  • MINIMUM_SELECTION_COUNT 를 companion 으로 끌어올린 것 깔끔
  • BottomSheet 를 stateless 로 만들어 ViewModel이 단일 진실 공급원이 된 것은 테스트 가능성을 크게 높입니다
  • applyPendingFilter 에서 isFilterVisible = false 를 함께 reduce 해 닫힘/적용을 원자적으로 처리한 것도 좋습니다

요약

방향은 좋지만 #1 (remember(state)) 와 #2 (stringResource 미사용) 은 Compose 모범사례 측면에서 꼭 정리하면 좋겠고, #3 (제네릭 손실) 은 장기 유지보수 측면에서 다시 고민해볼 만합니다. #4 는 단순 버그 가능성이라 빠르게 확인 부탁드립니다.


🤖 Reviewed by Claude Opus 4.6 (local session, not the Actions workflow)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant