Skip to content

[#247] 목적지 부근 도착 시 안내 종료#248

Merged
hyuns66 merged 16 commits intodevelopfrom
feat/stop-navigation-near-destination#247
Nov 30, 2025
Merged

[#247] 목적지 부근 도착 시 안내 종료#248
hyuns66 merged 16 commits intodevelopfrom
feat/stop-navigation-near-destination#247

Conversation

@jieeeunnn
Copy link
Member

@jieeeunnn jieeeunnn commented Nov 25, 2025

#️⃣연관된 이슈

📝작업 내용

  • 목적지 부근 도착 시 안내 종료

PR 발행 전 체크 리스트

  • 발행자 확인
  • 프로젝트 설정 확인
  • 라벨 확인
  • 코드 린트 확인

스크린샷 (선택)

💬리뷰 요구사항(선택)

Summary by CodeRabbit

  • 새로운 기능

    • 목적지 도착 자동 감지 서비스 추가(포그라운드 위치 추적, 근접 알림·진동·영구 알림·앱 복귀 인텐트·도착 브로드캐스트)
    • 홈 화면에서 도착 모니터링 서비스 자동 시작 및 도착 브로드캐스트 수신 처리
  • 리팩터

    • 캐릭터 말풍선 흐름 단순화 및 isMapReady 기반 연동, 말풍선 렌더링 일부 비활성화 및 API 축소
    • 지도 컴포저블에 지도 준비 콜백 추가
  • 문서/리소스

    • 알림/다이얼로그 문자열 추가·수정·불필요 문자열 제거
  • 테스트

    • 말풍선 UI 동작 검증용 컴포저블 테스트 추가
  • 잡일

    • 앱 버전 메타데이터 업데이트 (versionCode/versionName)

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

@jieeeunnn jieeeunnn linked an issue Nov 25, 2025 that may be closed by this pull request
3 tasks
@coderabbitai
Copy link

coderabbitai bot commented Nov 25, 2025

Walkthrough

도착 감지를 수행하는 새로운 포그라운드 서비스 ArrivalMonitorService 추가, HomeScreen에서 서비스 시작 연계, MainActivity에 도착 브로드캐스트 수신 등록. 또한 맵 준비·캐릭터 스피치 관련 상태·API 및 UI 컴포저블 시그니처가 변경되고 문자열·버전 등의 메타가 조정되었습니다.

Changes

Cohort / File(s) Summary
도착 감지 포그라운드 서비스
app/src/main/java/com/depromeet/team6/data/background/ArrivalMonitorService.kt
신규 Foreground Service 추가: FusedLocationProviderClient로 LOW_POWER 위치 업데이트 구독, 목적지까지 거리 계산(30m), 도착 시 진동·포그라운드 알림 생성·ACTION_ARRIVAL 브로드캐스트·stopSelf, 권한 확인 및 자원 정리. 관련 상수(채널/액션/엑스트라/ID) 공개.
서비스 시작 연계 (UI)
app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeScreen.kt, .../HomeRoute.kt
isAlarmRegistered일 때 목적지 좌표를 Intent extras로 넣어 ContextCompat.startForegroundService로 ArrivalMonitorService 시작. HomeScreenisMapReadyCallbackrequestCharacterSpeech 파라미터 추가/전달.
도착 이벤트 수신 (Activity)
app/src/main/java/com/depromeet/team6/presentation/ui/main/MainActivity.kt
DisposableEffect 기반 BroadcastReceiver 등록/해제 추가. ACTION_ARRIVAL 수신 시 기존 dialog controller로 1버튼 확인 다이얼로그 표시.
맵 준비 콜백 API 변경
app/src/main/java/com/depromeet/team6/presentation/ui/home/component/MapView.kt, .../AfterRegisterMap.kt, .../HomeScreen.kt
TMapViewCompose / AfterRegisterMap / HomeScreen에 isMapReadyCallback: () -> Unit 파라미터 추가 및 맵 준비 시 콜백 호출로 상위에 맵 준비 상태 전달.
캐릭터 스피치 상태·요청 추가
app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeContract.kt, .../HomeViewModel.kt
HomeUiState에 isMapReady, characterMessages: SpeechRequest 추가; SpeechRequest 데이터 클래스·HomeEvent.RequestCharacterSpeech 추가. ViewModel은 prefs에서 알람/코스 로드하고 taxi cost 등으로 초기 스피치 생성, RequestCharacterSpeech 처리 로직 추가.
새로운 캐릭터/버블 컴포저블
app/src/main/java/com/depromeet/team6/presentation/ui/common/speechbubble/AtchaSpeechBubbleTest.kt
BubbleMessage, AtchaSpeechCharacter, BubbleItem 추가: 메시지 큐·타이밍 관리, 순차 표시/제거, Lottie 연동, 탭 스로틀링 등.
AtchaSpeechBubble API 단순화
app/src/main/java/com/depromeet/team6/presentation/ui/common/speechbubble/AtchaSpeechBubble.kt
기존 prefix/emphasis/suffix 기반 API를 하나의 message: String 시그니처로 변경. 호출부 업데이트.
캐릭터 스피치 렌더링 비활성화/단순화
app/src/main/java/com/depromeet/team6/presentation/ui/home/component/CharacterLottieSpeechBubble.kt, .../CharacterSpeechBubble.kt
AtchaSpeechBubble 관련 렌더링 제거(주석 처리) 및 Spacer로 대체 — 스피치 표시 로직 비활성화.
AfterRegisterSheet 시그니처/로직 단순화
app/src/main/java/com/depromeet/team6/presentation/ui/home/component/AfterRegisterSheet.kt
public 파라미터 isConfirmed 제거, 관련 분기·조건부 UI 단순화(주석/삭제).
UI 상호작용 미세 변경
app/src/main/java/com/depromeet/team6/presentation/ui/home/component/CourseTextButton.kt
Row modifier 순서 변경: .noRippleClickable.padding 적용 순서 교체(히트테스트/레이아웃 적용 순서 영향 가능).
문자열 리소스 변경
app/src/main/res/values/strings.xml
taxi cost 관련 문자열 정리·통합, home_final_departure_time_text 값 변경, 알림·다이얼로그 관련 문자열 5개 추가, 몇몇 기존 문자열 삭제/주석 처리.
버전/문서/빌드 메타 변경
app/build.gradle.kts, GEMINI.md
android.defaultConfig.versionCode 20→21, versionName "1.3.14"→"1.3.2" 업데이트. GEMINI.md의 ktlint 표기 소폭 수정.

Sequence Diagram

sequenceDiagram
    participant User
    participant HomeScreen
    participant ArrivalMonitorService
    participant LocationProvider
    participant MainActivity

    User->>HomeScreen: 알람(목적지) 등록
    HomeScreen->>ArrivalMonitorService: startForegroundService(Intent{destLat,destLng})
    Note over ArrivalMonitorService: 포그라운드 알림 생성 및 startForeground()

    ArrivalMonitorService->>LocationProvider: requestLocationUpdates(LOW_POWER)
    loop 위치 업데이트 수신
        LocationProvider->>ArrivalMonitorService: onLocationResult(location)
        ArrivalMonitorService->>ArrivalMonitorService: 거리 계산 (현재 ↔ 목적지)
        alt within 30m
            ArrivalMonitorService->>ArrivalMonitorService: vibrate(), notify()
            ArrivalMonitorService->>MainActivity: sendBroadcast(ACTION_ARRIVAL)
            ArrivalMonitorService->>ArrivalMonitorService: stopSelf() / removeLocationUpdates
            MainActivity->>User: 도착 다이얼로그 표시
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • 집중 검토 권장 항목:
    • 권한·보안: 포그라운드/백그라운드 위치 권한 경계, 권한 거부 시 서비스 동작/종료 보장(무한 대기 방지).
    • 포그라운드 서비스 생명주기 및 동시성: startForeground 시점, stopSelf 호출 후 위치 업데이트 해제와 코루틴 취소 보장, 다중 인텐트/중복 서비스 시작 처리.
    • 전력·성능: LOW_POWER 요청 주기/정밀도와 진동·노티 트리거 타이밍이 배터리에 미치는 영향 확인.
    • 브로드캐스트 신뢰성: ACTION_ARRIVAL의 범위(implicit/explicit), 중복 수신 및 수신기 등록/해제 타이밍(DisposableEffect 범위) 검증.
    • 맵 준비 콜백 호출 타이밍: isMapReadyCallback이 여러 번 호출될 가능성(의도대로 한 번만 호출 필요 여부)과 레이스 컨디션.
    • ViewModel 초기화와 스피치 흐름: prefs 로드·비동기 작업과 맵 준비/UI 렌더 타이밍 간 경쟁 상태 검사.
    • 리소스·버전: versionName 하향처럼 보이는 변경 의도 확인(릴리스 메타데이터 일관성).

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.63% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 주요 변경 사항을 명확하게 설명합니다. 목적지 부근 도착 시 안내 종료라는 핵심 기능을 직접 반영하며, 구현된 ArrivalMonitorService, 포그라운드 서비스 시작, 도착 감지, 알림 및 브로드캐스트 수신 로직과 일치합니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/stop-navigation-near-destination#247

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cf69b37 and 4fa271e.

📒 Files selected for processing (1)
  • app/src/main/java/com/depromeet/team6/data/background/ArrivalMonitorService.kt (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.kt

📄 CodeRabbit inference engine (GEMINI.md)

**/*.kt: Use 100% Kotlin for all code implementation
Implement Clean Architecture with MVI (Model-View-Intent) pattern using single-direction data flow (UDF) where UI events flow through ViewModel to the data layer, and processed State results flow back to UI
Use Coroutines and Flow for asynchronous operations and reactive data streams
Use Hilt for dependency injection to manage dependencies between layers and reduce coupling between components
Apply Proguard and R8 code obfuscation and optimization for release builds to ensure security and performance
Maintain consistent code style using Ktlint plugin for Kotlin code formatting

Files:

  • app/src/main/java/com/depromeet/team6/data/background/ArrivalMonitorService.kt
**/*Service.kt

📄 CodeRabbit inference engine (GEMINI.md)

Use BackgroundService and BroadcastReceiver for background processing tasks such as push notifications and alarms

Files:

  • app/src/main/java/com/depromeet/team6/data/background/ArrivalMonitorService.kt
🧠 Learnings (1)
📚 Learning: 2025-11-30T06:06:10.920Z
Learnt from: CR
Repo: depromeet/16th-team6-Android PR: 0
File: GEMINI.md:0-0
Timestamp: 2025-11-30T06:06:10.920Z
Learning: Applies to **/*Service.kt : Use BackgroundService and BroadcastReceiver for background processing tasks such as push notifications and alarms

Applied to files:

  • app/src/main/java/com/depromeet/team6/data/background/ArrivalMonitorService.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: CI

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ce2a876 and b517400.

📒 Files selected for processing (4)
  • app/src/main/java/com/depromeet/team6/data/background/ArrivalMonitorService.kt (1 hunks)
  • app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeScreen.kt (3 hunks)
  • app/src/main/java/com/depromeet/team6/presentation/ui/main/MainActivity.kt (5 hunks)
  • app/src/main/res/values/strings.xml (1 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-10-18T12:10:51.782Z
Learnt from: CR
Repo: depromeet/16th-team6-Android PR: 0
File: GEMINI.md:0-0
Timestamp: 2025-10-18T12:10:51.782Z
Learning: Use Coroutines and Flow for asynchronous work and reactive streams

Applied to files:

  • app/src/main/java/com/depromeet/team6/presentation/ui/main/MainActivity.kt
  • app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeScreen.kt
📚 Learning: 2025-10-18T12:10:51.782Z
Learnt from: CR
Repo: depromeet/16th-team6-Android PR: 0
File: GEMINI.md:0-0
Timestamp: 2025-10-18T12:10:51.782Z
Learning: Separate layers: UI with Jetpack Compose subscribing to ViewModel state; Domain as pure Kotlin with UseCases; Data using repositories abstracting sources

Applied to files:

  • app/src/main/java/com/depromeet/team6/presentation/ui/main/MainActivity.kt
  • app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeScreen.kt
📚 Learning: 2025-10-18T12:10:51.782Z
Learnt from: CR
Repo: depromeet/16th-team6-Android PR: 0
File: GEMINI.md:0-0
Timestamp: 2025-10-18T12:10:51.782Z
Learning: Use AAC ViewModel and Navigation for lifecycle-aware state and app navigation

Applied to files:

  • app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeScreen.kt
📚 Learning: 2025-10-18T12:10:51.782Z
Learnt from: CR
Repo: depromeet/16th-team6-Android PR: 0
File: GEMINI.md:0-0
Timestamp: 2025-10-18T12:10:51.782Z
Learning: Adopt Clean Architecture with MVI and unidirectional data flow (Intent -> ViewModel -> State -> UI)

Applied to files:

  • app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeScreen.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: CI
🔇 Additional comments (2)
app/src/main/res/values/strings.xml (1)

254-260: LGTM!

도착 모니터링 기능을 위한 문자열 리소스가 올바르게 추가되었습니다.

app/src/main/java/com/depromeet/team6/presentation/ui/main/MainActivity.kt (1)

227-254: BroadcastReceiver 구현 양호

DisposableEffect를 사용한 생명주기 인식 등록/해제와 RECEIVER_NOT_EXPORTED 플래그 사용이 적절합니다. 내부 앱 통신에 올바른 보안 패턴을 따르고 있습니다.

private var destinationLatLng: LatLng? = null
private var hasArrived = false
private var vibrator: Vibrator? = null
private val timerScope = CoroutineScope(Dispatchers.Default)
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

사용되지 않는 timerScope 제거 필요

timerScope가 선언되고 onDestroy()에서 취소되지만, 어디에서도 코루틴을 실행하지 않습니다. 사용하지 않는 코드는 제거하세요.

-    private val timerScope = CoroutineScope(Dispatchers.Default)

그리고 onDestroy()에서도 제거:

 override fun onDestroy() {
     fusedLocationClient?.removeLocationUpdates(locationCallback)
-    timerScope.cancel()
     super.onDestroy()
 }

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

🤖 Prompt for AI Agents
In
app/src/main/java/com/depromeet/team6/data/background/ArrivalMonitorService.kt
around line 37, the private val timerScope = CoroutineScope(Dispatchers.Default)
is declared but never used; remove the unused timerScope declaration and also
remove its cancellation from onDestroy() so there is no dangling unused
coroutine scope; ensure there are no other references to timerScope before
deleting both the field and the onDestroy cancellation call.

Comment on lines +86 to +93
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val lat = intent?.getDoubleExtra(EXTRA_DEST_LAT, 0.0) ?: 0.0
val lng = intent?.getDoubleExtra(EXTRA_DEST_LNG, 0.0) ?: 0.0
destinationLatLng = LatLng(lat, lng)

startLocationTracking()
return START_STICKY
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

START_STICKY 사용 시 데이터 손실 위험

서비스가 시스템에 의해 종료 후 재시작되면 onStartCommand가 null intent로 호출됩니다. 이 경우 destinationLatLng(0.0, 0.0)으로 설정되어 대서양 한가운데를 목적지로 모니터링하게 됩니다.

 override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
     val lat = intent?.getDoubleExtra(EXTRA_DEST_LAT, 0.0) ?: 0.0
     val lng = intent?.getDoubleExtra(EXTRA_DEST_LNG, 0.0) ?: 0.0
+    
+    // 유효하지 않은 좌표인 경우 서비스 중지
+    if (lat == 0.0 && lng == 0.0) {
+        stopSelf()
+        return START_NOT_STICKY
+    }
+    
     destinationLatLng = LatLng(lat, lng)

     startLocationTracking()
-    return START_STICKY
+    return START_NOT_STICKY
 }
📝 Committable suggestion

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

Suggested change
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val lat = intent?.getDoubleExtra(EXTRA_DEST_LAT, 0.0) ?: 0.0
val lng = intent?.getDoubleExtra(EXTRA_DEST_LNG, 0.0) ?: 0.0
destinationLatLng = LatLng(lat, lng)
startLocationTracking()
return START_STICKY
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val lat = intent?.getDoubleExtra(EXTRA_DEST_LAT, 0.0) ?: 0.0
val lng = intent?.getDoubleExtra(EXTRA_DEST_LNG, 0.0) ?: 0.0
// 유효하지 않은 좌표인 경우 서비스 중지
if (lat == 0.0 && lng == 0.0) {
stopSelf()
return START_NOT_STICKY
}
destinationLatLng = LatLng(lat, lng)
startLocationTracking()
return START_NOT_STICKY
}

Comment on lines +95 to +105
private fun startLocationTracking() {
if (checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED && checkSelfPermission(
this,
Manifest.permission.ACCESS_COARSE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
return
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

권한 거부 시 서비스가 계속 실행됨

위치 권한이 없으면 startLocationTracking()이 조용히 반환되지만, 서비스는 포그라운드에서 계속 실행됩니다. 리소스 낭비이며 사용자에게 혼란을 줄 수 있습니다.

     private fun startLocationTracking() {
         if (checkSelfPermission(
                 this,
                 Manifest.permission.ACCESS_FINE_LOCATION
             ) != PackageManager.PERMISSION_GRANTED && checkSelfPermission(
                     this,
                     Manifest.permission.ACCESS_COARSE_LOCATION
                 ) != PackageManager.PERMISSION_GRANTED
         ) {
+            stopSelf()
             return
         }
📝 Committable suggestion

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

Suggested change
private fun startLocationTracking() {
if (checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED && checkSelfPermission(
this,
Manifest.permission.ACCESS_COARSE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
return
}
private fun startLocationTracking() {
if (checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED && checkSelfPermission(
this,
Manifest.permission.ACCESS_COARSE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
stopSelf()
return
}
🤖 Prompt for AI Agents
In
app/src/main/java/com/depromeet/team6/data/background/ArrivalMonitorService.kt
around lines 95 to 105, startLocationTracking() currently returns silently when
location permissions are missing which leaves the foreground service running;
update the method to explicitly stop the service when permissions are not
granted by calling stopForeground(true) and stopSelf(), and (optionally) post a
notification or broadcast to prompt the UI to request permissions before
restarting the service; ensure these cleanup calls run on the service main
thread and remove any further location setup when permissions are absent.

Comment on lines +148 to +158
val notification = NotificationCompat.Builder(this, "ARRIVAL_MONITOR_CHANNEL")
.setSmallIcon(R.drawable.ic_atcha_logo)
.setContentTitle(
getString(R.string.notification_arrival_guide_finish_title)
)
.setContentText(
getString(R.string.notification_arrival_near_destination_text)
)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.build()
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

알림 채널 ID 불일치 - 알림이 표시되지 않음

onArrived()에서 알림 생성 시 하드코딩된 채널 ID "ARRIVAL_MONITOR_CHANNEL"을 사용하고 있지만, 실제 생성된 채널 ID는 ATCHA_ARRIVAL_MONITOR_CHANNEL_ID ("Arrival_Monitor_Channel")입니다. Android 8.0 이상에서 알림이 표시되지 않습니다.

-        val notification = NotificationCompat.Builder(this, "ARRIVAL_MONITOR_CHANNEL")
+        val notification = NotificationCompat.Builder(this, ATCHA_ARRIVAL_MONITOR_CHANNEL_ID)
             .setSmallIcon(R.drawable.ic_atcha_logo)
📝 Committable suggestion

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

Suggested change
val notification = NotificationCompat.Builder(this, "ARRIVAL_MONITOR_CHANNEL")
.setSmallIcon(R.drawable.ic_atcha_logo)
.setContentTitle(
getString(R.string.notification_arrival_guide_finish_title)
)
.setContentText(
getString(R.string.notification_arrival_near_destination_text)
)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.build()
val notification = NotificationCompat.Builder(this, ATCHA_ARRIVAL_MONITOR_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_atcha_logo)
.setContentTitle(
getString(R.string.notification_arrival_guide_finish_title)
)
.setContentText(
getString(R.string.notification_arrival_near_destination_text)
)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.build()
🤖 Prompt for AI Agents
In
app/src/main/java/com/depromeet/team6/data/background/ArrivalMonitorService.kt
around lines 148 to 158, the NotificationCompat.Builder uses the hardcoded
channel ID "ARRIVAL_MONITOR_CHANNEL" whereas the actual created channel ID
constant is ATCHA_ARRIVAL_MONITOR_CHANNEL_ID ("Arrival_Monitor_Channel");
replace the hardcoded string with the correct channel ID constant
(ATCHA_ARRIVAL_MONITOR_CHANNEL_ID) so the notification is posted to the existing
channel and will appear on Android 8.0+.

Comment on lines 621 to +633
if (homeUiState.isAlarmRegistered) {
val context = LocalContext.current
val destination = homeUiState.destinationPoint

Intent(context, ArrivalMonitorService::class.java).apply {
putExtra(ArrivalMonitorService.EXTRA_DEST_LAT, destination.lat)
putExtra(ArrivalMonitorService.EXTRA_DEST_LNG, destination.lon)
}.also {
ContextCompat.startForegroundService(context, it)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Compose 부작용(Side Effect) 규칙 위반 - 서비스가 매 리컴포지션마다 시작됨

포그라운드 서비스를 Composable 함수 본문에서 직접 시작하고 있습니다. isAlarmRegistered가 true인 동안 리컴포지션이 발생할 때마다 startForegroundService가 호출됩니다. 이는 예상치 못한 동작과 잠재적인 성능 문제를 야기할 수 있습니다.

LaunchedEffect로 감싸서 조건 변경 시에만 실행되도록 수정하세요:

         if (homeUiState.isAlarmRegistered) {
             val context = LocalContext.current
             val destination = homeUiState.destinationPoint

-            Intent(context, ArrivalMonitorService::class.java).apply {
-                putExtra(ArrivalMonitorService.EXTRA_DEST_LAT, destination.lat)
-                putExtra(ArrivalMonitorService.EXTRA_DEST_LNG, destination.lon)
-            }.also {
-                ContextCompat.startForegroundService(context, it)
-            }
+            LaunchedEffect(destination.lat, destination.lon) {
+                Intent(context, ArrivalMonitorService::class.java).apply {
+                    putExtra(ArrivalMonitorService.EXTRA_DEST_LAT, destination.lat)
+                    putExtra(ArrivalMonitorService.EXTRA_DEST_LNG, destination.lon)
+                }.also {
+                    ContextCompat.startForegroundService(context, it)
+                }
+            }

             AfterRegisterMap(
🤖 Prompt for AI Agents
In app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeScreen.kt
around lines 624-633, the foreground service is started directly in the
Composable body causing startForegroundService to run on every recomposition
while isAlarmRegistered is true; move this logic into a LaunchedEffect keyed on
isAlarmRegistered (and destination if it can change) so the Intent creation and
ContextCompat.startForegroundService call only execute when isAlarmRegistered
becomes true (and/or when destination changes) rather than on every
recomposition.

@jieeeunnn jieeeunnn self-assigned this Nov 26, 2025
@jieeeunnn jieeeunnn added the feat Feature label Nov 26, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeScreen.kt (1)

646-655: Composable 본문에서 ForegroundService 직접 시작 → 리컴포지션마다 반복 호출 위험

homeUiState.isAlarmRegisteredtrue인 동안 이 블록이 컴포지션에 남아 있기 때문에, 리컴포지션이 발생할 때마다 startForegroundService 가 다시 호출될 수 있습니다. 이는:

  • 서비스 onStartCommand 가 불필요하게 여러 번 호출될 수 있고
  • 위치 갱신/알림 로직이 중복 실행되며
  • 배터리·자원 사용량 증가 및 예측하기 어려운 동작을 유발할 수 있습니다.

MVI/UDF 및 Compose side-effect 규칙 관점에서도, 이런 작업은 Composable 본문이 아니라 LaunchedEffect 같은 side-effect 블록으로 분리하는 것이 맞습니다. 과거 리뷰에서도 동일 지적이 있었는데 아직 반영이 안 된 상태입니다.

isAlarmRegistered == true 상태로 전환될 때(또는 목적지 좌표 변경 시)에만 서비스가 1회 시작되도록 아래처럼 LaunchedEffect 로 감싸는 방향을 권장합니다:

-        if (homeUiState.isAlarmRegistered) {
-            val context = LocalContext.current
-            val destination = homeUiState.destinationPoint
-
-            Intent(context, ArrivalMonitorService::class.java).apply {
-                putExtra(ArrivalMonitorService.EXTRA_DEST_LAT, destination.lat)
-                putExtra(ArrivalMonitorService.EXTRA_DEST_LNG, destination.lon)
-            }.also {
-                ContextCompat.startForegroundService(context, it)
-            }
-
+        if (homeUiState.isAlarmRegistered) {
+            val context = LocalContext.current
+            val destination = homeUiState.destinationPoint
+
+            LaunchedEffect(destination.lat, destination.lon) {
+                Intent(context, ArrivalMonitorService::class.java).apply {
+                    putExtra(ArrivalMonitorService.EXTRA_DEST_LAT, destination.lat)
+                    putExtra(ArrivalMonitorService.EXTRA_DEST_LNG, destination.lon)
+                }.also {
+                    ContextCompat.startForegroundService(context, it)
+                }
+            }
+
             AfterRegisterMap(

이렇게 하면 상태 변화(목적지/알람 등록)로 인해 컴포지션 트리에 이 블록이 진입할 때만 서비스가 시작되어, 불필요한 반복 호출을 막을 수 있습니다.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b517400 and da95dc1.

📒 Files selected for processing (2)
  • app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeScreen.kt (3 hunks)
  • app/src/main/java/com/depromeet/team6/presentation/ui/main/MainActivity.kt (4 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.kt

📄 CodeRabbit inference engine (GEMINI.md)

**/*.kt: Use 100% Kotlin for all code implementation
Implement Clean Architecture with MVI (Model-View-Intent) pattern using single-direction data flow (UDF) where UI events flow through ViewModel to the data layer, and processed State results flow back to UI
Use Coroutines and Flow for asynchronous operations and reactive data streams
Use Hilt for dependency injection to manage dependencies between layers and reduce coupling between components
Apply Proguard and R8 code obfuscation and optimization for release builds to ensure security and performance
Maintain consistent code style using Ktlint plugin for Kotlin code formatting

Files:

  • app/src/main/java/com/depromeet/team6/presentation/ui/main/MainActivity.kt
  • app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeScreen.kt
🧠 Learnings (6)
📚 Learning: 2025-11-30T06:06:10.920Z
Learnt from: CR
Repo: depromeet/16th-team6-Android PR: 0
File: GEMINI.md:0-0
Timestamp: 2025-11-30T06:06:10.920Z
Learning: Applies to **/*Service.kt : Use BackgroundService and BroadcastReceiver for background processing tasks such as push notifications and alarms

Applied to files:

  • app/src/main/java/com/depromeet/team6/presentation/ui/main/MainActivity.kt
  • app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeScreen.kt
📚 Learning: 2025-11-30T06:06:10.920Z
Learnt from: CR
Repo: depromeet/16th-team6-Android PR: 0
File: GEMINI.md:0-0
Timestamp: 2025-11-30T06:06:10.920Z
Learning: Applies to **/*Compose.kt : Use Jetpack Compose with declarative UI to construct UI components and handle user interactions for State subscription from ViewModel

Applied to files:

  • app/src/main/java/com/depromeet/team6/presentation/ui/main/MainActivity.kt
  • app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeScreen.kt
📚 Learning: 2025-11-30T06:06:10.920Z
Learnt from: CR
Repo: depromeet/16th-team6-Android PR: 0
File: GEMINI.md:0-0
Timestamp: 2025-11-30T06:06:10.920Z
Learning: Applies to **/*.kt : Use Hilt for dependency injection to manage dependencies between layers and reduce coupling between components

Applied to files:

  • app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeScreen.kt
📚 Learning: 2025-11-30T06:06:10.920Z
Learnt from: CR
Repo: depromeet/16th-team6-Android PR: 0
File: GEMINI.md:0-0
Timestamp: 2025-11-30T06:06:10.920Z
Learning: Applies to **/*ViewModel.kt : Use AAC ViewModel for state management and to handle the single-direction data flow (UDF) pattern between UI Layer and Domain Layer

Applied to files:

  • app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeScreen.kt
📚 Learning: 2025-11-30T06:06:10.920Z
Learnt from: CR
Repo: depromeet/16th-team6-Android PR: 0
File: GEMINI.md:0-0
Timestamp: 2025-11-30T06:06:10.920Z
Learning: Applies to **/*.kt : Implement Clean Architecture with MVI (Model-View-Intent) pattern using single-direction data flow (UDF) where UI events flow through ViewModel to the data layer, and processed State results flow back to UI

Applied to files:

  • app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeScreen.kt
📚 Learning: 2025-11-30T06:06:10.920Z
Learnt from: CR
Repo: depromeet/16th-team6-Android PR: 0
File: GEMINI.md:0-0
Timestamp: 2025-11-30T06:06:10.920Z
Learning: Applies to **/*.kt : Use Coroutines and Flow for asynchronous operations and reactive data streams

Applied to files:

  • app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeScreen.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: CI

coderabbitai[bot]
coderabbitai bot previously requested changes Nov 30, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
app/src/main/java/com/depromeet/team6/presentation/ui/home/component/AfterRegisterSheet.kt (2)

38-62: 사용되지 않는 파라미터 및 주석 처리된 코드 정리 필요

onRefreshClick, onTimerFinished, onHomeExpectDepartureTimeClick 파라미터가 함수 시그니처에 남아있지만 실제로 사용되지 않습니다. 또한 timerFinish 파라미터도 대부분의 관련 로직이 주석 처리되어 제한적으로만 사용됩니다.

주석 처리된 코드를 프로덕션에 남겨두면 코드 가독성이 저하되고 유지보수가 어려워집니다. 이 코드들이 더 이상 필요 없다면 완전히 제거하고, 필요하다면 주석을 해제하여 활성화하세요.

 fun AfterRegisterSheet(
     timerFinish: Boolean,
-//    isConfirmed: Boolean,
     afterUserDeparted: Boolean,
     transportType: TransportType,
     transportationNumber: Int,
     transportationName: String,
     timeToLeave: String,
     boardingTime: String,
     homeArrivedTime: String,
     startLocation: String,
     destination: String,
     deleteAlarmConfirmed: () -> Unit = {},
     dismissDialog: () -> Unit = {},
     busStationLeft: Int,
     onCourseTextClick: () -> Unit,
     onFinishClick: () -> Unit,
     onCourseDetailClick: (String) -> Unit,
-    onRefreshClick: () -> Unit,
-    onTimerFinished: () -> Unit = {},
     onIconClick: () -> Unit = {},
     onHomeDepartureTimeClick: () -> Unit = {},
-    onHomeExpectDepartureTimeClick: () -> Unit = {},
     modifier: Modifier = Modifier
 )

201-209: 조건 분기 로직 단순화 가능

timerFinishafterUserDeparted 조합에 따른 분기가 있지만, 주석 처리된 isConfirmed 관련 분기가 제거되면서 로직이 더 단순해질 수 있습니다. 현재 timerFinishfalse인 경우와 true인 경우 모두 afterUserDepartedtrue면 다른 amplitude 이벤트만 전송하는 구조입니다.

app/src/main/java/com/depromeet/team6/presentation/ui/home/component/CharacterSpeechBubble.kt (1)

17-46: 미사용 파라미터와 비활성화된 로직 정리 필요

prefixText, emphasisText, suffixText, showSpeechBubble 파라미터가 모두 사용되지 않습니다. 현재 showSpeechBubbletrue일 때 단순히 Spacer만 렌더링하며, 이는 의도한 동작이 아닌 것으로 보입니다.

이 컴포넌트가 더 이상 필요 없다면 완전히 제거하고, 리팩토링 중이라면 주석 처리 대신 명확한 TODO 또는 완전한 구현으로 대체하세요.

두 가지 옵션 중 선택:

옵션 1: 컴포넌트 단순화 (말풍선 불필요 시)

 @Composable
 fun CharacterSpeechBubble(
-    prefixText: String,
     modifier: Modifier = Modifier,
-    emphasisText: String? = null,
-    suffixText: String? = null,
     onClick: () -> Unit = {},
-    showSpeechBubble: Boolean = true
 ) {
     Column(
         modifier = modifier.noRippleClickable { onClick() }
     ) {
-        if (showSpeechBubble) {
-            Spacer(modifier = Modifier.height(2.dp))
-        }
-
         Image(
             imageVector = ImageVector.vectorResource(R.drawable.ic_all_atcha_character),
             contentDescription = stringResource(R.string.all_atcha_character)
         )
     }
 }

옵션 2: 말풍선 로직 복원 또는 새 API로 마이그레이션

app/src/main/java/com/depromeet/team6/presentation/ui/home/component/CharacterLottieSpeechBubble.kt (1)

44-91: 사용되지 않는 상태 및 불필요한 코루틴 실행

isTopSpeechBubbleVisible, isBottomSpeechBubbleVisible 상태와 관련된 모든 LaunchedEffect 로직(Lines 56-91)이 여전히 실행되지만, 해당 상태를 사용하는 UI 코드는 모두 주석 처리되었습니다. 이로 인해:

  1. 불필요한 코루틴 delay 실행 (최대 4000ms+)
  2. 사용되지 않는 상태 리컴포지션
  3. 불필요한 메모리 사용

UI 코드가 비활성화된 상태라면 관련 상태와 LaunchedEffect도 함께 제거하거나 주석 처리하세요:

-    var isTopSpeechBubbleVisible by remember { mutableStateOf(false) }
-    var isBottomSpeechBubbleVisible by remember { mutableStateOf(false) }
-
-    var speechBubbleTrigger by remember { mutableStateOf(0) }
-
-    val hasAnyText = !prefixText.isBlank() ||
-        !emphasisText.isNullOrBlank() ||
-        !suffixText.isNullOrBlank() ||
-        !topPrefixText.isNullOrBlank() ||
-        !topEmphasisText.isNullOrBlank() ||
-        !topSuffixText.isNullOrBlank()
-
-    LaunchedEffect(externalTrigger) { ... }
-    LaunchedEffect(speechBubbleTrigger, showSpeechBubble, hasAnyText) { ... }
-    LaunchedEffect(Unit) { ... }
app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeViewModel.kt (1)

74-105: Race condition in init block between async prefs loading and departure state check, plus unused initial speech calculation

The init block has two significant issues:

  1. Async execution order race condition (lines 81-82)

    loadAlarmAndCourseInfoFromPrefs() is not a suspend function—it internally launches a new viewModelScope.launch { ... } and returns immediately. This means loadUserDepartureState() executes right after, likely before the LoadFirstTransportation event (set at line 562) is processed into state. When loadUserDepartureState() checks at line 482:

    if (userDeparture && currentState.firtTransportTation == TransportType.BUS) {
        getBusArrival()
    }

    currentState.firtTransportTation is still at its default value (TransportType.WALK), so getBusArrival() may never be called even when the first transport is actually BUS.

    Make loadAlarmAndCourseInfoFromPrefs() a suspend function to ensure prefs/course data load sequentially before checking departure state:

  • fun loadAlarmAndCourseInfoFromPrefs() {
  •    viewModelScope.launch {
    
  • suspend fun loadAlarmAndCourseInfoFromPrefs() {
    // ...
  •    }
    
    }
    
    Then call it as a regular await in init:
    
    ```diff
              loadAlarmAndCourseInfoFromPrefs()
              val initialSpeech = if (loadUserDepartureState()) { ... }
    
  1. Initial speech message is computed but never applied to state (lines 82-96)

    initialSpeech is calculated based on departure state and taxi cost, but the setState block (lines 98-103) only copies currentLocation and loadState. The characterMessages field remains at its default empty SpeechRequest(emptyList()), so the initial speech bubble never appears.

    Add characterMessages = initialSpeech to the setState call:

              setState {
                  copy(
                      currentLocation = currentLocation,
  •                loadState = LoadState.Success
    
  •                loadState = LoadState.Success,
    
  •                characterMessages = initialSpeech
               )
           }
    
    
    
♻️ Duplicate comments (1)
app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeScreen.kt (1)

658-667: Compose 부작용(Side Effect) 규칙 위반 - 서비스가 매 리컴포지션마다 재시작됨

이 코드는 Composable 본문에서 직접 startForegroundService를 호출하고 있습니다. isAlarmRegistered가 true인 동안 UI가 리컴포지션될 때마다 서비스가 반복적으로 시작됩니다. 이는 불필요한 리소스 소비와 예측 불가능한 동작을 야기합니다.

LaunchedEffect를 사용하여 조건 변경 시에만 실행되도록 수정해야 합니다:

         if (homeUiState.isAlarmRegistered) {
             val context = LocalContext.current
             val destination = homeUiState.destinationPoint

+            LaunchedEffect(destination.lat, destination.lon) {
-            Intent(context, ArrivalMonitorService::class.java).apply {
-                putExtra(ArrivalMonitorService.EXTRA_DEST_LAT, destination.lat)
-                putExtra(ArrivalMonitorService.EXTRA_DEST_LNG, destination.lon)
-            }.also {
-                ContextCompat.startForegroundService(context, it)
-            }
+                Intent(context, ArrivalMonitorService::class.java).apply {
+                    putExtra(ArrivalMonitorService.EXTRA_DEST_LAT, destination.lat)
+                    putExtra(ArrivalMonitorService.EXTRA_DEST_LNG, destination.lon)
+                }.also {
+                    ContextCompat.startForegroundService(context, it)
+                }
+            }

             AfterRegisterMap(
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between da95dc1 and cf69b37.

⛔ Files ignored due to path filters (1)
  • app/release/.DS_Store is excluded by !**/.DS_Store
📒 Files selected for processing (15)
  • GEMINI.md (1 hunks)
  • app/build.gradle.kts (1 hunks)
  • app/src/main/java/com/depromeet/team6/presentation/ui/common/speechbubble/AtchaSpeechBubble.kt (3 hunks)
  • app/src/main/java/com/depromeet/team6/presentation/ui/common/speechbubble/AtchaSpeechBubbleTest.kt (1 hunks)
  • app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeContract.kt (3 hunks)
  • app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeScreen.kt (20 hunks)
  • app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeViewModel.kt (6 hunks)
  • app/src/main/java/com/depromeet/team6/presentation/ui/home/component/AfterRegisterMap.kt (3 hunks)
  • app/src/main/java/com/depromeet/team6/presentation/ui/home/component/AfterRegisterSheet.kt (6 hunks)
  • app/src/main/java/com/depromeet/team6/presentation/ui/home/component/CharacterLottieSpeechBubble.kt (1 hunks)
  • app/src/main/java/com/depromeet/team6/presentation/ui/home/component/CharacterSpeechBubble.kt (1 hunks)
  • app/src/main/java/com/depromeet/team6/presentation/ui/home/component/CourseTextButton.kt (1 hunks)
  • app/src/main/java/com/depromeet/team6/presentation/ui/home/component/MapView.kt (2 hunks)
  • app/src/main/java/com/depromeet/team6/presentation/ui/main/MainActivity.kt (5 hunks)
  • app/src/main/res/values/strings.xml (3 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.kt

📄 CodeRabbit inference engine (GEMINI.md)

**/*.kt: Use 100% Kotlin for all code implementation
Implement Clean Architecture with MVI (Model-View-Intent) pattern using single-direction data flow (UDF) where UI events flow through ViewModel to the data layer, and processed State results flow back to UI
Use Coroutines and Flow for asynchronous operations and reactive data streams
Use Hilt for dependency injection to manage dependencies between layers and reduce coupling between components
Apply Proguard and R8 code obfuscation and optimization for release builds to ensure security and performance
Maintain consistent code style using Ktlint plugin for Kotlin code formatting

Files:

  • app/src/main/java/com/depromeet/team6/presentation/ui/home/component/CharacterLottieSpeechBubble.kt
  • app/src/main/java/com/depromeet/team6/presentation/ui/home/component/CourseTextButton.kt
  • app/src/main/java/com/depromeet/team6/presentation/ui/main/MainActivity.kt
  • app/src/main/java/com/depromeet/team6/presentation/ui/home/component/AfterRegisterMap.kt
  • app/src/main/java/com/depromeet/team6/presentation/ui/common/speechbubble/AtchaSpeechBubble.kt
  • app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeContract.kt
  • app/src/main/java/com/depromeet/team6/presentation/ui/common/speechbubble/AtchaSpeechBubbleTest.kt
  • app/src/main/java/com/depromeet/team6/presentation/ui/home/component/CharacterSpeechBubble.kt
  • app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeViewModel.kt
  • app/src/main/java/com/depromeet/team6/presentation/ui/home/component/AfterRegisterSheet.kt
  • app/src/main/java/com/depromeet/team6/presentation/ui/home/component/MapView.kt
  • app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeScreen.kt
**/*ViewModel.kt

📄 CodeRabbit inference engine (GEMINI.md)

Use AAC ViewModel for state management and to handle the single-direction data flow (UDF) pattern between UI Layer and Domain Layer

Files:

  • app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeViewModel.kt
🧠 Learnings (6)
📚 Learning: 2025-11-30T06:06:10.920Z
Learnt from: CR
Repo: depromeet/16th-team6-Android PR: 0
File: GEMINI.md:0-0
Timestamp: 2025-11-30T06:06:10.920Z
Learning: Applies to **/*Compose.kt : Use Jetpack Compose with declarative UI to construct UI components and handle user interactions for State subscription from ViewModel

Applied to files:

  • app/src/main/java/com/depromeet/team6/presentation/ui/home/component/CharacterLottieSpeechBubble.kt
  • app/src/main/java/com/depromeet/team6/presentation/ui/main/MainActivity.kt
  • app/src/main/java/com/depromeet/team6/presentation/ui/common/speechbubble/AtchaSpeechBubble.kt
  • app/src/main/java/com/depromeet/team6/presentation/ui/common/speechbubble/AtchaSpeechBubbleTest.kt
  • app/src/main/java/com/depromeet/team6/presentation/ui/home/component/CharacterSpeechBubble.kt
  • app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeViewModel.kt
  • app/src/main/java/com/depromeet/team6/presentation/ui/home/component/AfterRegisterSheet.kt
  • app/src/main/java/com/depromeet/team6/presentation/ui/home/component/MapView.kt
  • app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeScreen.kt
📚 Learning: 2025-11-30T06:06:10.920Z
Learnt from: CR
Repo: depromeet/16th-team6-Android PR: 0
File: GEMINI.md:0-0
Timestamp: 2025-11-30T06:06:10.920Z
Learning: Applies to **/*Service.kt : Use BackgroundService and BroadcastReceiver for background processing tasks such as push notifications and alarms

Applied to files:

  • app/src/main/java/com/depromeet/team6/presentation/ui/main/MainActivity.kt
  • app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeViewModel.kt
  • app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeScreen.kt
📚 Learning: 2025-11-30T06:06:10.920Z
Learnt from: CR
Repo: depromeet/16th-team6-Android PR: 0
File: GEMINI.md:0-0
Timestamp: 2025-11-30T06:06:10.920Z
Learning: Applies to **/*ViewModel.kt : Use AAC ViewModel for state management and to handle the single-direction data flow (UDF) pattern between UI Layer and Domain Layer

Applied to files:

  • app/src/main/java/com/depromeet/team6/presentation/ui/main/MainActivity.kt
  • app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeViewModel.kt
  • app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeScreen.kt
📚 Learning: 2025-11-30T06:06:10.920Z
Learnt from: CR
Repo: depromeet/16th-team6-Android PR: 0
File: GEMINI.md:0-0
Timestamp: 2025-11-30T06:06:10.920Z
Learning: Applies to **/*.kt : Use Hilt for dependency injection to manage dependencies between layers and reduce coupling between components

Applied to files:

  • app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeScreen.kt
📚 Learning: 2025-11-30T06:06:10.920Z
Learnt from: CR
Repo: depromeet/16th-team6-Android PR: 0
File: GEMINI.md:0-0
Timestamp: 2025-11-30T06:06:10.920Z
Learning: Applies to **/*.kt : Implement Clean Architecture with MVI (Model-View-Intent) pattern using single-direction data flow (UDF) where UI events flow through ViewModel to the data layer, and processed State results flow back to UI

Applied to files:

  • app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeScreen.kt
📚 Learning: 2025-11-30T06:06:10.920Z
Learnt from: CR
Repo: depromeet/16th-team6-Android PR: 0
File: GEMINI.md:0-0
Timestamp: 2025-11-30T06:06:10.920Z
Learning: Applies to **/*.kt : Use Coroutines and Flow for asynchronous operations and reactive data streams

Applied to files:

  • app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeScreen.kt
🧬 Code graph analysis (4)
app/src/main/java/com/depromeet/team6/presentation/ui/common/speechbubble/AtchaSpeechBubbleTest.kt (2)
app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeViewModel.kt (1)
  • onCharacterClick (403-405)
app/src/main/java/com/depromeet/team6/presentation/ui/common/speechbubble/AtchaSpeechBubble.kt (1)
  • AtchaSpeechBubble (25-71)
app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeViewModel.kt (6)
app/src/main/java/com/depromeet/team6/presentation/ui/coursesearch/CourseSearchViewModel.kt (1)
  • getTaxiCost (274-290)
app/src/main/java/com/depromeet/team6/data/dataremote/service/TaxiCostService.kt (2)
  • getTaxiCost (10-18)
  • getTaxiCost (11-17)
app/src/main/java/com/depromeet/team6/domain/repository/TaxiCostRepository.kt (2)
  • getTaxiCost (6-16)
  • getTaxiCost (7-7)
app/src/main/java/com/depromeet/team6/data/repositoryimpl/TaxiCostRepositoryImpl.kt (1)
  • getTaxiCost (25-40)
app/src/main/java/com/depromeet/team6/presentation/util/base/BaseViewModel.kt (2)
  • setState (39-41)
  • setEvent (43-45)
app/src/main/java/com/depromeet/team6/data/repositoryimpl/TransitsRepositoryImpl.kt (1)
  • getBusArrival (29-42)
app/src/main/java/com/depromeet/team6/presentation/ui/home/component/AfterRegisterSheet.kt (1)
app/src/main/java/com/depromeet/team6/presentation/ui/home/component/TimeText.kt (1)
  • TimeText (15-57)
app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeScreen.kt (1)
app/src/main/java/com/depromeet/team6/presentation/ui/common/speechbubble/AtchaSpeechBubbleTest.kt (1)
  • AtchaSpeechCharacter (51-177)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: CI
🔇 Additional comments (4)
app/src/main/java/com/depromeet/team6/presentation/ui/home/component/CourseTextButton.kt (1)

31-36: 터치 영역 관점에서 더 안전한 변경입니다

noRippleClickablepadding보다 먼저 적용해서 패딩 포함 전체 Row 영역이 클릭 가능해졌고, 이는 사용자 실수 터치 감소와 접근성 측면에서 바람직합니다. 성능·아키텍처·보안 측면에서도 추가 리스크는 없어 보입니다.

app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeScreen.kt (3)

462-471: 화면 정리 로직 적절함

DisposableEffect를 사용하여 화면이 사라질 때 isMapReady 상태를 초기화하는 것은 올바른 패턴입니다.


836-849: 맵 준비 상태에 따른 조건부 렌더링 적절함

isMapReady 상태에 기반한 AtchaSpeechCharacter 조건부 렌더링은 맵이 로드되기 전 UI 문제를 방지하는 좋은 접근입니다. speechRequest 패턴을 통한 새로운 캐릭터 메시지 시스템도 MVI 패턴에 부합합니다.


589-596: 콜백을 통한 상태 업데이트 패턴 적절함

isMapReadyCallback이 ViewModel의 상태를 업데이트하는 방식은 단방향 데이터 흐름(UDF) 패턴을 따르고 있습니다.

Comment on lines +42 to +43
versionCode = 21
versionName = "1.3.2"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "*.kt" -o -name "*.java" | grep -i "viewmodel\|activity" | head -20

Repository: depromeet/16th-team6-Android

Length of output: 1153


🏁 Script executed:

find . -type f \( -name "*HomeViewModel*" -o -name "*ViewModel.kt" \) | head -10

Repository: depromeet/16th-team6-Android

Length of output: 919


🏁 Script executed:

rg "HomeViewModel|checkAppVersion" --type kt --type java -l | head -10

Repository: depromeet/16th-team6-Android

Length of output: 98


🏁 Script executed:

rg "compareVersions" --type kt --type java -A 15 -B 2

Repository: depromeet/16th-team6-Android

Length of output: 98


🏁 Script executed:

cat app/build.gradle.kts | grep -A 5 -B 5 "versionCode\|versionName"

Repository: depromeet/16th-team6-Android

Length of output: 1194


🏁 Script executed:

git log --oneline -20 -- app/build.gradle.kts

Repository: depromeet/16th-team6-Android

Length of output: 235


🏁 Script executed:

git show HEAD~1:app/build.gradle.kts | grep -A 3 -B 3 "versionCode\|versionName" || echo "Could not retrieve previous version"

Repository: depromeet/16th-team6-Android

Length of output: 161


🏁 Script executed:

cat app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeViewModel.kt

Repository: depromeet/16th-team6-Android

Length of output: 31602


🏁 Script executed:

rg "compareVersions" -A 20 -B 2

Repository: depromeet/16th-team6-Android

Length of output: 5215


versionName downgrade can break app update prompts

The version comparison in HomeViewModel.checkAppVersion() uses proper semantic versioning (major.minor.patch as integers). If versionName goes from "1.3.14" to "1.3.2", the numeric comparison will correctly identify patch 14 > 2, causing the app to report "UP_TO_DATE" when the server returns latest version "1.3.2". Users with 1.3.14 installed will not receive update notifications, even for critical patches in the new build.

Either confirm this version reset is intentional and adjust server version delivery accordingly, or keep versionName ≥ 1.3.14 to maintain update consistency.

🤖 Prompt for AI Agents
In app/build.gradle.kts around lines 42 to 43, the versionName was changed to
"1.3.2" while existing releases include "1.3.14", which will cause semantic
numeric comparisons in HomeViewModel.checkAppVersion() to treat 1.3.14 as newer
and suppress update prompts; either revert/increment versionName to be >= the
highest published version (e.g., set to "1.3.14" or higher) or coordinate with
the server to return a version string that is semantically greater than
installed builds; update the versionName appropriately and ensure versionCode is
increased to match the new versionName so update checks behave correctly.

@@ -0,0 +1,213 @@
package com.depromeet.team6.presentation.ui.common.speechbubble
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

파일 이름이 실제 용도와 불일치

이 파일은 AtchaSpeechBubbleTest.kt로 명명되어 있지만 테스트 파일이 아닌 프로덕션 Composable입니다. 이는 다른 개발자들에게 혼란을 줄 수 있고, 테스트 파일로 오인되어 빌드/패키징 시 의도치 않게 제외될 수 있습니다.

AtchaSpeechCharacter.kt 또는 SpeechBubbleStack.kt와 같은 명확한 이름으로 변경하세요.

🤖 Prompt for AI Agents
In app/src/main/java/com/depromeet/team6/presentation/ui/common/speechbubble
AtchaSpeechBubbleTest.kt (line 1), the filename wrongly indicates a test file
although it contains production Composable code; rename the file to a clear
production name (e.g., AtchaSpeechCharacter.kt or SpeechBubbleStack.kt), update
any references/usages and imports in the codebase to the new filename, adjust
the Kotlin class/Composable name if it currently mismatches the new filename,
and run a full build/IDE sync to ensure no unresolved references remain.

Comment on lines +100 to +130
speechJob = scope.launch {
val removalJobs = mutableListOf<Job>()
// 말풍선 생성 로직
messagesToAdd.forEach { messageText ->
// 부모 Job(speechJob)이 취소되면 delay에서 예외가 발생하며 중단됩니다.
delay(700L)
val newBubble = BubbleMessage(
id = System.nanoTime(),
text = messageText,
isVisible = true
)
bubbles.add(newBubble)

// 개별 말풍선 제거 타이머
val removalJob = scope.launch {
delay(2500L)
val idx = bubbles.indexOfFirst { it.id == newBubble.id }
if (idx != -1) {
bubbles[idx] = bubbles[idx].copy(isVisible = false)
}
delay(350)
val removeIdx = bubbles.indexOfFirst { it.id == newBubble.id }
if (removeIdx != -1) {
bubbles.removeAt(removeIdx)
}
}
removalJobs.add(removalJob)
}
// --- 모든 말풍선이 제거되면 speechJob 완료 -> 해당 라이프사이클 기준으로 쓰로틀링 ---
removalJobs.joinAll()
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

코루틴 Job 관리의 잠재적 메모리 누수 및 동작 불일치

removalJobsscope.launch로 생성되어 부모 speechJob이 취소되어도 개별 removal job들은 계속 실행됩니다. 이로 인해:

  1. 컴포저블이 dispose될 때 removal job이 계속 실행되어 메모리 누수 발생 가능
  2. speechJob이 취소되어도 bubbles 상태가 계속 수정되어 예기치 않은 UI 동작 발생

scope.launch 대신 현재 코루틴 컨텍스트에서 launch하여 부모 Job 취소 시 함께 취소되도록 수정하세요:

         speechJob = scope.launch {
             val removalJobs = mutableListOf<Job>()
             messagesToAdd.forEach { messageText ->
                 delay(700L)
                 val newBubble = BubbleMessage(
                     id = System.nanoTime(),
                     text = messageText,
                     isVisible = true
                 )
                 bubbles.add(newBubble)

-                val removalJob = scope.launch {
+                val removalJob = launch {
                     delay(2500L)
                     val idx = bubbles.indexOfFirst { it.id == newBubble.id }
                     if (idx != -1) {
                         bubbles[idx] = bubbles[idx].copy(isVisible = false)
                     }
                     delay(350)
                     val removeIdx = bubbles.indexOfFirst { it.id == newBubble.id }
                     if (removeIdx != -1) {
                         bubbles.removeAt(removeIdx)
                     }
                 }
                 removalJobs.add(removalJob)
             }
             removalJobs.joinAll()
         }
📝 Committable suggestion

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

Suggested change
speechJob = scope.launch {
val removalJobs = mutableListOf<Job>()
// 말풍선 생성 로직
messagesToAdd.forEach { messageText ->
// 부모 Job(speechJob)이 취소되면 delay에서 예외가 발생하며 중단됩니다.
delay(700L)
val newBubble = BubbleMessage(
id = System.nanoTime(),
text = messageText,
isVisible = true
)
bubbles.add(newBubble)
// 개별 말풍선 제거 타이머
val removalJob = scope.launch {
delay(2500L)
val idx = bubbles.indexOfFirst { it.id == newBubble.id }
if (idx != -1) {
bubbles[idx] = bubbles[idx].copy(isVisible = false)
}
delay(350)
val removeIdx = bubbles.indexOfFirst { it.id == newBubble.id }
if (removeIdx != -1) {
bubbles.removeAt(removeIdx)
}
}
removalJobs.add(removalJob)
}
// --- 모든 말풍선이 제거되면 speechJob 완료 -> 해당 라이프사이클 기준으로 쓰로틀링 ---
removalJobs.joinAll()
}
speechJob = scope.launch {
val removalJobs = mutableListOf<Job>()
// 말풍선 생성 로직
messagesToAdd.forEach { messageText ->
// 부모 Job(speechJob)이 취소되면 delay에서 예외가 발생하며 중단됩니다.
delay(700L)
val newBubble = BubbleMessage(
id = System.nanoTime(),
text = messageText,
isVisible = true
)
bubbles.add(newBubble)
// 개별 말풍선 제거 타이머
val removalJob = launch {
delay(2500L)
val idx = bubbles.indexOfFirst { it.id == newBubble.id }
if (idx != -1) {
bubbles[idx] = bubbles[idx].copy(isVisible = false)
}
delay(350)
val removeIdx = bubbles.indexOfFirst { it.id == newBubble.id }
if (removeIdx != -1) {
bubbles.removeAt(removeIdx)
}
}
removalJobs.add(removalJob)
}
// --- 모든 말풍선이 제거되면 speechJob 완료 -> 해당 라이프사이클 기준으로 쓰로틀링 ---
removalJobs.joinAll()
}

Comment on lines +156 to +174
modifier = Modifier.noRippleClickable {
// 캐릭터 클릭시 애니메이션은 무조건 재생하되, API 호출만 throttling 처리
if (speechJob?.isActive == false && !debouncing) {
debouncing = true
onCharacterClick()
scope.launch {
delay(1000)
debouncing = false
}
}
scope.launch {
// 매 클릭마다 0에서 1까지 1회 재생
lottie.animate(
composition = composition,
iterations = 1,
initialProgress = 0f
)
}
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

클릭 핸들러 내 코루틴 로직 개선 필요

Line 166-173의 Lottie 애니메이션 실행은 debouncing 조건과 무관하게 항상 실행됩니다. 주석에 "애니메이션은 무조건 재생"이라고 명시되어 있어 의도된 동작으로 보이나, speechJob?.isActive == false 체크와 debouncing 체크가 분리되어 있어 의도 파악이 어렵습니다.

또한 Line 161-164의 debouncing 해제 코루틴은 예외 발생 시 debouncing = false가 실행되지 않아 영구적으로 클릭이 차단될 수 있습니다.

                 if (speechJob?.isActive == false && !debouncing) {
                     debouncing = true
                     onCharacterClick()
                     scope.launch {
-                        delay(1000)
-                        debouncing = false
+                        try {
+                            delay(1000)
+                        } finally {
+                            debouncing = false
+                        }
                     }
                 }
🤖 Prompt for AI Agents
In
app/src/main/java/com/depromeet/team6/presentation/ui/common/speechbubble/AtchaSpeechBubbleTest.kt
around lines 156-174, the click handler currently runs the Lottie animation
unconditionally while the API-throttling debouncing logic is separate, and the
coroutine that clears debouncing (lines 161-164) can fail to reset debouncing if
an exception occurs; refactor so the intent is explicit by either: 1) keeping
the animation launch outside the debouncing branch but moving the debouncing
reset into a try/finally inside the coroutine that sets debouncing (so
debouncing is always cleared even on error), or 2) running the animation inside
the same conditional branch as the API debouncing if you want them tied
together; implement the chosen approach and ensure any coroutine that sets
debouncing always clears it in a finally block and that animation coroutines are
launched on the same scope to avoid leaks.

Comment on lines +56 to 58
onTransportMarkerClick: (FocusedMarkerParameter) -> Unit = {},
isMapReadyCallback: () -> Unit = {}
) {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

isMapReadyCallback 이 리컴포지션마다 호출되어 부작용이 중복 실행될 수 있습니다

현재 구현에서는:

var isMapReady by remember { mutableStateOf(false) }
// ...
tMapView.setOnMapReadyListener {
    // ...
    isMapReady = true
}
// ...
if (isMapReady) {
    isMapReadyCallback()
} else {
    AtChaLoadingView(...)
}

isMapReady 가 한 번 true 가 된 이후로는, 해당 컴포저블이 리컴포즈될 때마다 isMapReadyCallback() 이 다시 호출됩니다. 이 콜백이:

  • ForegroundService 시작
  • ViewModel 이벤트 디스패치
  • Analytics 이벤트 전송

등의 부작용을 포함할 경우, 동일 작업이 여러 번 실행되어 서비스 중복 시작, 불필요한 네트워크 호출, 상태 플리커 등이 발생할 수 있습니다.

의도가 “지도 준비 완료 시 1회만 콜백”이라면, 다음과 같이 one‑shot 패턴으로 바꾸는 것을 권장합니다:

-    var isMapReady by remember { mutableStateOf(false) }
+    var isMapReady by remember { mutableStateOf(false) }

     AndroidView(
         // ...
         factory = { context ->
             val tMapView = TMapView(context)
             // ...
             tMapView.setOnMapReadyListener {
                 // 기존 로직 유지
                 // ...
                 isMapReady = true
             }
             tMapView
         },
         // ...
     )

-    if (isMapReady) {
-        isMapReadyCallback()
-    } else {
-        AtChaLoadingView(
-            transparent = false
-        )
-    }
+    LaunchedEffect(isMapReady) {
+        if (isMapReady) {
+            isMapReadyCallback()
+        }
+    }
+
+    if (!isMapReady) {
+        AtChaLoadingView(
+            transparent = false
+        )
+    }

이렇게 하면:

  • isMapReadyfalse → true 로 전이될 때 콜백이 정확히 한 번만 호출되고,
  • 이후 리컴포지션에서는 콜백이 재실행되지 않아 부작용이 중복되지 않습니다.

Also applies to: 337-343

Comment on lines +121 to +175
// AtchaSpeechCharacter(
// modifier = Modifier
// .offset(y = 10.dp),
// messagesToAdd = listOf(
// "테스트 테스트 야 야 야",
// "두번째 테스트 호롤로",
// "아라라라랄랄ㄹ라"
// )
// )

// if (hasAnyText) {
// Box {
// [email protected](
// visible = isBottomSpeechBubbleVisible && showSpeechBubble,
// enter = fadeIn(),
// exit = fadeOut(),
// modifier = Modifier
// .offset(y = 10.dp) // 캐릭터 상단 기준 아래로 10dp 간격
// ) {
// AtchaSpeechCharacter(
// messagesToAdd = listOf(
// "테스트 테스트 야 야 야",
// "두번째 테스트 호롤로",
// "아라라라랄랄ㄹ라"
// )
// )
// AtchaSpeechBubble(
// prefix = prefixText,
// modifier = Modifier,
// emphasisText = emphasisText,
// suffix = suffixText,
// tailExist = true
// )
// }

// if (lineCount == 2) {
// androidx.compose.animation.AnimatedVisibility(
// visible = isTopSpeechBubbleVisible && showSpeechBubble,
// enter = fadeIn(),
// exit = fadeOut(),
// modifier = Modifier
// .align(Alignment.TopStart)
// .offset(y = (-26).dp) // 말풍선 높이 + 간격
// ) {
// AtchaSpeechBubble(
// prefix = topPrefixText ?: "",
// modifier = Modifier,
// emphasisText = topEmphasisText,
// suffix = topSuffixText,
// tailExist = false
// )
// }
// }
// }
// }
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

대량의 주석 처리된 코드 제거 필요

약 55줄의 주석 처리된 코드가 프로덕션 파일에 남아있습니다. 이는 코드 가독성을 심각하게 저해하고 향후 유지보수를 어렵게 만듭니다.

Git 히스토리를 통해 이전 구현을 확인할 수 있으므로, 주석 처리된 코드는 완전히 제거하세요. 추후 참조가 필요하다면 별도 브랜치나 태그로 보존하는 것이 좋습니다.

🤖 Prompt for AI Agents
In
app/src/main/java/com/depromeet/team6/presentation/ui/home/component/CharacterLottieSpeechBubble.kt
around lines 121 to 175, there are ~55 lines of commented-out code left in the
production file; remove the entire commented block (all commented
AtchaSpeechCharacter/AtchaSpeechBubble attempts and related AnimatedVisibility
scaffolding) so the file contains only active code, then commit the change; if
you want to preserve the old implementation for reference, create a branch or
tag or rely on git history rather than leaving commented code in the file.

Comment on lines +49 to 51
mapModified: () -> Unit,
isMapReadyCallback: () -> Unit
) {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

TMapViewCompose 의 isMapReadyCallback 도 리컴포지션마다 반복 호출됩니다

여기서도 isMapReadytrue 로 한 번 바뀐 이후에는, 컴포저블가 리컴포즈될 때마다 콜백이 다시 실행됩니다:

var isMapReady by remember { mutableStateOf(false) }
// ...
tMapView.setOnMapReadyListener {
    // ...
    isMapReady = true
}
// ...
if (isMapReady) {
    isMapReadyCallback()
    Image( /* 출발 마커 렌더링 */ )
} else {
    AtChaLoadingView(...)
}

이 구조에서는:

  • 콜백이 ViewModel 이벤트 디스패치, ForegroundService 시작, 분석 이벤트 전송 등에 연결되어 있을 경우,
  • 스크롤/상태 변경으로 인한 리컴포지션마다 동일 부작용이 반복 실행될 수 있습니다.

AfterRegisterMap 과 마찬가지로, “지도 준비 완료 시 한 번만 알려주는” 용도라면 LaunchedEffect 를 통해 one‑shot 으로 바꾸는 편이 안전합니다:

-    var isMapReady by remember { mutableStateOf(false) }
+    var isMapReady by remember { mutableStateOf(false) }
    // ...

    AndroidView(
        // ...
        factory = { context ->
            val tMapView = TMapView(context)
            // ...
            tMapView.setOnMapReadyListener {
                // 기존 초기화 로직 유지
                // ...
                isMapReady = true
            }
            tMapView
        },
        // ...
    )

-    if (isMapReady) {
-        isMapReadyCallback()
-        // 출발 마커
-        Image(...)
-    } else {
-        AtChaLoadingView(transparent = false)
-    }
+    LaunchedEffect(isMapReady) {
+        if (isMapReady) {
+            isMapReadyCallback()
+        }
+    }
+
+    if (isMapReady) {
+        // 출발 마커
+        Image(...)
+    } else {
+        AtChaLoadingView(transparent = false)
+    }

이렇게 하면:

  • 콜백은 false → true 전이 시점에 한 번만 호출되고,
  • 이후 지도 상호작용/리컴포지션에도 중복 부작용이 발생하지 않습니다.

Also applies to: 165-175

Comment on lines +112 to +142
// val characterTexts = CharacterTexts(
// taxiCostText = stringResource(R.string.home_bubble_basic_text),
// aboutText = stringResource(R.string.home_bubble_least_text),
// wonText = stringResource(R.string.home_bubble_won_text),
// moveMapText = stringResource(R.string.home_bubble_map_text),
//
// expectDepartText = stringResource(R.string.home_bubble_alarm_emphasis_text),
// expectTaxiCostText = stringResource(R.string.home_bubble_user_departure_taxi_cost_text),
// expectBustDepartureText = stringResource(R.string.home_bubble_before_bus_departure_text),
//
// expectTimeClickedText1 = stringResource(R.string.home_bubble_expect_departure_time_clicked_text_1),
// expectTimeClickedText2 = stringResource(R.string.home_bubble_expect_departure_time_clicked_text_2),
// changeLocationClickedText = stringResource(R.string.home_bubble_location_clicked_text),
//
// busDepartureText1 = stringResource(R.string.home_bubble_bus_departure_text_1),
// busDepartureText2 = stringResource(R.string.home_bubble_bus_departure_text_2),
// subwayDepartureText1 = stringResource(R.string.home_bubble_subway_departure_text_1),
// subwayDepartureText2 = stringResource(R.string.home_bubble_subway_departure_text_2),
// timeInfoText1 = stringResource(R.string.home_bubble_time_info_text_1),
// timeInfoText2 = stringResource(R.string.home_bubble_time_info_text_2),
// departureTimeText1 = stringResource(R.string.home_bubble_departure_time_info_text_1),
// busDepartureTaxiCostText = stringResource(R.string.home_bubble_departed_taxi_cost_text),
// trustText1 = stringResource(R.string.home_bubble_trust_text_1),
// trustText2 = stringResource(R.string.home_bubble_trust_text_2),
//
// userDepartureBusText = stringResource(R.string.home_bubble_user_departure_bus_text),
// userDepartureSubwayText = stringResource(R.string.home_bubble_user_departure_subway_text),
// userDepartureDownText = stringResource(R.string.home_bubble_user_departure_down_text),
// userDepartureDetailBtnText = stringResource(R.string.home_bubble_user_departure_detail_btn_text),
// userDepartureCheckDetailText = stringResource(R.string.home_bubble_user_departure_check_detail_text)
// )
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

대규모 주석 처리된 코드 제거 권장

파일 전반에 걸쳐 150줄 이상의 주석 처리된 코드가 존재합니다 (CharacterTexts, baseCharacterData, showTempMessage 함수 등). 새로운 speech 시스템으로의 전환이 완료되면 이 코드들을 정리해야 합니다. Git 히스토리에서 필요시 복구할 수 있으므로 주석 상태로 유지할 필요가 없습니다.

🤖 Prompt for AI Agents
In app/src/main/java/com/depromeet/team6/presentation/ui/home/HomeScreen.kt
around lines 112-142 there is a large commented-out CharacterTexts block (and
the review notes other big commented blocks like baseCharacterData and
showTempMessage) that should be removed: delete these commented code blocks from
the file (and any other similar large commented sections in this file) ensuring
you do not remove any live references, run a build/tests to confirm nothing
depended on them, and commit the cleanup with a message like "chore(home):
remove obsolete commented speech-system code (recoverable via git history)".

- **빌드 시스템**: Gradle (Kotlin DSL - `build.gradle.kts`)
- **CI/CD**: GitHub Actions (`.github/workflows/android_ci.yml`)를 통해 CI 파이프라인이 구축되어 있어 코드 통합 및 빌드 자동화를 수행합니다.
- **코드 품질**: Ktlint 플러그인을 사용하여 코드 스타일을 일관되게 유지합니다.
- **코드 품질**: Kt-lint 플러그인을 사용하여 코드 스타일을 일관되게 유지합니다.
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

공식 명칭과 불일치

Ktlint의 공식 명칭은 하이픈 없이 "Ktlint"입니다. 문서 일관성을 위해 원래 표기를 유지하는 것이 좋습니다.

-- **코드 품질**: Kt-lint 플러그인을 사용하여 코드 스타일을 일관되게 유지합니다.
+- **코드 품질**: Ktlint 플러그인을 사용하여 코드 스타일을 일관되게 유지합니다.
📝 Committable suggestion

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

Suggested change
- **코드 품질**: Kt-lint 플러그인을 사용하여 코드 스타일을 일관되게 유지합니다.
- **코드 품질**: Ktlint 플러그인을 사용하여 코드 스타일을 일관되게 유지합니다.
🤖 Prompt for AI Agents
In GEMINI.md around line 57, the document uses the incorrect official name
"Kt-lint"; replace that instance with the correct official spelling "Ktlint" to
maintain consistency in the docs (update the line to read: **코드 품질**: Ktlint
플러그인을 사용하여 코드 스타일을 일관되게 유지합니다.).

- 브로드캐스트 발송 로직을 명시적 브로드캐스트로 수정했습니다.
@hyuns66 hyuns66 merged commit 6561932 into develop Nov 30, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat Feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 목적지 부근 도착 시 안내 종료

2 participants