Skip to content

[AN][feat]: 제스처 네비게이션 구현#1179

Open
junseo511 wants to merge 3 commits intodevelop-anfrom
feat/1177
Open

[AN][feat]: 제스처 네비게이션 구현#1179
junseo511 wants to merge 3 commits intodevelop-anfrom
feat/1177

Conversation

@junseo511
Copy link
Contributor

@junseo511 junseo511 commented Mar 4, 2026

🔗 관련 이슈

📝 작업 내용

사용자가 앱 내에서 물리적 제스처를 통해 직관적으로 이전 화면으로 돌아갈 수 있도록 하기 위해 플랫폼별 제스처 네비게이션을 구현했습니다. iOS의 네이티브 스와이프-백 동작을 지원하면서, Android에서는 기존 네비게이션 구조를 유지하는 멀티플랫폼 솔루션을 제공합니다.

주요 변경사항

  1. 플랫폼별 제스처 핸들러 추상화 및 구현

    • Kotlin Multiplatform의 expect/actual 패턴을 이용해 PlatformBackGestureContainer 컴포저블을 정의
    • iOS: 화면 좌측 28dp 영역에서 32dp 이상 우향 드래그 감지 시 뒤로가기 제스처 인식. 수평 드래그가 수직 드래그를 우월하게 한다는 조건을 통해 오인식 방지
    • Android: no-op 래퍼로 동작하며, 향후 플랫폼별 기능 추가 시 확장 가능한 구조 제공
  2. 조건부 제스처 활성화 로직

    • MulKkamApp.kt에서 enableBackGesture 파생 상태를 도입
    • 제스처는 로그인 화면(AuthRoute.Login)을 제외한 모든 화면과 탭 선택 전 상태에서만 활성화
    • 이를 통해 로그인 과정이나 탭 네비게이션 중 의도하지 않은 뒤로가기 동작 방지
  3. 내비게이션 애니메이션 변경

    • NavDisplay.kt의 화면 전환 애니메이션을 슬라이드(수평)에서 페이드로 변경
    • 지속시간을 180ms로 통일하여 제스처 기반 네비게이션과의 UX 일관성 개선
  4. 구조적 계층 재설정

    • PlatformBackGestureContainerScaffold 상위에 배치하여 전역 제스처 인식을 가능하게 함
    • 기존 네비게이션 구조와 SnackbarHost, BottomNavigationBar의 동작은 그대로 유지

📊 변경 효과 요약

  • 플랫폼 특화 로직 분리: iOS에서만 제스처 인식 로직 실행(86줄), Android는 비용 최소화(12줄)
  • 상태 관리 추가: 네비게이션 컨텍스트(현재 경로, 탭 상태)를 고려한 조건부 활성화로 사이드 이펙트 예방
  • 정량 지표는 이번 PR에서 측정하지 않음

📸 스크린샷 (Optional)

ScreenRecording_03-04-2026.11-20-35_1.mov

- iOS 플랫폼을 위한 `PlatformBackGestureContainer` 및 스와이프 제스처 로직 추가
- Android는 별도 처리 없이 콘텐츠만 노출하도록 구현
- `NavDisplay` 애니메이션을 슬라이드에서 페이드 효과로 변경 및 지속 시간 조정
- `MulKkamApp`에 뒤로가기 제스처 컨테이너 적용 및 활성화 조건 설정 (로그인 화면 제외)
@junseo511 junseo511 added the AN 안드로이드 관련 이슈 label Mar 4, 2026
@junseo511 junseo511 added the feat 새로운 기능을 구현합니다. label Mar 4, 2026
@github-actions github-actions bot requested review from hwannow March 4, 2026 02:25
@github-actions github-actions bot changed the title feat: iOS 스와이프 뒤로가기 제스처 구현 및 네비게이션 애니메이션 수정 [AN][feat]: 제스처 네비게이션 구현 Mar 4, 2026
@github-actions github-actions bot changed the title feat: iOS 스와이프 뒤로가기 제스처 구현 및 네비게이션 애니메이션 수정 [AN][feat]: 제스처 네비게이션 구현 Mar 4, 2026
@github-actions github-actions bot requested a review from devfeijoa March 4, 2026 02:25
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 4, 2026

Note

.coderabbit.yaml has unrecognized properties

CodeRabbit is using all valid settings from your configuration. Unrecognized properties (listed below) have been ignored and may indicate typos or deprecated fields that can be removed.

⚠️ Parsing warnings (1)
Validation error: Unrecognized key(s) in object: 'tools'
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Walkthrough

이 변경은 Kotlin Multiplatform 모바일 앱에 플랫폼별 뒤로가기 제스처 훅을 추가합니다. 공통(expect)으로 PlatformBackGestureContainer(enabled, onBack, content)를 선언하고, Android에서는 no-op 래퍼(actual)로 구현했으며 iOS에서는 화면 왼쪽 모서리 스와이프를 감지해 onBack()을 호출하는 실제 제스처 처리 로직을 추가했습니다. 메인 앱(MulKkamApp)은 현재 라우트와 탭 선택 상태로 제스처 활성화를 결정하도록 enableBackGesture 파생 상태를 도입해 Scaffold를 해당 컨테이너로 감쌌습니다. 또한 NavDisplay의 화면 전환은 방향(팝/푸시)에 따라 슬라이드/페이드 조합을 선택하도록 전환 스펙이 변경되고, 두 개의 애니메이션 지속시간 상수가 도입되었습니다.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

상세 검토 의견

1. iOS 제스처 로직 정확성 및 안전성

문제점

  • 현재는 시작 지점이 왼쪽 엣지 내인지, 수평/수직 누적 거리 비교로 제스처 판정, 제스처 유효 시점의 포인터 소비 처리가 이뤄집니다. 수직 흔들림이나 미세한 흔들림에 의해 오탐 가능성이 있고, 포인터 소비 시점이 늦어 경쟁 제스처(스크롤 등)와 충돌할 위험이 있습니다.

왜 문제인지

  • 단순 거리 비교는 드래그 각도가 완만할 때(예: 25°) 수직 요소가 섞여 있어도 수평 우위로 판단될 수 있어 UX 불안정으로 이어집니다.
  • 포인터를 제스처 유효 시점에 소비하지 않으면 하위 컴포넌트가 먼저 이벤트를 처리해 앱 반응이 엉킬 수 있습니다.

개선안 (선택지)

  1. 각도 기반 필터링 도입
    • 설명: atan2(verticalDelta, horizontalDelta)를 이용해 각도 임계값(예: |angle| <= 30°)을 적용.
    • 장점: 수평 의도 판별이 더 견고해짐.
    • 단점: 추가 수학 연산, 튜닝 필요.
  2. 조기 소비(consume) 및 슬로틀링
    • 설명: 제스처가 임계값을 넘는 순간 즉시 consume()하고 추가 이동을 추적.
    • 장점: 다른 제스처와 충돌 감소.
    • 단점: 잘못된 조기 소비는 하위 제스처 차단 유발.
  3. GestureDetector/DetectDragGesturesWithObserver 같은 고수준 API 사용
    • 설명: Compose 제공 고수준 제스처 API로 판정과 소비 흐름을 위임.
    • 장점: 유지보수성 향상, 플랫폼 최적화 활용.
    • 단점: 복잡한 커스텀 논리를 그대로 옮기기 어려울 수 있음.

권장 개선 방향

  • 각도 기반 판정 + 임계값(엣지 폭, 최소 수평 거리, 각도)을 조합하고, 임계 도달 시 즉시 consume()으로 전파 차단. 테스트 케이스(미세 스와이프, 대각선 스와이프, 스크롤과 동시 발생)를 작성해 튜닝하세요.

2. Android 구현의 불완전성

문제점

  • Android actual 구현은 단순히 content()만 호출해 enabledonBack을 전혀 사용하지 않습니다. 결과적으로 iOS와 행동 일관성이 없고, 앱레벨에서 onBack 콜백이 호출되지 않습니다.

왜 문제인지

  • 플랫폼 간 기대 동작(엣지 스와이프 → 네비게이트 백)이 다르면 UX 불일치가 발생합니다. 또한 MulKkamApp에서 onBack을 전달했으나 Android에서 무시되면 논리적 연결이 깨집니다.

대안 (선택지)

  1. 시스템 뒤로가기(BackDispatcher)에 의존
    • 설명: Android는 시스템 네비게이션으로 대부분 처리되므로 onBack 필요 시 onBackPressedDispatcher 사용.
    • 장점: 플랫폼 표준 활용, 중복 코드 없음.
    • 단점: 엣지 스와이프와 정확히 동일한 타이밍/감각을 보장하지 못할 수 있음.
  2. iOS와 유사한 커스텀 제스처 구현
    • 설명: pointerInput으로 엣지 스와이프 감지 후 onBack 호출.
    • 장점: 플랫폼 간 일관성 유지.
    • 단점: 중복 로직, Android 터치/제스처 관점에서 추가 테스팅 필요.
  3. 현재 방식 유지하되 주석·문서화 및 enable/사용 흐름 정리
    • 설명: 의도적으로 시스템에 위임하고 onBack이 사용되지 않음을 명시.
    • 장점: 간단함 유지.
    • 단점: 호출자 코드의 혼란 여지 남음.

권장 개선 방향

  • 우선 의도를 명확히 주석으로 남기고(왜 no-op인지), 가능하면 onBack이 전달되는 경우를 처리하거나 시스템 back dispatcher를 통해 연결해 주세요. 장기적으로는 iOS와의 UX 통일을 검토하십시오.

3. MulKkamApp.kt의 enableBackGesture 조건 및 구조

문제점

  • enableBackGesturecurrentRoute != AuthRoute.Login && currentTab == null로 직접 표현되어 있습니다. 이 비즈니스 규칙이 분산되면 유지보수성이 떨어집니다.

왜 문제인지

  • 의미가 암묵적이며 조건이 복잡해질 때 수정이 어려워집니다.

개선안

  • 헬퍼 함수 또는 정책 객체로 추출:
    • 예: private fun shouldEnableBackGesture(route: Route?, tab: Tab?): Boolean { ... }
  • 테스트 가능한 단위로 분리해 유닛 테스트 추가.

장점

  • 의도 명확화, 확장/테스트 용이.

4. NavDisplay 전환(transition) 변경 검토

문제점

  • 전환 스펙이 팝/푸시 방향에 따라 다른 슬라이드 오프셋과 페이드/슬라이드 조합을 사용하도록 변경되었습니다. 원래 상수 ANIMATION_DURATION_MILLIS를 두 상수로 분리한 것은 명확하지만, 시각적 의도(슬라이드 vs 페이드)가 바뀌었는지 확인 필요합니다.

왜 문제인지

  • UX 측면에서 슬라이드는 네비게이션의 방향성을 전달하고, 페이드는 덜 명확합니다. 변경 의도가 성능(짧은 페이드)인지, 디자인 변경인지 명시되어야 합니다.

권장 조치

  • 변경 의도(디자인/성능)를 커밋 메시지/PR 본문에 명시.
  • 제품팀/디자이너와 확인하고, A/B 또는 사용자 피드백 수집을 권장.
  • 애니메이션 상수는 주석으로 용도(슬라이드/페이드 분리 이유)를 덧붙이세요.

5. 테스트 및 문서

권장

  • iOS의 다양한 제스처 케이스(빠른 스와이프, 느린 드래그, 대각선)에 대한 UI 테스트/수동 체크리스트를 작성하세요.
  • Android no-op 의도를 문서화하거나, onBack 경로의 테스트를 추가해 regression을 방지하세요.
  • API(PlatformBackGestureContainer) 사용 예시를 README나 컴포넌트 문서에 추가하면 소비자 혼란을 줄일 수 있습니다.

요약

  • iOS 제스처는 더욱 견고하게(각도 임계·조기 소비) 개선 필요.
  • Android의 no-op는 의도 명시 또는 시스템/커스텀 처리 연결 필요.
  • MulKkamApp의 조건은 헬퍼로 추출해 명확히 하고 테스트 가능하게 만드세요.
  • NavDisplay 애니메이션 변경 의도를 문서화하고 UX 검증을 권장합니다.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 제스처 네비게이션 구현이라는 핵심 변경사항을 명확히 드러내며, 실제 코드 변경(PlatformBackGestureContainer 추가, 제스처 활성화 로직, 애니메이션 개선)과 일치합니다.
Description check ✅ Passed PR 설명은 템플릿의 모든 필수 섹션을 포함하고 있으며, 관련 이슈, 작업 내용, 주요 변경사항을 체계적으로 설명하고 스크린샷도 첨부했습니다.
Linked Issues check ✅ Passed PR은 이슈 #1177의 제스처 네비게이션 구현 목표를 충족하며, iOS 제스처 인식(86줄), Android no-op 구조(12줄), 조건부 활성화 로직, 애니메이션 개선을 모두 포함했습니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 제스처 네비게이션 구현과 관련된 범위 내에 있으며, PlatformBackGestureContainer 추상화, 조건부 활성화, 애니메이션 개선은 모두 목표 달성을 위한 필수 변경입니다.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/1177
📝 Coding Plan
  • Generate coding plan for human review comments

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.

Tip

CodeRabbit can enforce grammar and style rules using `languagetool`.

Configure the reviews.tools.languagetool setting to enable/disable rules and categories. Refer to the LanguageTool Community to learn more.

Xcode 프로젝트 파일 내 `DEVELOPMENT_TEAM`의 하드코딩된 값을 `${TEAM_ID}` 변수를 사용하도록 수정하였습니다.
Copy link
Contributor

@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: 1

🧹 Nitpick comments (3)
mobile/shared/src/commonMain/kotlin/com/mulkkam/ui/navigation/NavDisplay.kt (1)

46-52: 매직 넘버 180을 상수로 추출하는 것을 고려해주세요.

transitionDuration = 180이 함수 내부에 하드코딩되어 있어요. 이 값은 다른 곳에서도 재사용되거나 변경될 가능성이 있으므로, 파일 상단에 private const val로 선언하면 유지보수성이 높아집니다.

제스처 네비게이션과 함께 slide → fade 전환으로 변경한 것은 적절한 선택이에요. 스와이프 제스처 중에 슬라이드 애니메이션이 겹치면 시각적으로 어색할 수 있거든요.

♻️ 상수 추출 제안
+private const val TRANSITION_DURATION_MILLIS = 180
+
 private fun defaultTransitionSpec(): ContentTransform {
-    val transitionDuration = 180
-
-    val enterTransition = fadeIn(animationSpec = tween(durationMillis = transitionDuration))
-    val exitTransition = fadeOut(animationSpec = tween(durationMillis = transitionDuration))
+    val enterTransition = fadeIn(animationSpec = tween(durationMillis = TRANSITION_DURATION_MILLIS))
+    val exitTransition = fadeOut(animationSpec = tween(durationMillis = TRANSITION_DURATION_MILLIS))
 
     return enterTransition togetherWith exitTransition
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/shared/src/commonMain/kotlin/com/mulkkam/ui/navigation/NavDisplay.kt`
around lines 46 - 52, defaultTransitionSpec() currently hardcodes
transitionDuration = 180; extract this magic number into a top-level constant
(e.g. private const val TRANSITION_DURATION_MS = 180) and replace the inline
value in defaultTransitionSpec() so the tween(durationMillis =
TRANSITION_DURATION_MS) calls use the constant; ensure the constant is private
and placed at the file top for reuse and easier maintenance.
mobile/shared/src/androidMain/kotlin/com/mulkkam/ui/main/component/PlatformBackGestureContainer.android.kt (1)

5-12: 의도적인 no-op 구현이라면 설명 주석을 추가해주세요.

Android에서 enabledonBack 파라미터를 사용하지 않는 이유가 있을 거예요 (Android는 시스템 레벨의 뒤로가기 제스처/버튼을 사용하므로). 하지만 코드만 봐서는 이것이 의도된 것인지, 아직 구현되지 않은 것인지 알기 어려워요.

두 가지 개선 방안이 있습니다:

  1. 설계 의도를 주석으로 명시 - 미래의 개발자가 "왜 파라미터를 무시하지?"라는 의문을 품지 않도록
  2. @Suppress("UNUSED_PARAMETER") - Lint 경고를 명시적으로 억제하여 의도적임을 표시
📝 주석 추가 제안
 `@Composable`
 actual fun PlatformBackGestureContainer(
+    // Android에서는 시스템의 예측 뒤로가기(Predictive Back) 제스처를 사용하므로
+    // 별도의 커스텀 제스처 처리가 필요하지 않습니다.
     enabled: Boolean,
     onBack: () -> Unit,
     content: `@Composable` () -> Unit,
 ) {
     content()
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@mobile/shared/src/androidMain/kotlin/com/mulkkam/ui/main/component/PlatformBackGestureContainer.android.kt`
around lines 5 - 12, The PlatformBackGestureContainer actual implementation
currently ignores its parameters (enabled, onBack) and simply invokes content(),
which looks like a deliberate no-op on Android; add a brief explanatory comment
above the PlatformBackGestureContainer function stating that Android uses system
back gestures/buttons so the parameters are intentionally unused, and also add
`@Suppress`("UNUSED_PARAMETER") on the PlatformBackGestureContainer declaration to
silence lint and make the intent explicit (or alternatively remove the suppress
and keep only the explanatory comment if you prefer).
mobile/shared/src/iosMain/kotlin/com/mulkkam/ui/main/component/PlatformBackGestureContainer.ios.kt (1)

47-80: isBackGestureTriggered 변수와 마지막 if 블록이 중복되어 보여요.

현재 로직 흐름을 보면:

  1. 제스처가 트리거되면 (Line 66-71): isBackGestureTriggered = true, shouldFinishGesture = true 설정
  2. shouldFinishGesture = true이므로 while 루프 종료
  3. Lines 78-80에서 isBackGestureTriggered를 체크하고 return@awaitEachGesture

하지만 awaitEachGesture 블록은 어차피 끝나면 다음 제스처를 기다리므로, 마지막 return 문이 동작에 영향을 주지 않아요. 이 패턴이 의도적인 것(가독성 향상이나 미래 확장성)이라면 괜찮지만, 그렇지 않다면 제거하여 코드를 간결하게 만들 수 있습니다.

♻️ 불필요한 변수 및 조건문 제거 제안
                         var accumulatedHorizontalDragDistance = 0f
                         var accumulatedVerticalDragDistance = 0f
                         var shouldFinishGesture = false
-                        var isBackGestureTriggered = false

                         while (!shouldFinishGesture) {
                             // ... 중간 로직 동일 ...

                             if (isDraggingToRight && isHorizontalDominant && isDistanceEnough) {
                                 pointerEvent.changes.forEach { it.consume() }
                                 onBack()
-                                isBackGestureTriggered = true
                                 shouldFinishGesture = true
                             }

                             if (!pointerChange.pressed) {
                                 shouldFinishGesture = true
                             }
                         }
-
-                        if (isBackGestureTriggered) {
-                            return@awaitEachGesture
-                        }
                     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@mobile/shared/src/iosMain/kotlin/com/mulkkam/ui/main/component/PlatformBackGestureContainer.ios.kt`
around lines 47 - 80, The isBackGestureTriggered flag and the final "if
(isBackGestureTriggered) return@awaitEachGesture" are redundant; remove the
isBackGestureTriggered declaration and all assignments, delete the final if
block, and instead exit the awaitEachGesture lambda immediately when the back
gesture is detected by invoking return@awaitEachGesture right after calling
onBack() inside the branch that currently sets isBackGestureTriggered (use the
existing onBack()/shouldFinishGesture logic location to add the early return).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mobile/shared/src/commonMain/kotlin/com/mulkkam/MulKkamApp.kt`:
- Around line 48-53: The enableBackGesture derived state currently enables
gestures on Splash because it only excludes AuthRoute.Login; update the
condition in the derivedStateOf (where navigator.currentRoute, currentTab and
enableBackGesture are used) to exclude the whole AuthRoute hierarchy — e.g.,
change the check from `currentRoute !is AuthRoute.Login && currentTab == null`
to `currentRoute !is AuthRoute && currentTab == null` (or alternatively also
require `navigator.backStack.size > 1` if you prefer guarding against initial
stack); if the original intent was to only exclude Login, add an inline comment
explaining that exception.

---

Nitpick comments:
In
`@mobile/shared/src/androidMain/kotlin/com/mulkkam/ui/main/component/PlatformBackGestureContainer.android.kt`:
- Around line 5-12: The PlatformBackGestureContainer actual implementation
currently ignores its parameters (enabled, onBack) and simply invokes content(),
which looks like a deliberate no-op on Android; add a brief explanatory comment
above the PlatformBackGestureContainer function stating that Android uses system
back gestures/buttons so the parameters are intentionally unused, and also add
`@Suppress`("UNUSED_PARAMETER") on the PlatformBackGestureContainer declaration to
silence lint and make the intent explicit (or alternatively remove the suppress
and keep only the explanatory comment if you prefer).

In `@mobile/shared/src/commonMain/kotlin/com/mulkkam/ui/navigation/NavDisplay.kt`:
- Around line 46-52: defaultTransitionSpec() currently hardcodes
transitionDuration = 180; extract this magic number into a top-level constant
(e.g. private const val TRANSITION_DURATION_MS = 180) and replace the inline
value in defaultTransitionSpec() so the tween(durationMillis =
TRANSITION_DURATION_MS) calls use the constant; ensure the constant is private
and placed at the file top for reuse and easier maintenance.

In
`@mobile/shared/src/iosMain/kotlin/com/mulkkam/ui/main/component/PlatformBackGestureContainer.ios.kt`:
- Around line 47-80: The isBackGestureTriggered flag and the final "if
(isBackGestureTriggered) return@awaitEachGesture" are redundant; remove the
isBackGestureTriggered declaration and all assignments, delete the final if
block, and instead exit the awaitEachGesture lambda immediately when the back
gesture is detected by invoking return@awaitEachGesture right after calling
onBack() inside the branch that currently sets isBackGestureTriggered (use the
existing onBack()/shouldFinishGesture logic location to add the early return).

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d269d992-35ec-4c1e-8b02-9410ebb17668

📥 Commits

Reviewing files that changed from the base of the PR and between fe9b14b and b3428a7.

📒 Files selected for processing (5)
  • mobile/shared/src/androidMain/kotlin/com/mulkkam/ui/main/component/PlatformBackGestureContainer.android.kt
  • mobile/shared/src/commonMain/kotlin/com/mulkkam/MulKkamApp.kt
  • mobile/shared/src/commonMain/kotlin/com/mulkkam/ui/main/component/PlatformBackGestureContainer.kt
  • mobile/shared/src/commonMain/kotlin/com/mulkkam/ui/navigation/NavDisplay.kt
  • mobile/shared/src/iosMain/kotlin/com/mulkkam/ui/main/component/PlatformBackGestureContainer.ios.kt

Copy link
Contributor

@hwannow hwannow left a comment

Choose a reason for hiding this comment

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

수고하셨습니다 공백 🥔

어푸어푸 🌊🏊‍♀️🌊

Comment on lines +20 to +24
actual fun PlatformBackGestureContainer(
enabled: Boolean,
onBack: () -> Unit,
content: @Composable () -> Unit,
) {
Copy link
Contributor

Choose a reason for hiding this comment

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

p3) 혹시 여기에서 previousContent 같이 이전 화면을 받아서...
Swipe 애니메이션을 보여 주는 건 어려울까요?
( 옆으로 화면이 밀려나면서, 이전 화면이 드러나게 )

구현 난이도가 높다면 넘어가도 좋습니당

Copy link
Contributor Author

Choose a reason for hiding this comment

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

환노도 저와 같은 말씀을 하셨었네요?! 예리하시군요 👀👀
추가 완료했습니다 :) 기회가 되면 영상도 첨부하도록 하겠습니다!

feat: NavDisplay 내비게이션 전환 시 슬라이드 애니메이션 추가

Copy link
Contributor

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mobile/shared/src/commonMain/kotlin/com/mulkkam/ui/navigation/NavDisplay.kt`:
- Around line 29-34: The current isPopTransition in NavDisplay incorrectly
infers "back" vs "reset" by comparing backStack.size; instead, add an explicit
NavigationType enum and a lastNavigationType field to MainNavigator and set it
inside MainNavigator.navigate, MainNavigator.popBackStack, and
MainNavigator.navigateAndClearBackStack (or whatever methods implement
clear/reset) to PUSH, POP, REPLACE respectively; then change NavDisplay to
derive isPopTransition from navigator.lastNavigationType == NavigationType.POP
instead of comparing sizes so tab switches and clears use the correct animation
direction.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e8e7a4fa-c7f7-4e71-bc7d-1632990398a2

📥 Commits

Reviewing files that changed from the base of the PR and between b3428a7 and 220b3c1.

📒 Files selected for processing (1)
  • mobile/shared/src/commonMain/kotlin/com/mulkkam/ui/navigation/NavDisplay.kt

Comment on lines +29 to +34
val previousBackStackSizeState = remember { mutableIntStateOf(backStack.size) }
val isPopTransition = backStack.size < previousBackStackSizeState.intValue

LaunchedEffect(backStack.size) {
previousBackStackSizeState.intValue = backStack.size
}
Copy link
Contributor

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:

#!/bin/bash
# Description: Check where navigateAndClearBackStack is called to understand impact

echo "=== navigateAndClearBackStack 사용처 확인 ==="
rg -n "navigateAndClearBackStack" --type=kotlin -C 3

echo ""
echo "=== NavDisplay 호출부 확인 ==="
rg -n "NavDisplay\(" --type=kotlin -C 3

Repository: woowacourse-teams/2025-mul-kkam

Length of output: 6746


🏁 Script executed:

cat -n mobile/shared/src/commonMain/kotlin/com/mulkkam/ui/navigation/MainNavigator.kt

Repository: woowacourse-teams/2025-mul-kkam

Length of output: 3995


🏁 Script executed:

cat -n mobile/shared/src/commonMain/kotlin/com/mulkkam/ui/navigation/NavDisplay.kt

Repository: woowacourse-teams/2025-mul-kkam

Length of output: 4187


백스택 크기 비교만으로는 "뒤로가기"와 "화면 초기화"를 구분할 수 없습니다

현재 isPopTransition 로직이 크기 감소에만 의존하면서, 의미 있는 네비게이션 구분이 불가능합니다. 실제로 코드를 보면 이 문제가 매우 구체적으로 나타나는데요:

문제의 구체적인 영향:

  1. 하단 탭 네비게이션 (navigateToHome(), navigateToHistory(), navigateToFriends(), navigateToSetting())

    • 예: 깊은 중첩 화면(예: Home → Encyclopedia → Detail, 스택 깊이 4)에서 다른 탭(History)으로 이동
    • 백스택이 4 → 1로 줄어들어 isPopTransition = true 판정
    • 하지만 이는 탭 전환이므로 앞으로 가는 애니메이션이 적절함
    • 현재는 뒤로 가는 애니메이션(왼쪽에서 슬라이드인)이 재생되어 사용자 기대와 맞지 않음
  2. 인증 상태 전환 (navigateToLoginAndClearBackStack(), navigateToSplash())

    • 로그아웃 후 로그인 화면으로: 백스택 초기화 → 틀린 애니메이션 방향
  3. 온보딩 플로우 (navigateToOnboardingTerms())

    • 동의 화면으로 리셋할 때도 동일한 문제

근본 원인: navigateAndClearBackStack()popBackStack()이 모두 백스택 크기를 감소시키므로, 크기만으로는 두 경우를 구분할 수 없습니다.

추천되는 해결 방식:

Option A (권장): MainNavigator에서 네비게이션 의도를 명시적으로 추적

  • 장점: 의도가 명확하고 향후 다른 네비게이션 패턴(예: 모달 표시) 추가가 용이
  • 단점: MainNavigator 인터페이스 변경 필요
enum class NavigationType { PUSH, POP, REPLACE }

`@Stable`
class MainNavigator internal constructor(
    val backStack: SnapshotStateList<Any>,
) {
    var lastNavigationType: NavigationType = NavigationType.PUSH
        private set
    
    fun popBackStack() {
        if (backStack.size > 1) {
            lastNavigationType = NavigationType.POP
            backStack.removeAt(backStack.lastIndex)
        }
    }

    fun navigate(route: Any) {
        lastNavigationType = NavigationType.PUSH
        backStack.add(route)
    }

    fun navigateAndClearBackStack(route: Any) {
        lastNavigationType = NavigationType.REPLACE
        backStack.clear()
        backStack.add(route)
    }
}

그 후 NavDisplay에서:

val isPopTransition = navigationType == NavigationType.POP

Option B: 라우트 정체성으로 판단

  • 장점: 구현이 간단함
  • 단점: 같은 라우트 타입으로의 전환이나 엣지 케이스를 완벽히 커버하기 어려움

의도를 명시적으로 드러내는 방식이 디버깅과 유지보수 관점에서 더 건강한 패턴입니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/shared/src/commonMain/kotlin/com/mulkkam/ui/navigation/NavDisplay.kt`
around lines 29 - 34, The current isPopTransition in NavDisplay incorrectly
infers "back" vs "reset" by comparing backStack.size; instead, add an explicit
NavigationType enum and a lastNavigationType field to MainNavigator and set it
inside MainNavigator.navigate, MainNavigator.popBackStack, and
MainNavigator.navigateAndClearBackStack (or whatever methods implement
clear/reset) to PUSH, POP, REPLACE respectively; then change NavDisplay to
derive isPopTransition from navigator.lastNavigationType == NavigationType.POP
instead of comparing sizes so tab switches and clears use the correct animation
direction.

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

Labels

AN 안드로이드 관련 이슈 feat 새로운 기능을 구현합니다.

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants