Skip to content

Phase 2: Single Activity로 전환#714

Open
l2hyunwoo wants to merge 16 commits into
feature/nav-3-basefrom
feature/nav-3-single-activity
Open

Phase 2: Single Activity로 전환#714
l2hyunwoo wants to merge 16 commits into
feature/nav-3-basefrom
feature/nav-3-single-activity

Conversation

@l2hyunwoo
Copy link
Copy Markdown
Collaborator

@l2hyunwoo l2hyunwoo commented Mar 29, 2026

Summary by CodeRabbit

  • 개선 사항

    • 네비게이션이 통합되어 화면 전환이 더 일관되고 예측 가능해졌습니다.
    • 딥링크 및 알림 진입 처리가 보다 안정적으로 동작합니다.
    • 뒤로가기 동작이 중앙화되어 복귀 흐름이 향상되었습니다.
    • 스플래시·온보딩·메인 흐름이 재구성되어 초기 로딩과 재진입 경험이 최적화되었습니다.
  • 새로운 기능

    • 주요 화면들이 키 기반 내비게이션 엔트리로 등록되어 모듈화된 경로 탐색을 지원합니다.
  • 버그 수정

    • 알림·딥링크 인텐트 처리와 관련된 예외/잘못된 경로 처리를 개선했습니다.

@l2hyunwoo l2hyunwoo self-assigned this Mar 29, 2026
@auto-assign auto-assign Bot requested review from boiledeggg and mwy3055 March 29, 2026 12:12
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 29, 2026

Walkthrough

여러 Activity를 제거하고 HostActivity를 런처로 통합하며, 상태형 중앙 네비게이터(Navigator)와 딥링크 인텐트 팩토리(DeepLinkIntentFactory)를 도입해 키(key)-기반 Compose EntryBuilder 네비게이션으로 전환했습니다. AndroidManifest와 DI 바인딩이 이에 맞춰 갱신되었습니다.

Changes

Cohort / File(s) Summary
매니페스트 / 호스트
app/src/main/AndroidManifest.xml, app/src/main/java/com/ku_stacks/ku_ring/HostActivity.kt
다수의 Activity 선언 삭제. HostActivity를 런처로 유지하며 android:exported="true", launchMode="singleTask", screenOrientation="portrait", windowSoftInputMode="adjustResize" 추가. Intent에서 NavKey 추출 및 Navigator 주입/딥링크 처리(onCreate/onNewIntent) 추가.
네비게이션 인터페이스·팩토리 추가/변경
domain/navigation/src/main/java/.../DeepLinkIntentFactory.kt, app/src/main/java/.../navigator/DeepLinkIntentFactoryImpl.kt
딥링크 인텐트 생성 책임을 가진 DeepLinkIntentFactory 인터페이스와 DeepLinkIntentFactoryImpl 구현 추가(HostActivity 대상 Intent 생성, 파라미터 검증 포함).
기존 네비게이터 제거 / DI 변경
domain/navigation/src/main/java/.../KuringNavigator.kt (삭제), app/src/main/java/.../navigator/KuringNavigatorImpl.kt (삭제), app/src/main/java/com/ku_stacks/ku_ring/di/NavigatorModule.kt
기존 KuringNavigator 인터페이스 및 구현 삭제. Hilt 바인딩을 KuringNavigatorDeepLinkIntentFactory/Navigator 연관 바인딩으로 전환.
중앙 Navigator 도입 / Compose 로컬 변경
core/navigation/src/main/java/com/ku_stacks/ku_ring/navigation/Navigator.kt, core/compose-locals/src/main/java/.../CompositionLocalEntryPoint.kt, core/compose-locals/build.gradle.kts
애플리케이션 전역의 상태형 Navigator 추가(backStack·navigate·goBack·replaceAll·pendingDeepLink 등). Compose 로컬 타입을 KuringNavigatorNavigator로 변경하고 모듈 의존성 core.navigation으로 조정.
FCM / Worker 인텐트 생성 변경
core/firebase-messaging/src/.../KuringMessagingService.kt, core/work/src/.../ReEngagementNotificationWork.kt
알림 처리 및 Worker에서 KuringNavigator 대신 DeepLinkIntentFactory 주입으로 변경하여 Intent 생성만 위임하도록 수정.
키·페이로드/직렬화 변경
core/navigation/src/main/java/.../keys/AppLifecycleKeys.kt
AuthFlowKey.entryPoint 타입을 String→직렬화된 AuthEntryPoint enum으로 변경.
Activity → EntryBuilder(Compose) 전환 (다수)
feature/splash/*, feature/onboarding/*, feature/main/*, feature/main/archive/*, feature/main/search/*, feature/main/setting/*, feature/notice_detail/*, feature/notification/*, feature/auth/*, feature/library/*, feature/club/*, feature/feedback/*, feature/kuringbot/*, feature/edit_*/*, feature/notion/*
여러 Activity 파일(예: SplashActivity, MainActivity, SearchActivity, NoticeWebActivity 등) 삭제. 대응하는 EntryBuilder/EntryBuilderModule 추가 및 각 화면의 닫기/뒤로동작을 Activity.finish() → LocalNavigator.current/navigator.goBack()로 전환. 관련 build.gradle에 core.navigation, core.composeLocals 의존성 추가.
화면 내 네비게이션 호출부 교체
feature/main/src/.../MainScreen.kt, feature/main/.../notice/NoticeScreen.kt, 기타 EntryBuilder 파일들
앱 전역에서 Activity 기반 네비게이션 호출을 키 기반 navigator.navigate(Key)Navigator API로 교체. Notice→NoticeWebKey 변환 등 딥링크 키 조립 로직 추가.

Sequence Diagram(s)

sequenceDiagram
    participant FCM as Firebase/MessagingService
    participant Work as WorkManager/Worker
    participant IntentFactory as DeepLinkIntentFactory
    participant System as OS (startActivity)
    participant Host as HostActivity
    participant Nav as Navigator
    participant Entry as EntryBuilder/Registry
    participant Screen as Composable Screen

    FCM->>IntentFactory: createMainIntent(...) / createNoticeWebIntent(...)
    Work->>IntentFactory: createMainIntent(...)
    IntentFactory->>System: startActivity(Intent targeting HostActivity)
    System->>Host: onCreate(intent) / onNewIntent(intent)
    Host->>Nav: setPendingDeepLink(key) (onCreate) / navigate(key) (onNewIntent)
    Nav->>Nav: backStack 업데이트 (add / replace)
    Host->>Entry: resolve EntryBuilder for backStack.last()
    Entry->>Screen: compose UI for key
    Screen->>Nav: navigate(otherKey) / goBack()
    Nav->>Nav: backStack 변경 (add/remove)
    Host->>Entry: resolve updated EntryBuilder
    Entry->>Screen: compose new UI
Loading

Possibly related PRs

Poem

🐰
호스트 안에 키를 품었네, 뒤로는 스택이 춤추네.
액티비티는 쉬고 Compose가 길을 잇네.
한 번의 Intent로 딥링크는 와서 쌓이고,
navigator가 길을 열면 화면이 피어나네.
당근 들고 축하해 — 토끼의 기쁨이 움트네 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 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 (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목이 변경 사항의 주요 내용을 명확하게 나타냅니다. 여러 Activity를 제거하고 단일 Activity(HostActivity) 아키텍처로 전환하는 Phase 2 마이그레이션의 핵심을 잘 요약합니다.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/nav-3-single-activity

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.

@l2hyunwoo l2hyunwoo force-pushed the feature/nav3-entry-builder-pattern branch from 125c671 to 929b9f6 Compare March 29, 2026 12:52
@l2hyunwoo l2hyunwoo force-pushed the feature/nav-3-single-activity branch from 4e8ac75 to 535c816 Compare March 29, 2026 12:54
Base automatically changed from feature/nav3-entry-builder-pattern to feature/nav-3-base March 29, 2026 13:02
@l2hyunwoo l2hyunwoo force-pushed the feature/nav-3-single-activity branch from 535c816 to 95e893c Compare March 29, 2026 13:03
NavKey 기반 네비게이션을 Activity 생명주기에서 분리하기 위한 추상화 인터페이스 추가
SplashActivity를 삭제하고 SplashEntryBuilder로 전환
OnboardingActivity를 삭제하고 OnboardingEntryBuilder로 전환
AuthActivity를 삭제하고 AuthFlowEntryBuilder로 전환
MainActivity, ArchiveActivity, SearchActivity, OpenSourceActivity를 삭제하고
MainHubEntryBuilder, ArchiveEntryBuilder, SearchEntryBuilder, OpenSourceEntryBuilder로 전환
EditDepartments, EditSubscription, Feedback, KuringBot, LibrarySeat,
Notification, NoticeWeb, NotionView, Club(Detail/Onboarding/Subscription)
Activity를 삭제하고 각각 EntryBuilder로 전환
…t 업데이트

NavigationController 기반으로 KuringNavigatorImpl 전환하고
AndroidManifest에서 삭제된 Activity 참조 정리
Nav3 스타일의 Navigator 클래스를 core/navigation에 추가하고
더 이상 필요 없는 NavigationController 인터페이스를 삭제
Intent 생성 메서드를 KuringNavigator에서 분리하여 DeepLinkIntentFactory 인터페이스로 추출.
KuringMessagingService와 ReEngagementNotificationWork가 새 인터페이스를 사용하도록 전환
NavigationController 브릿지를 제거하고 Navigator.backStack을 NavDisplay에 직접 전달.
빈 backStack에서 뒤로가기 시 앱 종료 처리 추가
- LocalNavigator 타입을 KuringNavigator에서 Navigator로 변경
- 모든 navigateTo*(activity) 호출을 navigator.navigate(NavKey)로 전환
- activity?.finish()를 navigator.goBack()으로 교체
- Splash→Onboarding/MainHub, Onboarding→MainHub에서 replaceAll 사용
- OssLicensesMenu 로직을 OpenSourceEntryBuilder 내부로 이동
- feature/auth, club, library에 composeLocals 의존 추가
Navigator 싱글톤으로 완전히 대체되었으므로 레거시 코드 제거
@l2hyunwoo l2hyunwoo force-pushed the feature/nav-3-base branch from 45455e9 to 888ff3d Compare March 29, 2026 13:04
@l2hyunwoo l2hyunwoo force-pushed the feature/nav-3-single-activity branch from 95e893c to 6a73077 Compare March 29, 2026 13:04
Copy link
Copy Markdown
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: 7

🧹 Nitpick comments (3)
feature/notice_detail/src/main/java/com/ku_stacks/ku_ring/notice_detail/noticeweb/NoticeWebScreen.kt (1)

242-242: AuthFlowKey("SIGN_IN") 문자열 리터럴 대신 상수나 팩토리로 교체하세요.

하드코딩된 문자열은 오타 위험이 있으므로, 가능하면 Auth 모듈에서 제공하는 상수나 타입-안전 팩토리 메서드를 사용하여 키 문자열을 중앙에서 관리하기를 권장합니다.

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

In
`@feature/notice_detail/src/main/java/com/ku_stacks/ku_ring/notice_detail/noticeweb/NoticeWebScreen.kt`
at line 242, Replace the hardcoded "SIGN_IN" literal passed to
navigator.navigate(AuthFlowKey("SIGN_IN")) with the Auth module's defined
constant or type-safe factory for the auth flow key (e.g., use an exported
SIGN_IN constant or an AuthFlowKey.create/signIn factory); update the call to
navigator.navigate(...) to use that symbol and add the appropriate import for
the Auth module so the key is centrally managed and typo-safe.
app/src/main/java/com/ku_stacks/ku_ring/HostActivity.kt (1)

86-95: route extra 키는 공용 계약으로 빼두는 편이 좋습니다.

지금은 HostActivityDeepLinkIntentFactoryImpl"MAIN_SCREEN_ROUTE"를 각각 들고 있어서 한쪽만 바뀌어도 메인 탭 딥링크가 조용히 깨집니다. navigation 공용 상수/contract로 올려 두 파일이 같은 심볼을 참조하게 만드는 편이 안전합니다.

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

In `@app/src/main/java/com/ku_stacks/ku_ring/HostActivity.kt` around lines 86 -
95, HostActivity currently defines INTENT_KEY_ROUTE = "MAIN_SCREEN_ROUTE" while
DeepLinkIntentFactoryImpl duplicates the same literal; extract that string into
a single shared constant (e.g., NavigationContract.MAIN_SCREEN_ROUTE or
DeepLinkContract.MAIN_SCREEN_ROUTE) in a common module/class and update
HostActivity (remove INTENT_KEY_ROUTE) and DeepLinkIntentFactoryImpl to
reference the new shared symbol so both use the same intent extra key when
creating/reading the route (see usage points
navigator.navigate(MainHubKey(startTab = route)) and any intent creation in
DeepLinkIntentFactoryImpl).
feature/main/src/main/java/com/ku_stacks/ku_ring/main/MainScreen.kt (1)

155-159: 공지 상세 매핑은 Notice 쪽으로 내려주세요.

toWebViewNotice()NoticeWebKey 생성까지 여기서 처리하면 MainScreen이 notice 도메인 모델을 직접 알게 됩니다. 이 파일은 탭 컨테이너 역할만 유지하고, 공지 전용 변환은 notice ViewModel/네비게이션 레이어에서 완성된 목적지만 넘기도록 분리하는 편이 안전합니다.

As per coding guidelines "Keep MainScreen logic tab-agnostic; move sub-feature business logic to respective sub-ViewModels".

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

In `@feature/main/src/main/java/com/ku_stacks/ku_ring/main/MainScreen.kt` around
lines 155 - 159, MainScreen currently converts notice models and builds
navigation keys in onNoticeClick (calls toWebViewNotice() and constructs
NoticeWebKey), which leaks notice-domain logic into the tab container; move the
mapping and NoticeWebKey creation into the notice feature (e.g., the Notice
ViewModel or a notice-specific navigator helper). Replace the onNoticeClick
handler in MainScreen to simply forward the raw notice item or an ID via a
single callback (e.g., onNoticeSelected(id or model)), then implement the
conversion toWebViewNotice() and NoticeWebKey construction inside the
NoticeViewModel or notice navigation layer and invoke navigator.navigate(...)
there.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/com/ku_stacks/ku_ring/HostActivity.kt`:
- Around line 30-35: The current handling of external intents in HostActivity
(onCreate/onNewIntent -> handleIntent) navigates immediately and can bypass the
Splash initialization flow and accumulate deep links on singleTask re-entry;
change handleIntent usage so external intents are deferred until after the
Splash flow completes (i.e., process the intent after SplashKey is resolved) or,
if you must navigate immediately, use Navigator.replaceAll(...) to rebuild the
root stack instead of pushing MainHubKey/NoticeWebKey directly; update logic in
onCreate/onNewIntent/handleIntent to either queue the incoming intent and
consume it after SplashKey finishes, or call
navigator.replaceAll(listOf(SplashKey, /* then target key */)) so the
initialization flow is preserved and deep links do not stack repeatedly.

In `@core/navigation/src/main/java/com/ku_stacks/ku_ring/navigation/Navigator.kt`:
- Around line 18-20: goBack() currently removes the last entry even when the
backStack only contains the root, which can leave the navigation state empty;
update Navigator.goBack to check backStack size and only removeLastOrNull when
backStack.size > 1 (keep at least one root entry), returning true if an item was
removed and false otherwise so the root is never popped.

In
`@domain/navigation/src/main/java/com/ku_stacks/ku_ring/navigation/DeepLinkIntentFactory.kt`:
- Around line 3-16: DeepLinkIntentFactory currently exposes Android types
(Context/Intent) via createMainIntent and createNoticeWebIntent which couples
domain/navigation to the Android framework; change the contract to return
domain-only route/state models (e.g., use MainScreenRoute and a new
WebViewNotice or NoticeRoute data class) and remove any android.* imports from
this file, leaving only pure Kotlin/domain types; then implement the actual
Intent creation and Context usage in the app navigator (implement the routing
logic in app/navigator/KuringNavigatorImpl.kt) so
createMainIntent/createNoticeWebIntent become domain-only methods that produce
route objects and the Android-specific conversion to Intent happens in the app
layer.

In
`@feature/auth/src/main/java/com/ku_stacks/ku_ring/auth/AuthFlowEntryBuilder.kt`:
- Around line 23-27: The code uses a magic string check on key.entryPoint
("SIGN_OUT") in AuthFlowEntryBuilder.kt which is not type-safe; change
AuthFlowKey from a raw String to a sealed class or enum (e.g.,
AuthFlowKey.EntryPoint { SIGN_IN, SIGN_OUT }) so callers pass a typed value,
then replace the if (key.entryPoint == "SIGN_OUT") branching with a when/on enum
branch that returns AuthDestination.SignOut or AuthDestination.SignIn
accordingly; also update any Compose Navigation route definitions to pass the
typed argument (or use nav arguments) so routing is type-safe per
feature/**/*Route.kt guidelines.

In
`@feature/club/src/main/java/com/ku_stacks/ku_ring/club/detail/ClubDetailEntryBuilder.kt`:
- Around line 28-29: The ViewModel (ClubDetailViewModel) is being created with
hiltViewModel() before its clubId is set, so its init load runs with a
wrong/empty ID; move or change initialization so clubId is assigned prior to
triggering load: either pass the clubId into the ViewModel factory/creation
(provide a SavedStateHandle or use a parameterized hilt-assisted injection) or
set clubId immediately on creation and then call an explicit load method (e.g.,
viewModel.loadClub()) instead of relying on init; update ClubDetailEntryBuilder
to ensure clubId is provided to ClubDetailViewModel before any network/load
logic runs.

In
`@feature/club/src/main/java/com/ku_stacks/ku_ring/club/detail/ClubDetailViewModel.kt`:
- Line 28: Make clubId immutable from outside by changing its setter to private
(e.g., var clubId by mutableIntStateOf(...) private set) and add a public setter
method setClubId(newId: Int) that updates clubId (using savedStateHandle if
needed) and then calls loadClub() to trigger re-fetch; reference the existing
clubId property, CLUB_ID_KEY/savedStateHandle for initial value, and the
loadClub() method when implementing setClubId to ensure state changes and
loading stay within the ViewModel’s API and preserve unidirectional data flow.

In
`@feature/onboarding/src/main/java/com/ku_stacks/ku_ring/onboarding/OnboardingEntryBuilder.kt`:
- Around line 32-35: 분석 이벤트 호출 analytics.click에서 전달하는 screenClass 값이 현재 UI 구조와
일치하지 않습니다; analytics.click(...) 호출의 screenClass 매개변수를 화면 단위 실제 이름(예:
"OnboardingScreen" 또는 해당 Compose 엔트리의 실제 이름)으로 변경해 주세요 so that the analytics
classification matches the EntryBuilder/Compose screen; update the string in the
analytics.click call (refer to the analytics.click invocation around the
OnboardingEntryBuilder) to the correct screen unit name.

---

Nitpick comments:
In `@app/src/main/java/com/ku_stacks/ku_ring/HostActivity.kt`:
- Around line 86-95: HostActivity currently defines INTENT_KEY_ROUTE =
"MAIN_SCREEN_ROUTE" while DeepLinkIntentFactoryImpl duplicates the same literal;
extract that string into a single shared constant (e.g.,
NavigationContract.MAIN_SCREEN_ROUTE or DeepLinkContract.MAIN_SCREEN_ROUTE) in a
common module/class and update HostActivity (remove INTENT_KEY_ROUTE) and
DeepLinkIntentFactoryImpl to reference the new shared symbol so both use the
same intent extra key when creating/reading the route (see usage points
navigator.navigate(MainHubKey(startTab = route)) and any intent creation in
DeepLinkIntentFactoryImpl).

In `@feature/main/src/main/java/com/ku_stacks/ku_ring/main/MainScreen.kt`:
- Around line 155-159: MainScreen currently converts notice models and builds
navigation keys in onNoticeClick (calls toWebViewNotice() and constructs
NoticeWebKey), which leaks notice-domain logic into the tab container; move the
mapping and NoticeWebKey creation into the notice feature (e.g., the Notice
ViewModel or a notice-specific navigator helper). Replace the onNoticeClick
handler in MainScreen to simply forward the raw notice item or an ID via a
single callback (e.g., onNoticeSelected(id or model)), then implement the
conversion toWebViewNotice() and NoticeWebKey construction inside the
NoticeViewModel or notice navigation layer and invoke navigator.navigate(...)
there.

In
`@feature/notice_detail/src/main/java/com/ku_stacks/ku_ring/notice_detail/noticeweb/NoticeWebScreen.kt`:
- Line 242: Replace the hardcoded "SIGN_IN" literal passed to
navigator.navigate(AuthFlowKey("SIGN_IN")) with the Auth module's defined
constant or type-safe factory for the auth flow key (e.g., use an exported
SIGN_IN constant or an AuthFlowKey.create/signIn factory); update the call to
navigator.navigate(...) to use that symbol and add the appropriate import for
the Auth module so the key is centrally managed and typo-safe.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 948a7223-596d-4daa-bcf8-b4ab50aef4b4

📥 Commits

Reviewing files that changed from the base of the PR and between 888ff3d and 6a73077.

📒 Files selected for processing (58)
  • app/src/main/AndroidManifest.xml
  • app/src/main/java/com/ku_stacks/ku_ring/HostActivity.kt
  • app/src/main/java/com/ku_stacks/ku_ring/di/NavigatorModule.kt
  • app/src/main/java/com/ku_stacks/ku_ring/navigator/DeepLinkIntentFactoryImpl.kt
  • app/src/main/java/com/ku_stacks/ku_ring/navigator/KuringNavigatorImpl.kt
  • core/compose-locals/build.gradle.kts
  • core/compose-locals/src/main/java/com/ku_stacks/ku_ring/compose/locals/di/CompositionLocalEntryPoint.kt
  • core/firebase-messaging/src/main/java/com/ku_stacks/ku_ring/firebase/messaging/KuringMessagingService.kt
  • core/navigation/src/main/java/com/ku_stacks/ku_ring/navigation/Navigator.kt
  • core/work/src/main/java/com/ku_stacks/ku_ring/work/ReEngagementNotificationWork.kt
  • domain/navigation/src/main/java/com/ku_stacks/ku_ring/navigation/DeepLinkIntentFactory.kt
  • domain/navigation/src/main/java/com/ku_stacks/ku_ring/navigation/KuringNavigator.kt
  • feature/auth/build.gradle.kts
  • feature/auth/src/main/java/com/ku_stacks/ku_ring/auth/AuthActivity.kt
  • feature/auth/src/main/java/com/ku_stacks/ku_ring/auth/AuthFlowEntryBuilder.kt
  • feature/club/build.gradle.kts
  • feature/club/src/main/java/com/ku_stacks/ku_ring/club/detail/ClubDetailActivity.kt
  • feature/club/src/main/java/com/ku_stacks/ku_ring/club/detail/ClubDetailEntryBuilder.kt
  • feature/club/src/main/java/com/ku_stacks/ku_ring/club/detail/ClubDetailViewModel.kt
  • feature/club/src/main/java/com/ku_stacks/ku_ring/club/onboarding/ClubOnboardingActivity.kt
  • feature/club/src/main/java/com/ku_stacks/ku_ring/club/onboarding/ClubOnboardingEntryBuilder.kt
  • feature/club/src/main/java/com/ku_stacks/ku_ring/club/subscription/ClubSubscriptionActivity.kt
  • feature/club/src/main/java/com/ku_stacks/ku_ring/club/subscription/ClubSubscriptionEntryBuilder.kt
  • feature/edit_departments/src/main/java/com/ku_stacks/ku_ring/edit_departments/EditDepartmentsActivity.kt
  • feature/edit_departments/src/main/java/com/ku_stacks/ku_ring/edit_departments/EditDepartmentsEntryBuilder.kt
  • feature/edit_subscription/src/main/java/com/ku_stacks/ku_ring/edit_subscription/EditSubscriptionActivity.kt
  • feature/edit_subscription/src/main/java/com/ku_stacks/ku_ring/edit_subscription/EditSubscriptionEntryBuilder.kt
  • feature/feedback/src/main/java/com/ku_stacks/ku_ring/feedback/feedback/FeedbackActivity.kt
  • feature/feedback/src/main/java/com/ku_stacks/ku_ring/feedback/feedback/FeedbackEntryBuilder.kt
  • feature/kuringbot/src/main/java/com/ku_stacks/ku_ring/kuringbot/KuringBotActivity.kt
  • feature/kuringbot/src/main/java/com/ku_stacks/ku_ring/kuringbot/KuringBotEntryBuilder.kt
  • feature/library/build.gradle.kts
  • feature/library/src/main/java/com/ku_stacks/ku_ring/library/LibrarySeatActivity.kt
  • feature/library/src/main/java/com/ku_stacks/ku_ring/library/LibrarySeatEntryBuilder.kt
  • feature/main/build.gradle.kts
  • feature/main/src/main/java/com/ku_stacks/ku_ring/main/MainActivity.kt
  • feature/main/src/main/java/com/ku_stacks/ku_ring/main/MainHubEntryBuilder.kt
  • feature/main/src/main/java/com/ku_stacks/ku_ring/main/MainScreen.kt
  • feature/main/src/main/java/com/ku_stacks/ku_ring/main/archive/ArchiveActivity.kt
  • feature/main/src/main/java/com/ku_stacks/ku_ring/main/archive/ArchiveEntryBuilder.kt
  • feature/main/src/main/java/com/ku_stacks/ku_ring/main/archive/compose/ArchiveScreen.kt
  • feature/main/src/main/java/com/ku_stacks/ku_ring/main/notice/compose/NoticeScreen.kt
  • feature/main/src/main/java/com/ku_stacks/ku_ring/main/search/SearchActivity.kt
  • feature/main/src/main/java/com/ku_stacks/ku_ring/main/search/SearchEntryBuilder.kt
  • feature/main/src/main/java/com/ku_stacks/ku_ring/main/setting/compose/OpenSourceActivity.kt
  • feature/main/src/main/java/com/ku_stacks/ku_ring/main/setting/compose/OpenSourceEntryBuilder.kt
  • feature/notice_detail/src/main/java/com/ku_stacks/ku_ring/notice_detail/NoticeWebActivity.kt
  • feature/notice_detail/src/main/java/com/ku_stacks/ku_ring/notice_detail/NoticeWebEntryBuilder.kt
  • feature/notice_detail/src/main/java/com/ku_stacks/ku_ring/notice_detail/noticeweb/NoticeWebScreen.kt
  • feature/notification/src/main/java/com/ku_stacks/ku_ring/notification/NotificationActivity.kt
  • feature/notification/src/main/java/com/ku_stacks/ku_ring/notification/NotificationEntryBuilder.kt
  • feature/notion/src/main/java/com/ku_stacks/ku_ring/notion/NotionViewActivity.kt
  • feature/onboarding/build.gradle.kts
  • feature/onboarding/src/main/java/com/ku_stacks/ku_ring/onboarding/OnboardingActivity.kt
  • feature/onboarding/src/main/java/com/ku_stacks/ku_ring/onboarding/OnboardingEntryBuilder.kt
  • feature/splash/build.gradle.kts
  • feature/splash/src/main/java/com/ku_stacks/ku_ring/splash/SplashActivity.kt
  • feature/splash/src/main/java/com/ku_stacks/ku_ring/splash/SplashEntryBuilder.kt
💤 Files with no reviewable changes (20)
  • feature/library/src/main/java/com/ku_stacks/ku_ring/library/LibrarySeatActivity.kt
  • feature/club/src/main/java/com/ku_stacks/ku_ring/club/subscription/ClubSubscriptionActivity.kt
  • feature/edit_subscription/src/main/java/com/ku_stacks/ku_ring/edit_subscription/EditSubscriptionActivity.kt
  • feature/kuringbot/src/main/java/com/ku_stacks/ku_ring/kuringbot/KuringBotActivity.kt
  • feature/club/src/main/java/com/ku_stacks/ku_ring/club/onboarding/ClubOnboardingActivity.kt
  • feature/feedback/src/main/java/com/ku_stacks/ku_ring/feedback/feedback/FeedbackActivity.kt
  • feature/notification/src/main/java/com/ku_stacks/ku_ring/notification/NotificationActivity.kt
  • feature/notion/src/main/java/com/ku_stacks/ku_ring/notion/NotionViewActivity.kt
  • feature/edit_departments/src/main/java/com/ku_stacks/ku_ring/edit_departments/EditDepartmentsActivity.kt
  • feature/main/src/main/java/com/ku_stacks/ku_ring/main/setting/compose/OpenSourceActivity.kt
  • feature/club/src/main/java/com/ku_stacks/ku_ring/club/detail/ClubDetailActivity.kt
  • feature/onboarding/src/main/java/com/ku_stacks/ku_ring/onboarding/OnboardingActivity.kt
  • feature/notice_detail/src/main/java/com/ku_stacks/ku_ring/notice_detail/NoticeWebActivity.kt
  • feature/auth/src/main/java/com/ku_stacks/ku_ring/auth/AuthActivity.kt
  • feature/main/src/main/java/com/ku_stacks/ku_ring/main/archive/ArchiveActivity.kt
  • feature/main/src/main/java/com/ku_stacks/ku_ring/main/search/SearchActivity.kt
  • feature/splash/src/main/java/com/ku_stacks/ku_ring/splash/SplashActivity.kt
  • feature/main/src/main/java/com/ku_stacks/ku_ring/main/MainActivity.kt
  • domain/navigation/src/main/java/com/ku_stacks/ku_ring/navigation/KuringNavigator.kt
  • app/src/main/java/com/ku_stacks/ku_ring/navigator/KuringNavigatorImpl.kt

Comment thread app/src/main/java/com/ku_stacks/ku_ring/HostActivity.kt Outdated
Comment on lines +3 to +16
import android.content.Context
import android.content.Intent
import com.ku_stacks.ku_ring.domain.WebViewNotice

interface DeepLinkIntentFactory {
fun createMainIntent(context: Context, route: MainScreenRoute = MainScreenRoute.Notice): Intent
fun createNoticeWebIntent(
context: Context,
url: String?,
articleId: String?,
id: Int?,
category: String?,
subject: String?,
): Intent
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

도메인 모듈에 Android 타입 의존이 들어와 아키텍처 경계를 깨고 있습니다.

DeepLinkIntentFactoryContext/Intent를 직접 노출해 domain/navigation이 Android 프레임워크에 결합됩니다. 이 계약은 app 네비게이터 계층으로 이동하고, 도메인에는 route/state 모델만 남기는 구조로 분리해 주세요.

As per coding guidelines domain/**/src/main/java/**/*.kt: Domain module Kotlin files must not import Android Framework classes (android.* or androidx.*), and domain/navigation/**/*.kt: Define navigation state in domain:navigation module and implement navigation routing in app/navigator/KuringNavigatorImpl.kt.

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

In
`@domain/navigation/src/main/java/com/ku_stacks/ku_ring/navigation/DeepLinkIntentFactory.kt`
around lines 3 - 16, DeepLinkIntentFactory currently exposes Android types
(Context/Intent) via createMainIntent and createNoticeWebIntent which couples
domain/navigation to the Android framework; change the contract to return
domain-only route/state models (e.g., use MainScreenRoute and a new
WebViewNotice or NoticeRoute data class) and remove any android.* imports from
this file, leaving only pure Kotlin/domain types; then implement the actual
Intent creation and Context usage in the app navigator (implement the routing
logic in app/navigator/KuringNavigatorImpl.kt) so
createMainIntent/createNoticeWebIntent become domain-only methods that produce
route objects and the Android-specific conversion to Intent happens in the app
layer.

Comment thread feature/auth/src/main/java/com/ku_stacks/ku_ring/auth/AuthFlowEntryBuilder.kt Outdated
Comment on lines +28 to +29
val viewModel = hiltViewModel<ClubDetailViewModel>()
viewModel.clubId = key.clubId
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

초기 로딩이 잘못된 clubId로 실행될 수 있습니다.

Line 28에서 ViewModel이 생성될 때 이미 init 로딩이 수행되는데, Line 29 대입은 그 이후라서 첫 요청이 잘못된 ID로 나가고 재조회도 자동으로 일어나지 않습니다. 현재 구조는 상세 데이터 로딩 실패로 이어질 수 있습니다.

수정 방향 예시
+import androidx.compose.runtime.LaunchedEffect
 ...
             val viewModel = hiltViewModel<ClubDetailViewModel>()
-            viewModel.clubId = key.clubId
+            LaunchedEffect(key.clubId) {
+                viewModel.setClubId(key.clubId)
+            }
// ClubDetailViewModel.kt
-    var clubId by mutableIntStateOf(savedStateHandle.get<Int>(CLUB_ID_KEY) ?: -1)
+    var clubId by mutableIntStateOf(savedStateHandle.get<Int>(CLUB_ID_KEY) ?: -1)
+        private set
+
+    fun setClubId(newClubId: Int) {
+        if (clubId == newClubId) return
+        clubId = newClubId
+        loadClub()
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/club/src/main/java/com/ku_stacks/ku_ring/club/detail/ClubDetailEntryBuilder.kt`
around lines 28 - 29, The ViewModel (ClubDetailViewModel) is being created with
hiltViewModel() before its clubId is set, so its init load runs with a
wrong/empty ID; move or change initialization so clubId is assigned prior to
triggering load: either pass the clubId into the ViewModel factory/creation
(provide a SavedStateHandle or use a parameterized hilt-assisted injection) or
set clubId immediately on creation and then call an explicit load method (e.g.,
viewModel.loadClub()) instead of relying on init; update ClubDetailEntryBuilder
to ensure clubId is provided to ClubDetailViewModel before any network/load
logic runs.

@l2hyunwoo
Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 29, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
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.

♻️ Duplicate comments (1)
app/src/main/java/com/ku_stacks/ku_ring/HostActivity.kt (1)

35-37: ⚠️ Potential issue | 🟠 Major

외부 인텐트를 즉시 navigate()하면 스플래시 초기화 우회 + 스택 누적 문제가 계속 남습니다.

Line 35/Line 63에서 즉시 처리된 딥링크가 Line 74/Line 89의 navigate로 바로 push되어, cold start에서는 초기 플로우를 건너뛰고 singleTask 재진입에서는 목적지가 계속 쌓일 수 있습니다. 스플래시 완료 후 소비하거나, 최소한 루트 스택 재구성 방식으로 처리해 누적을 막아주세요.

Also applies to: 61-64, 73-90

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

In `@app/src/main/java/com/ku_stacks/ku_ring/HostActivity.kt` around lines 35 -
37, handleIntent immediately calls navigate on incoming external intents causing
splash flow to be skipped and destinations to accumulate; modify HostActivity so
handleIntent only records the incoming deep link (e.g., store it in a
pendingIntent/deepLink field) instead of calling navigate directly, then after
the splash/initial flow completes (in your splash completion callback or
onPostSplash method) consume the pending deep link and perform navigation,
making sure to reconstruct the root stack (clearBackStack or use a
singleTop/singleTask-safe nav method) before pushing the destination to avoid
duplicate stacking; update references to handleIntent, navigate, and the splash
completion handler to implement this deferred-and-consume behavior.
🧹 Nitpick comments (2)
core/navigation/src/main/java/com/ku_stacks/ku_ring/navigation/keys/AppLifecycleKeys.kt (1)

10-14: enum 직렬화 값은 명시적으로 고정해두는 것을 권장합니다.

현재는 enum 이름(SIGN_IN, SIGN_OUT)이 그대로 직렬화 포맷이 됩니다. 추후 enum 상수명 리네임 시 직렬화 호환성이 깨질 수 있어 @SerialName으로 wire format을 고정하는 편이 안전합니다.

제안 diff
 import androidx.navigation3.runtime.NavKey
+import kotlinx.serialization.SerialName
 import kotlinx.serialization.Serializable

 `@Serializable` data object SplashKey : NavKey
 `@Serializable` data object OnboardingKey : NavKey
 `@Serializable` data class AuthFlowKey(val entryPoint: AuthEntryPoint) : NavKey

 `@Serializable`
 enum class AuthEntryPoint {
-    SIGN_IN,
-    SIGN_OUT,
+    `@SerialName`("sign_in")
+    SIGN_IN,
+    `@SerialName`("sign_out")
+    SIGN_OUT,
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/navigation/src/main/java/com/ku_stacks/ku_ring/navigation/keys/AppLifecycleKeys.kt`
around lines 10 - 14, The AuthEntryPoint enum's serialized wire values rely on
constant names which can break compatibility if constants are renamed; update
AuthEntryPoint (keeping the `@Serializable` annotation) to use explicit
`@SerialName` annotations on each enum constant (e.g., annotate SIGN_IN and
SIGN_OUT with stable string names) so the wire format is fixed and renames won't
break serialization; ensure the kotlinx.serialization.SerialName import is added
if missing.
app/src/main/java/com/ku_stacks/ku_ring/navigator/DeepLinkIntentFactoryImpl.kt (1)

13-14: 하드코딩된 HostActivity 문자열을 타입 안전한 Intent 생성으로 변경하세요.

현재 방식은 리팩터링(패키지/클래스명 변경) 시 런타임 오류로만 드러납니다. Intent(context, HostActivity::class.java)로 교체하면 컴파일 시점에 검증되어 더 안전합니다.

제안 diff
+import com.ku_stacks.ku_ring.HostActivity
+
 class DeepLinkIntentFactoryImpl `@Inject` constructor() : DeepLinkIntentFactory {
 
     override fun createMainIntent(context: Context, route: MainScreenRoute): Intent {
-        return Intent().setClassName(context, "com.ku_stacks.ku_ring.HostActivity").apply {
+        return Intent(context, HostActivity::class.java).apply {
             putExtra(DeepLinkIntentFactory.INTENT_KEY_ROUTE, route.route)
         }
     }
 
     override fun createNoticeWebIntent(
         context: Context,
         url: String?,
         articleId: String?,
         id: Int?,
         category: String?,
         subject: String?,
     ): Intent {
         if (url == null || articleId == null || category == null || id == null) {
             throw IllegalArgumentException("intent parameters shouldn't be null: $url, $articleId, $id, $category")
         }
-        return Intent().setClassName(context, "com.ku_stacks.ku_ring.HostActivity").apply {
+        return Intent(context, HostActivity::class.java).apply {
             putExtra(
                 WebViewNotice.EXTRA_KEY,
                 WebViewNotice(url, articleId, id, category, subject.orEmpty()),
             )
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/ku_stacks/ku_ring/navigator/DeepLinkIntentFactoryImpl.kt`
around lines 13 - 14, Replace the hardcoded class-name Intent creation in
DeepLinkIntentFactoryImpl (currently using Intent().setClassName(context,
"com.ku_stacks.ku_ring.HostActivity")) with a type-safe constructor
Intent(context, HostActivity::class.java); keep the existing extras
(DeepLinkIntentFactory.INTENT_KEY_ROUTE and route.route) and update imports to
include the HostActivity class so compilation catches renames/refactors instead
of causing runtime errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@app/src/main/java/com/ku_stacks/ku_ring/HostActivity.kt`:
- Around line 35-37: handleIntent immediately calls navigate on incoming
external intents causing splash flow to be skipped and destinations to
accumulate; modify HostActivity so handleIntent only records the incoming deep
link (e.g., store it in a pendingIntent/deepLink field) instead of calling
navigate directly, then after the splash/initial flow completes (in your splash
completion callback or onPostSplash method) consume the pending deep link and
perform navigation, making sure to reconstruct the root stack (clearBackStack or
use a singleTop/singleTask-safe nav method) before pushing the destination to
avoid duplicate stacking; update references to handleIntent, navigate, and the
splash completion handler to implement this deferred-and-consume behavior.

---

Nitpick comments:
In
`@app/src/main/java/com/ku_stacks/ku_ring/navigator/DeepLinkIntentFactoryImpl.kt`:
- Around line 13-14: Replace the hardcoded class-name Intent creation in
DeepLinkIntentFactoryImpl (currently using Intent().setClassName(context,
"com.ku_stacks.ku_ring.HostActivity")) with a type-safe constructor
Intent(context, HostActivity::class.java); keep the existing extras
(DeepLinkIntentFactory.INTENT_KEY_ROUTE and route.route) and update imports to
include the HostActivity class so compilation catches renames/refactors instead
of causing runtime errors.

In
`@core/navigation/src/main/java/com/ku_stacks/ku_ring/navigation/keys/AppLifecycleKeys.kt`:
- Around line 10-14: The AuthEntryPoint enum's serialized wire values rely on
constant names which can break compatibility if constants are renamed; update
AuthEntryPoint (keeping the `@Serializable` annotation) to use explicit
`@SerialName` annotations on each enum constant (e.g., annotate SIGN_IN and
SIGN_OUT with stable string names) so the wire format is fixed and renames won't
break serialization; ensure the kotlinx.serialization.SerialName import is added
if missing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: abadaa66-7c6e-4fc9-91a8-0ed1236c6e8a

📥 Commits

Reviewing files that changed from the base of the PR and between 6a73077 and 34d1895.

📒 Files selected for processing (12)
  • app/src/main/java/com/ku_stacks/ku_ring/HostActivity.kt
  • app/src/main/java/com/ku_stacks/ku_ring/navigator/DeepLinkIntentFactoryImpl.kt
  • core/navigation/src/main/java/com/ku_stacks/ku_ring/navigation/Navigator.kt
  • core/navigation/src/main/java/com/ku_stacks/ku_ring/navigation/keys/AppLifecycleKeys.kt
  • domain/navigation/src/main/java/com/ku_stacks/ku_ring/navigation/DeepLinkIntentFactory.kt
  • feature/auth/src/main/java/com/ku_stacks/ku_ring/auth/AuthFlowEntryBuilder.kt
  • feature/club/src/main/java/com/ku_stacks/ku_ring/club/detail/ClubDetailEntryBuilder.kt
  • feature/club/src/main/java/com/ku_stacks/ku_ring/club/detail/ClubDetailViewModel.kt
  • feature/kuringbot/src/main/java/com/ku_stacks/ku_ring/kuringbot/KuringBotEntryBuilder.kt
  • feature/main/src/main/java/com/ku_stacks/ku_ring/main/MainScreen.kt
  • feature/notice_detail/src/main/java/com/ku_stacks/ku_ring/notice_detail/noticeweb/NoticeWebScreen.kt
  • feature/onboarding/src/main/java/com/ku_stacks/ku_ring/onboarding/OnboardingEntryBuilder.kt
✅ Files skipped from review due to trivial changes (2)
  • feature/notice_detail/src/main/java/com/ku_stacks/ku_ring/notice_detail/noticeweb/NoticeWebScreen.kt
  • domain/navigation/src/main/java/com/ku_stacks/ku_ring/navigation/DeepLinkIntentFactory.kt
🚧 Files skipped from review as they are similar to previous changes (7)
  • feature/kuringbot/src/main/java/com/ku_stacks/ku_ring/kuringbot/KuringBotEntryBuilder.kt
  • feature/club/src/main/java/com/ku_stacks/ku_ring/club/detail/ClubDetailEntryBuilder.kt
  • core/navigation/src/main/java/com/ku_stacks/ku_ring/navigation/Navigator.kt
  • feature/club/src/main/java/com/ku_stacks/ku_ring/club/detail/ClubDetailViewModel.kt
  • feature/auth/src/main/java/com/ku_stacks/ku_ring/auth/AuthFlowEntryBuilder.kt
  • feature/onboarding/src/main/java/com/ku_stacks/ku_ring/onboarding/OnboardingEntryBuilder.kt
  • feature/main/src/main/java/com/ku_stacks/ku_ring/main/MainScreen.kt

@l2hyunwoo
Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 29, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
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: 2

🧹 Nitpick comments (1)
core/navigation/src/main/java/com/ku_stacks/ku_ring/navigation/Navigator.kt (1)

14-14: pendingDeepLink 동시성 고려 필요.

pendingDeepLinkvar로 선언되어 있어, 다른 스레드에서 동시 접근 시 race condition이 발생할 수 있습니다. 현재 코드에서는 대부분 메인 스레드에서 접근하지만, 방어적으로 @Volatile 어노테이션 또는 AtomicReference를 사용하는 것이 안전합니다.

Optional: Volatile 적용
-    private var pendingDeepLink: NavKey? = null
+    `@Volatile`
+    private var pendingDeepLink: NavKey? = null
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/navigation/src/main/java/com/ku_stacks/ku_ring/navigation/Navigator.kt`
at line 14, The field pendingDeepLink in Navigator is a mutable var and can
suffer race conditions across threads; make access thread-safe by either marking
pendingDeepLink as `@Volatile` (add the annotation above the declaration) or
replace it with an AtomicReference<NavKey?> (change the type to AtomicReference
and adjust reads/writes to use get()/set()/compareAndSet as needed), updating
all usages in Navigator (reads/writes) to use the chosen concurrency-safe
pattern.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/com/ku_stacks/ku_ring/HostActivity.kt`:
- Around line 93-97:
intent.getStringExtra(DeepLinkIntentFactory.INTENT_KEY_ROUTE)로 받은 route 값을 검증하지
않고 MainHubKey(startTab = route)를 반환하고 있어 잘못된 탭명이 들어오면 예기치 않은 동작이 발생할 수 있으니,
intent.getStringExtra(...)로 받은 route를 허용되는 탭 이름 집합(또는 기존 MainScreenRoute enum/상수
목록)과 대조하여 유효할 때만 MainHubKey에 전달하고, 유효하지 않다면 기본 탭으로 폴백하거나 null 반환하도록 변경하세요; 또는 더
나은 타입 안전성을 위해 MainHubKey의 startTab을 문자열 대신 enum/sealed class 타입으로 바꾸어 파싱/검증을
강제하세요.

In `@core/navigation/src/main/java/com/ku_stacks/ku_ring/navigation/Navigator.kt`:
- Around line 29-34: replaceAll currently clears pendingDeepLink unconditionally
causing deep links to be lost when onboarding occurs; change replaceAll(NavKey)
so it preserves pendingDeepLink when the new key is an onboarding entry (e.g.,
OnboardingKey) instead of consuming it, and add an explicit
consumePendingDeepLink(): Boolean (or similar) that appends pendingDeepLink to
backStack and nulls it only when onboarding is finished (call from the
onboarding completion flow, e.g., after OnboardingKey is replaced with
MainHubKey), referencing replaceAll, pendingDeepLink, backStack,
setPendingDeepLink and the new consumePendingDeepLink to control when the deep
link is applied.

---

Nitpick comments:
In `@core/navigation/src/main/java/com/ku_stacks/ku_ring/navigation/Navigator.kt`:
- Line 14: The field pendingDeepLink in Navigator is a mutable var and can
suffer race conditions across threads; make access thread-safe by either marking
pendingDeepLink as `@Volatile` (add the annotation above the declaration) or
replace it with an AtomicReference<NavKey?> (change the type to AtomicReference
and adjust reads/writes to use get()/set()/compareAndSet as needed), updating
all usages in Navigator (reads/writes) to use the chosen concurrency-safe
pattern.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2b4a34e3-ca69-4ab0-b94a-a136bc32213c

📥 Commits

Reviewing files that changed from the base of the PR and between 34d1895 and 7f283ef.

📒 Files selected for processing (4)
  • app/src/main/java/com/ku_stacks/ku_ring/HostActivity.kt
  • app/src/main/java/com/ku_stacks/ku_ring/navigator/DeepLinkIntentFactoryImpl.kt
  • core/navigation/src/main/java/com/ku_stacks/ku_ring/navigation/Navigator.kt
  • core/navigation/src/main/java/com/ku_stacks/ku_ring/navigation/keys/AppLifecycleKeys.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • core/navigation/src/main/java/com/ku_stacks/ku_ring/navigation/keys/AppLifecycleKeys.kt
  • app/src/main/java/com/ku_stacks/ku_ring/navigator/DeepLinkIntentFactoryImpl.kt

Comment thread app/src/main/java/com/ku_stacks/ku_ring/HostActivity.kt
Comment on lines +29 to +34
fun replaceAll(key: NavKey) {
backStack.clear()
backStack.add(key)
pendingDeepLink?.let { backStack.add(it) }
pendingDeepLink = null
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

온보딩 플로우에서 딥링크가 손실될 수 있습니다.

replaceAll 로직 분석 결과:

  1. 앱 cold start 시 setPendingDeepLink(key) 호출
  2. Splash 완료 후 온보딩 필요 시 replaceAll(OnboardingKey) 호출
  3. backStack = [OnboardingKey, deepLinkKey] — 딥링크가 스택 상단에 추가됨
  4. 사용자가 온보딩 완료 시 replaceAll(MainHubKey()) 호출 → 딥링크 손실

온보딩이 필요한 경우 딥링크를 별도로 보존하거나, 온보딩 완료 후에만 pendingDeepLink를 소비하도록 수정이 필요합니다.

수정 제안: 온보딩 플로우에서 딥링크 보존
 fun replaceAll(key: NavKey) {
     backStack.clear()
     backStack.add(key)
-    pendingDeepLink?.let { backStack.add(it) }
-    pendingDeepLink = null
+    // 온보딩 화면으로 전환 시에는 딥링크를 보존
+    // MainHubKey로 전환 시에만 딥링크 소비
+    if (key is MainHubKey) {
+        pendingDeepLink?.let { backStack.add(it) }
+        pendingDeepLink = null
+    }
 }

또는 consumePendingDeepLink(): Boolean 메서드를 별도로 분리하여 호출 시점을 명시적으로 제어할 수 있습니다.

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

In `@core/navigation/src/main/java/com/ku_stacks/ku_ring/navigation/Navigator.kt`
around lines 29 - 34, replaceAll currently clears pendingDeepLink
unconditionally causing deep links to be lost when onboarding occurs; change
replaceAll(NavKey) so it preserves pendingDeepLink when the new key is an
onboarding entry (e.g., OnboardingKey) instead of consuming it, and add an
explicit consumePendingDeepLink(): Boolean (or similar) that appends
pendingDeepLink to backStack and nulls it only when onboarding is finished (call
from the onboarding completion flow, e.g., after OnboardingKey is replaced with
MainHubKey), referencing replaceAll, pendingDeepLink, backStack,
setPendingDeepLink and the new consumePendingDeepLink to control when the deep
link is applied.

@l2hyunwoo
Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 29, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
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: 3

♻️ Duplicate comments (1)
app/src/main/java/com/ku_stacks/ku_ring/HostActivity.kt (1)

63-74: ⚠️ Potential issue | 🟠 Major

Splash가 아직 top일 때는 새 인텐트도 defer해야 합니다.

지금은 onNewIntent()가 항상 deferred = false로 들어와서, singleTask 인스턴스가 아직 SplashKey를 표시 중일 때 Line 73에서 바로 navigate(key)합니다. 그러면 splash 초기화/업데이트 분기를 우회할 수 있으니, 현재 top이 SplashKey면 cold start와 동일하게 pending deep link로 보류하는 쪽이 안전합니다.

수정 예시
 private fun handleIntent(intent: Intent, deferred: Boolean) {
     val key = extractNavKey(intent) ?: return
-    if (deferred) {
+    val shouldDefer = deferred || navigator.backStack.lastOrNull() == SplashKey
+    if (shouldDefer) {
         navigator.setPendingDeepLink(key)
     } else {
         navigator.navigate(key)
     }
 }
import com.ku_stacks.ku_ring.navigation.keys.SplashKey
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/ku_stacks/ku_ring/HostActivity.kt` around lines 63 -
74, onNewIntent currently calls handleIntent(intent, deferred = false) which
causes navigate(key) to run even when the Splash screen is still the top; change
the logic so that new intents are deferred if the current top navigation key is
SplashKey. In practice, import SplashKey, detect the current top key (e.g., via
navigator.currentTopKey or equivalent), and call handleIntent(intent, deferred =
true) when that top is a SplashKey; keep using navigator.setPendingDeepLink(key)
for deferred handling and navigator.navigate(key) otherwise, and update any
reference to extractNavKey, onNewIntent, handleIntent,
navigator.setPendingDeepLink, and navigator.navigate accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@feature/notification/src/main/java/com/ku_stacks/ku_ring/notification/NotificationEntryBuilder.kt`:
- Around line 26-39: The click handler in NotificationEntryBuilder
(onNotificationClick) only handles NotificationContent.Notice, leaving
NotificationContent.Club and NotificationContent.Common unhandled so taps on
those items do nothing; update onNotificationClick to use a when on
notification.content covering Notice, Club, and Common (or an else branch) and
for each branch either call navigator.navigate with the appropriate destination
(e.g., a Club or Common navigation key) or explicitly handle no-op/logging so
the behavior is deterministic; reference the existing Notice handling that uses
NoticeWebKey and navigator.navigate as the pattern to follow when adding
Club/Common routing or an explicit branch.

In
`@feature/splash/src/main/java/com/ku_stacks/ku_ring/splash/SplashEntryBuilder.kt`:
- Around line 77-84: 현재 consumePendingDeepLink()는 pending 키를 backStack에
append하므로 MainHubKey같은 루트 키를 가진 cold-start 딥링크에서 backStack에 중복된 루트가 생깁니다;
SplashEntryBuilder.kt의 흐름에서 MainHubKey로 네비게이트할 때는 pending deep link를 append하지 않고
replaceAll로 루트 교체해야 합니다. 수정 방법: navigator.replaceAll(MainHubKey()) 후 바로
navigator.consumePendingDeepLink() 하지 말고 대신 Navigator의 pending 값을 확인해 pending이
MainHubKey 타입(루트 역할)이면 navigator.replaceAll(pendingKey)로 교체하고 아니면 기존처럼
navigator.consumePendingDeepLink()로 append하도록 분기 처리(참고 심볼:
navigator.replaceAll(), navigator.consumePendingDeepLink(), MainHubKey,
OnboardingKey 및 OnboardingEntryBuilder의 동일 패턴도 동일하게 적용).
- Around line 52-64: getUserData() is currently fire-and-forget (launched inside
viewModelScope) which allows updateDepartmentsFromRemote() /
checkUpdateRequired() to run before preferences are actually updated; change
getUserData() to be suspend (or provide a suspend wrapper that completes when
preferences are written) and call it with suspend semantics from the
LaunchedEffect, or have getUserData return a Job/Deferred and await it in
SplashEntryBuilder's LaunchedEffect before invoking
updateDepartmentsFromRemote() and viewModel.checkUpdateRequired(...); ensure you
await completion of getUserData() (referencing getUserData(),
updateDepartmentsFromRemote(), checkUpdateRequired(), and the LaunchedEffect
block) so onboarding branching based on screenState only runs after preferences
update.

---

Duplicate comments:
In `@app/src/main/java/com/ku_stacks/ku_ring/HostActivity.kt`:
- Around line 63-74: onNewIntent currently calls handleIntent(intent, deferred =
false) which causes navigate(key) to run even when the Splash screen is still
the top; change the logic so that new intents are deferred if the current top
navigation key is SplashKey. In practice, import SplashKey, detect the current
top key (e.g., via navigator.currentTopKey or equivalent), and call
handleIntent(intent, deferred = true) when that top is a SplashKey; keep using
navigator.setPendingDeepLink(key) for deferred handling and
navigator.navigate(key) otherwise, and update any reference to extractNavKey,
onNewIntent, handleIntent, navigator.setPendingDeepLink, and navigator.navigate
accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f62a51d3-756e-4bbc-a119-7f8dddc4f20b

📥 Commits

Reviewing files that changed from the base of the PR and between 7f283ef and f8c64ae.

📒 Files selected for processing (7)
  • app/src/main/java/com/ku_stacks/ku_ring/HostActivity.kt
  • core/navigation/src/main/java/com/ku_stacks/ku_ring/navigation/Navigator.kt
  • feature/club/src/main/java/com/ku_stacks/ku_ring/club/subscription/ClubSubscriptionEntryBuilder.kt
  • feature/main/src/main/java/com/ku_stacks/ku_ring/main/MainScreen.kt
  • feature/notification/src/main/java/com/ku_stacks/ku_ring/notification/NotificationEntryBuilder.kt
  • feature/onboarding/src/main/java/com/ku_stacks/ku_ring/onboarding/OnboardingEntryBuilder.kt
  • feature/splash/src/main/java/com/ku_stacks/ku_ring/splash/SplashEntryBuilder.kt
✅ Files skipped from review due to trivial changes (1)
  • feature/main/src/main/java/com/ku_stacks/ku_ring/main/MainScreen.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • feature/club/src/main/java/com/ku_stacks/ku_ring/club/subscription/ClubSubscriptionEntryBuilder.kt
  • core/navigation/src/main/java/com/ku_stacks/ku_ring/navigation/Navigator.kt

Comment on lines +26 to +39
onNotificationClick = { notification ->
val content = notification.content
if (content is NotificationContent.Notice) {
navigator.navigate(
NoticeWebKey(
url = content.fullUrl,
articleId = content.articleId,
id = notification.id.toString(),
category = content.noticeCategory,
subject = content.subject,
)
)
}
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# NotificationContent subtype와 NotificationScreen의 클릭 전달 경로를 확인합니다.
fd 'Notification\.kt$' -x sed -n '1,120p' {}
fd 'NotificationScreen\.kt$' -x sed -n '1,260p' {}
rg -nC3 -g '*.kt' 'NotificationContent\.(Notice|Club|Common)|onNotificationClick\s*=|onNotificationClick\('

Repository: ku-ring/KU-Ring-Android

Length of output: 17117


Notice 외의 알림 클릭 시 라우팅 처리가 누락되어 있습니다.

도메인 모델상 NotificationContentNotice, Club, Common 세 가지 서브타입을 가지고 있으며, NotificationScreen에서는 모든 타입의 알림을 렌더링하고 클릭 가능하게 제공합니다. 그러나 NotificationEntryBuilder에서는 Notice만 처리하므로, Club이나 Common 타입의 알림을 탭하면 onNotificationClick 콜백이 실행되지만 네비게이션이 발생하지 않습니다.

의도적으로 이동 대상이 없다면 when에서 각 분기를 명시하고, 아니라면 해당 타입들에 대한 라우팅을 연결해야 합니다.

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

In
`@feature/notification/src/main/java/com/ku_stacks/ku_ring/notification/NotificationEntryBuilder.kt`
around lines 26 - 39, The click handler in NotificationEntryBuilder
(onNotificationClick) only handles NotificationContent.Notice, leaving
NotificationContent.Club and NotificationContent.Common unhandled so taps on
those items do nothing; update onNotificationClick to use a when on
notification.content covering Notice, Club, and Common (or an else branch) and
for each branch either call navigator.navigate with the appropriate destination
(e.g., a Club or Common navigation key) or explicitly handle no-op/logging so
the behavior is deterministic; reference the existing Notice handling that uses
NoticeWebKey and navigator.navigate as the pattern to follow when adding
Club/Common routing or an explicit branch.

Comment on lines +52 to +64
LaunchedEffect(Unit) {
// WorkManager re-engagement notification 예약
enqueueReengagementNotificationWork(context)

// 사용자 유효성 검증
viewModel.getUserData()

// 원격 학과 데이터 업데이트
viewModel.updateDepartmentsFromRemote()

// 최소 앱 버전 체크 (500ms delay 포함)
delay(500)
viewModel.checkUpdateRequired(context.getAppVersionName())
Copy link
Copy Markdown
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
set -euo pipefail

file="$(fd -i 'SplashViewModel.kt' | head -n 1)"
if [ -z "$file" ]; then
  echo "SplashViewModel.kt not found" >&2
  exit 1
fi

echo "== $file =="
sed -n '1,260p' "$file"

echo
echo "== Relevant definitions =="
rg -n -C4 'class\s+SplashViewModel\b|suspend\s+fun\s+getUserData\b|fun\s+getUserData\b|suspend\s+fun\s+updateDepartmentsFromRemote\b|fun\s+updateDepartmentsFromRemote\b|suspend\s+fun\s+checkUpdateRequired\b|fun\s+checkUpdateRequired\b|splashScreenState' "$file"

Repository: ku-ring/KU-Ring-Android

Length of output: 3849


getUserData()를 suspend 함수로 변경하거나 완료를 대기해야 합니다.

getUserData()viewModelScope.launch로 정의되어 있어 fire-and-forget 방식으로 동작합니다. 현재 코드는 getUserData()를 호출한 직후 즉시 updateDepartmentsFromRemote()로 진행하므로, 사용자 preference 갱신이 완료되기 전에 onboarding 분기 로직(screenState 관찰)이 실행될 수 있습니다.

getUserData() 호출 후 preferences가 실제로 업데이트될 때까지 대기하도록 수정하세요. suspend 함수로 변경하거나, LaunchedEffect 내에서 명시적으로 완료를 기다려야 합니다.

Also applies to: 68-79

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

In
`@feature/splash/src/main/java/com/ku_stacks/ku_ring/splash/SplashEntryBuilder.kt`
around lines 52 - 64, getUserData() is currently fire-and-forget (launched
inside viewModelScope) which allows updateDepartmentsFromRemote() /
checkUpdateRequired() to run before preferences are actually updated; change
getUserData() to be suspend (or provide a suspend wrapper that completes when
preferences are written) and call it with suspend semantics from the
LaunchedEffect, or have getUserData return a Job/Deferred and await it in
SplashEntryBuilder's LaunchedEffect before invoking
updateDepartmentsFromRemote() and viewModel.checkUpdateRequired(...); ensure you
await completion of getUserData() (referencing getUserData(),
updateDepartmentsFromRemote(), checkUpdateRequired(), and the LaunchedEffect
block) so onboarding branching based on screenState only runs after preferences
update.

Comment on lines +77 to +84
delay(500)
val onboardingRequired = preferences.firstRunFlag && preferences.subscription.isEmpty()
if (onboardingRequired) {
createNotificationChannel(context)
navigator.replaceAll(OnboardingKey)
} else {
navigator.replaceAll(MainHubKey())
navigator.consumePendingDeepLink()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

루트형 딥링크는 append가 아니라 root replacement로 처리해야 합니다.

Navigator.consumePendingDeepLink()는 pending 키를 그대로 backStack.add(...) 합니다 (core/navigation/src/main/java/com/ku_stacks/ku_ring/navigation/Navigator.kt:30-38). 그래서 pending 값이 MainHubKey(startTab = ...)인 cold start 탭 딥링크에서는 여기서 [MainHubKey(), MainHubKey(route)] 스택이 만들어지고, 뒤로 가기가 앱 종료 대신 기본 탭으로 떨어집니다. MainHubKey처럼 루트 역할인 키는 append하지 말고 root replacement로 처리해야 합니다. 같은 패턴이 feature/onboarding/src/main/java/com/ku_stacks/ku_ring/onboarding/OnboardingEntryBuilder.kt Line 31-32에도 있습니다.

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

In
`@feature/splash/src/main/java/com/ku_stacks/ku_ring/splash/SplashEntryBuilder.kt`
around lines 77 - 84, 현재 consumePendingDeepLink()는 pending 키를 backStack에
append하므로 MainHubKey같은 루트 키를 가진 cold-start 딥링크에서 backStack에 중복된 루트가 생깁니다;
SplashEntryBuilder.kt의 흐름에서 MainHubKey로 네비게이트할 때는 pending deep link를 append하지 않고
replaceAll로 루트 교체해야 합니다. 수정 방법: navigator.replaceAll(MainHubKey()) 후 바로
navigator.consumePendingDeepLink() 하지 말고 대신 Navigator의 pending 값을 확인해 pending이
MainHubKey 타입(루트 역할)이면 navigator.replaceAll(pendingKey)로 교체하고 아니면 기존처럼
navigator.consumePendingDeepLink()로 append하도록 분기 처리(참고 심볼:
navigator.replaceAll(), navigator.consumePendingDeepLink(), MainHubKey,
OnboardingKey 및 OnboardingEntryBuilder의 동일 패턴도 동일하게 적용).

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant