Skip to content

Conversation

@Hyobeen-Park
Copy link
Collaborator

@Hyobeen-Park Hyobeen-Park commented Oct 4, 2025

Related issue 🛠

Work Description ✏️

  • 믹스패널 붙이기

Screenshot 📸

N/A

Uncompleted Tasks 😅

  • 추가 못한 속성들 추가하기
  • 불가한 속성들 기획쌤들이랑 논의하기

To Reviewers 📢

드디어 숙제 하나 끝!!ㅠㅠ 하다보니 점점 욕심이 나서 캡슐화까지 해버렸어요...ㅎㅎ 그래도 막 엄청 딥하게 막 하지는 않았음!! 리뷰하실 때 구조정도만 봐주셔도 될 것 같아요! 그치만 같이 꼼꼼하게 확인해주신다면 개큰 박수와 사랑을 드리겠습니다😊

벌써부터 conflict가 너무 무서운데요..?? 세홍오빠 머지하고 나면 진짜 충돌 많이 생길 것 같아서 마음이 심란하네요ㅜㅜ 오빠 머지 언제할거야!!!!!!!! 이 pr을 보고있다면 당장 머지해

함수에 주석처리 되어있는 부분이 있는데요..!! 요건 현재 상태에서 넣을 수 없는 속성입니다..ㅎㅎ entryPoint의 경우 기획쌤들이랑 상의를 조금 해야 할 것 같구... 나머지 속성들은 최대한 작업 더 해서 넣으려구요 지금 주석처리 되어있는 부분 + 탐색뷰 필터 변경 이벤트만 제외하고 다 심었는데 위에서 말했던 것처럼 conflict가 너무 심할 것 같아서 하다가 중간에 멈췄어요ㅎㅎ 이건 세홍오빠 pr 머지하고 나면 한 번 더 작업해볼게요!!

로컬프로퍼티 수정하기

속성 못 넣은거 + 탐색뷰 필터 변경 이벤트 수정하고 draft 풀게요!! 구조는 수정되지 않을거라 리뷰 달아주셔도 괜찮습니다ㅎㅎ

Summary by CodeRabbit

  • New Features

    • 앱 행동 분석 이벤트 수집 추가: 앱 실행, 로그인/회원가입, 온보딩(완료/스킵), 탭 진입, 탐색 정렬/검색, 지도 검색/필터, 팔로우/언팔로우, 리뷰 상세(스푼 사용·저장·길찾기), 업로드 등 다양함
    • 사용자 프로필 속성 전송(스플래시 진입 시) 및 초기화(계정관리/탈퇴)
  • Improvements

    • 스플래시 흐름을 상태·생명주기 기반으로 보강해 초기 진입 및 이동 신뢰성 향상
  • Chores

    • 외부 분석 도구 연동 및 빌드/CI 설정 추가

@Hyobeen-Park Hyobeen-Park added this to the 5차 스프린트 milestone Oct 4, 2025
@Hyobeen-Park Hyobeen-Park self-assigned this Oct 4, 2025
@Hyobeen-Park Hyobeen-Park requested a review from a team as a code owner October 4, 2025 17:58
@Hyobeen-Park Hyobeen-Park added the ❤️효빈❤️ ❤️효빈❤️ label Oct 4, 2025
@Hyobeen-Park Hyobeen-Park removed the request for review from a team October 4, 2025 17:58
@Hyobeen-Park Hyobeen-Park added the FEAT✨ 새로운 기능 구현 label Oct 4, 2025
@coderabbitai
Copy link

coderabbitai bot commented Oct 4, 2025

Walkthrough

앱에 Mixpanel SDK를 통합하고 BuildConfig/의존성/CI 설정을 추가했으며, Mixpanel 래퍼와 여러 도메인별 이벤트 클래스 및 CompositionLocal 주입을 도입하고 주요 화면들에 이벤트 호출을 삽입했다. 스플래시 흐름과 일부 모델/매퍼/열거형도 확장됐다.

Changes

Cohort / File(s) Change Summary
Build & CI
app/build.gradle.kts, gradle/libs.versions.toml, .github/workflows/pr_builder.yml
Mixpanel 라이브러리 및 버전 추가, BuildConfig MIXPANEL_KEY(debug/release) 주입, CI에서 mixpanelDevKey/mixpanelProdKey로 local.properties 작성.
Analytics Core
app/src/main/java/.../core/analytics/MixPanelTracker.kt, .../core/analytics/events/MixPanelEvents.kt, .../core/analytics/events/MixPanelUserProperties.kt
Mixpanel SDK 래퍼 트래커 추가, MixPanelEvents 집합과 LocalTracker CompositionLocal 제공, 사용자 프로필 설정/리셋 위임 클래스 추가.
도메인별 이벤트 클래스
.../core/analytics/events/AnalyticsEvents.kt, CommonEvents.kt, ExploreEvents.kt, MapEvents.kt, OnboardingEvents.kt, RegisterEvents.kt, ReviewDetailEvents.kt, SpoonDrawEvents.kt, MypageEvents.kt
도메인별 이벤트 메서드 추가(앱오픈/로그인/탭진입/정렬/검색/온보딩/리뷰/스푼/마이페이지 등), JSONObject/JSONArray 기반 속성 구성.
CompositionLocal 주입 및 초기화
app/src/main/java/.../presentation/MainActivity.kt, .../designsystem/component/dialog/SpoonDrawDialog.kt
MainActivity에 MixPanelEvents 주입 및 CompositionLocalProvider로 전역 제공. SpoonDrawDialog 결과 상태에서 이벤트 전송 추가.
온보딩·인증 화면 트래킹
.../presentation/auth/onboarding/*.kt, .../presentation/auth/signin/SignInScreen.kt
온보딩 단계 완료/스킵 및 로그인 성공 시 analytics 이벤트 호출 추가.
탐색·검색 화면 트래킹
.../presentation/explore/ExploreScreen.kt, .../presentation/explore/component/ExploreFilterSection.kt, .../presentation/exploreSearch/ExploreSearchScreen.kt, .../presentation/explore/type/SortingOption.kt, .../presentation/exploreSearch/type/SearchType.kt
탭 진입, 정렬 선택(trackingCode 추가), 로컬 리뷰 토글, 검색 시 이벤트 전송; SortingOption/SearchType 시그니처 확장.
지도·장소 상세 트래킹
.../presentation/gourmet/map/MapScreen.kt, .../presentation/gourmet/search/MapSearchScreen.kt, .../presentation/placeDetail/PlaceDetailRoute.kt, .../presentation/placeDetail/model/PlaceDetailModel.kt
지도 탭/필터/검색 트래킹, 장소 상세에서 조회·스푼·저장·길찾기·팔로우 이벤트 삽입 및 PlaceDetailModel에 category 추가. PlaceDetailScreen에 트래킹 콜백 파라미터 추가.
팔로우 UI 변경
.../presentation/follow/FollowRoute.kt, .../presentation/follow/component/UserListScreen.kt
UserListScreentype: FollowType 파라미터 추가 및 팔로우/언팔로우 이벤트(진입 지점 포함) 로깅.
마이페이지·프로필 변경
.../presentation/userpage/mypage/MyPageRoute.kt, .../presentation/userpage/component/UserScreen.kt, .../presentation/profileedit/ProfileEditScreen.kt, .../presentation/profileedit/ProfileEditViewModel.kt
마이페이지 탭 진입, 프로필 조회·스푼캐릭터·프로필 업데이트 이벤트 추가; ProfileEditViewModel에 수정된 필드 추적 세트(fieldsUpdated) 추가.
등록(리뷰) 플로우 확장
.../presentation/register/*.kt, .../presentation/register/model/PlaceReviewModel.kt, .../presentation/register/model/RegisterState.kt
업로드 탭 진입/등록 스텝 이벤트 추가, PlaceReviewModel에 addMapCount 추가, RegisterState에 userId/addMapCount 필드 추가.
스플래시 리팩터링
.../presentation/splash/SplashScreen.kt, SplashSideEffect.kt, SplashViewModel.kt, .../presentation/splash/model/UserModel.kt
초기화 로직 변경: 토큰 확인 → 사용자 정보 조회 → 사이드이펙트 기반 내비게이션; 앱오픈 트래킹 및 사용자 프로필 속성 설정, UserModel 추가.
Location 모델/매퍼 변경
.../data/mapper/LocationMapper.kt, .../domain/entity/LocationEntity.kt, .../presentation/gourmet/map/model/LocationModel.kt
LocationEntity/Model에 locationType 필드 추가 및 매퍼에서 해당 값 채움.
기타 UI 트래킹 삽입
여러 composable 파일 (Register*, MapSearch, ProfileEdit, AccountManagement, UserPage 등)
다양한 화면의 버튼/행동에 LocalTracker를 사용한 이벤트 전송 추가(프로필 리셋, 스푼 수령 등).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as 사용자
  participant App as MainActivity/Compose
  participant VM as SplashViewModel
  participant Repo as Token/User/SpoonRepo
  participant MP as MixPanelEvents
  participant SDK as Mixpanel SDK

  U->>App: 앱 시작
  App->>VM: 초기화 요청
  VM->>Repo: 토큰 조회
  alt 토큰 존재
    VM->>Repo: 사용자 정보 요청
    Repo-->>VM: UserModel
    VM-->>App: SideEffect.NavigateToMap
    App->>MP: analyticsEvents.appOpen()
    App->>MP: userProperties.setUserProfile(userId, properties)
    MP->>SDK: identify / people.set
  else 토큰 없음
    VM-->>App: SideEffect.NavigateToSignIn
    App->>MP: analyticsEvents.appOpen()
  end
Loading
sequenceDiagram
  autonumber
  participant Act as MainActivity
  participant Loc as LocalTracker (CompositionLocal)
  participant UI as Composable Screen
  participant Evt as MixPanelEvents
  participant Trk as MixPanelTracker
  participant SDK as Mixpanel SDK

  Act->>Loc: CompositionLocalProvider(LocalTracker = Evt)
  UI->>Loc: LocalTracker.current
  UI->>Evt: commonEvents.tabEntered("explore")
  Evt->>Trk: track("tab_entered", props)
  Trk->>SDK: track(event, props)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

Possibly related PRs

Suggested labels

ADD➕

Suggested reviewers

  • chattymin
  • Roel4990
  • angryPodo

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning 이 PR에는 믹스패널 통합과 직접 관련 없는 SplashViewModel의 초기화 흐름 변경 및 RegisterState·PlaceReviewModel에 기능과 무관한 속성 추가 등 분석 범위를 벗어난 도메인 로직 수정이 포함되어 있습니다. 도메인 로직 변경과 믹스패널 통합을 명확히 분리하여 별도 PR로 분할하거나 해당 변경이 분석 통합을 위해 필요함을 설명해 주시기 바랍니다.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed 제목이 주요 변경사항인 믹스패널 통합을 간결하게 요약하고 있어 이 PR의 목적을 명확히 전달하고 있습니다.
Linked Issues Check ✅ Passed 링크된 이슈 #386의 주된 목표인 믹스패널 통합이 PR 전반에 걸쳐 구현되었으며 의존성 추가, 이벤트 래퍼 클래스, UI 후킹 등 모든 요구사항을 충족하고 있습니다.
Description Check ✅ Passed 제공된 설명은 템플릿의 모든 필수 섹션(관련 이슈, 작업 내용, 스크린샷, 미완료 작업, 검토자 안내)을 포함하고 있으며 각 항목에 적절한 내용이 기재되어 있어 구조와 정보 전달이 충분합니다.
✨ 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/#386-add-mixpanel

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: 10

🧹 Nitpick comments (13)
app/src/main/java/com/spoony/spoony/presentation/follow/component/UserListScreen.kt (1)

25-25: 분석 이벤트 통합이 올바르게 구현되었습니다.

user.isFollowing 상태를 기반으로 팔로우/언팔로우 이벤트를 로깅하는 로직이 정확합니다. user는 불변 리스트의 스냅샷이므로 버튼 클릭 이전의 상태를 올바르게 반영합니다.

선택적 리팩토링: 코드 중복 제거

FOLLOWERFOLLOWING 분기의 로직이 entryPoint 문자열을 제외하고 동일합니다. 다음과 같이 리팩토링하여 중복을 줄일 수 있습니다:

 onFollowClick = {
     onButtonClick(user.userId)
-
-    when (type) {
-        FollowType.FOLLOWER -> {
-            if (user.isFollowing) {
-                tracker.commonEvents.unfollowUser(
-                    unfollowedUserId = user.userId,
-                    entryPoint = "followed_list"
-                )
-            } else {
-                tracker.commonEvents.followUser(
-                    followedUserId = user.userId,
-                    entryPoint = "followed_list"
-                )
-            }
-        }
-
-        FollowType.FOLLOWING -> {
-            if (user.isFollowing) {
-                tracker.commonEvents.unfollowUser(
-                    unfollowedUserId = user.userId,
-                    entryPoint = "following_list"
-                )
-            } else {
-                tracker.commonEvents.followUser(
-                    followedUserId = user.userId,
-                    entryPoint = "following_list"
-                )
-            }
-        }
+    
+    val entryPoint = when (type) {
+        FollowType.FOLLOWER -> "followed_list"
+        FollowType.FOLLOWING -> "following_list"
+    }
+    
+    if (user.isFollowing) {
+        tracker.commonEvents.unfollowUser(
+            unfollowedUserId = user.userId,
+            entryPoint = entryPoint
+        )
+    } else {
+        tracker.commonEvents.followUser(
+            followedUserId = user.userId,
+            entryPoint = entryPoint
+        )
     }
 },

Also applies to: 43-75

app/src/main/java/com/spoony/spoony/domain/entity/LocationEntity.kt (1)

11-11: LocationEntity 인스턴스화 검토 완료, 기본값 지정 권장
검색 결과 모든 LocationEntity 생성이 named args로 이뤄져 positional 인자 이슈는 없음을 확인했습니다. 신규 필드인 locationType에 기본값(= "" 등)을 지정하면 향후 호환성 유지에 도움이 됩니다.

app/src/main/java/com/spoony/spoony/presentation/gourmet/map/model/LocationModel.kt (1)

9-9: LocationModel.locationType nullable 여부 재검토
MapSearchScreen.kt:174에서 locationInfo.locationType.orEmpty()로 null을 처리하고 있으나, LocationEntity.locationType은 non-null이므로 LocationModel.locationType을 non-null String(기본값 "")으로 변경해 .orEmpty 호출을 제거하는 방안을 고려하세요.

gradle/libs.versions.toml (1)

73-74: Mixpanel 버전을 고정해 주세요.

7.+ 와일드카드 버전은 빌드 시점마다 다른 아티팩트를 가져올 수 있어 재현성과 디버깅이 크게 떨어집니다. 안정적인 빌드를 위해 프로젝트에서 검증한 정확한 버전을 명시적으로 지정해 주세요.

Also applies to: 153-155

app/src/main/java/com/spoony/spoony/presentation/explore/type/SortingOption.kt (1)

5-9: trackingCode 도입 적절합니다. 스펙 합의만 확인 부탁드립니다.

  • 영문 고정값으로 i18n 영향 분리된 점 좋습니다.
  • analytics 네이밍 스펙과 값(latest/most_saved) 합의 여부만 확인해 주세요. 또한 stringCode와 역할 구분을 KDoc 한 줄로 남겨두면 혼선이 줄어듭니다.
app/src/main/java/com/spoony/spoony/presentation/gourmet/search/MapSearchScreen.kt (2)

80-80: LocalTracker 제공 보장 확인

CompositionLocal 미제공 시 런타임/프리뷰 크래시 위험 있습니다. 앱 루트(예: MainActivity/Theme)에서 항상 Provider가 깔리는지와 Preview에 대체 Provider가 있는지 확인해 주세요.


173-176: 빈 locationType 처리와 검색어 로깅 정책 확인 제안

  • locationType이 비어있을 때 "" 대신 null/제외 또는 상수 "unknown"으로 정규화하면 분석 품질이 좋아집니다. 스펙상 가능하다면 아래처럼 대체를 권장합니다.
-                                            tracker.mapEvents.mapSearched(
-                                                locationType = locationInfo.locationType.orEmpty(),
-                                                searchTerm = searchKeyword
-                                            )
+                                            tracker.mapEvents.mapSearched(
+                                                locationType = locationInfo.locationType?.takeIf { it.isNotBlank() } ?: "unknown",
+                                                searchTerm = searchKeyword
+                                            )
  • search_term는 사용자 입력 원문입니다. 개인정보/민감정보 취급 정책에 부합하는지(마스킹, 길이 제한 등) 보안/기획과 합의 여부를 확인해 주세요.
app/src/main/java/com/spoony/spoony/presentation/userpage/component/UserScreen.kt (1)

60-74: 프로필 조회 이벤트 발화 시점 적절. 차단 상태/진입 경로 속성 검토 권장

  • profileId 키로 1회 발화되어 중복 방지 OK.
  • isBlocked 프로필을 별도 속성으로 표기할지, 제외할지 제품/분석 스펙 합의가 필요합니다. 또한 진입 경로(entryPoint) 등 추가 속성은 추후 합의 시 확장해 주세요.
app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingStepTwoScreen.kt (1)

71-77: 이벤트는 네비게이션 전에 호출하는 것이 안전합니다.

구성 해제 전 전송 보장을 위해 순서 교체를 권장합니다.

-        onNextButtonClick = {
-            onNextButtonClick()
-            tracker.onboardingEvents.onboard2Completed(
-                isBirthdateEntered = !state.birth.isNullOrBlank(),
-                isActiveRegionEntered = state.region != null
-            )
-        }
+        onNextButtonClick = {
+            tracker.onboardingEvents.onboard2Completed(
+                isBirthdateEntered = !state.birth.isNullOrBlank(),
+                isActiveRegionEntered = state.region != null
+            )
+            onNextButtonClick()
+        }
app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingStepOneScreen.kt (1)

57-60: 네비게이션 전 이벤트 호출 권장

전송 보장을 위해 호출 순서 교체를 제안합니다.

-        onButtonClick = {
-            onNextButtonClick()
-            tracker.onboardingEvents.onboard1Completed()
-        }
+        onButtonClick = {
+            tracker.onboardingEvents.onboard1Completed()
+            onNextButtonClick()
+        }
app/src/main/java/com/spoony/spoony/core/designsystem/component/dialog/SpoonDrawDialog.kt (1)

93-94: 이벤트 로깅 위치를 검토하세요.

SpoonDrawDialogState.RESULT 분기의 컴포저블 본문에서 직접 이벤트를 로깅하고 있습니다. 이는 리컴포지션 시마다 이벤트가 중복으로 전송될 수 있습니다.

LaunchedEffect 블록 내에서 상태 전환 시 한 번만 로깅하는 것이 더 안전합니다.

다음과 같이 수정하는 것을 권장합니다:

+    LaunchedEffect(dialogState) {
+        if (dialogState == SpoonDrawDialogState.RESULT) {
+            tracker.spoonDrawEvents.spoonReceived(drawResult.spoonAmount)
+        }
+    }
+
     when (dialogState) {
         ...
         SpoonDrawDialogState.RESULT -> {
-            tracker.spoonDrawEvents.spoonReceived(drawResult.spoonAmount)
-
             TitleButtonDialog(
app/src/main/java/com/spoony/spoony/core/analytics/events/SpoonDrawEvents.kt (1)

9-18: JSON 문자열 직접 구성 방식을 개선해주세요.

원시 JSON 문자열에 변수를 직접 삽입하는 방식은 유지보수가 어렵고 오류가 발생하기 쉽습니다. spoonCount가 Int이므로 현재는 안전하지만, 이 패턴이 String 타입에 사용될 경우 JSON 주입 위험이 있습니다.

다음과 같이 구조화된 방식으로 개선하는 것을 권장합니다:

-    fun spoonReceived(spoonCount: Int) {
-        tracker.track(
-            eventName = "spoon_received",
-            properties = """
-                {
-                    "spoon_count": $spoonCount
-                }
-            """.trimIndent()
-        )
-    }
+    fun spoonReceived(spoonCount: Int) {
+        val properties = mapOf(
+            "spoon_count" to spoonCount
+        )
+        tracker.track(
+            eventName = "spoon_received",
+            properties = JSONObject(properties).toString()
+        )
+    }

또는 MixPanelTracker.track 메서드가 Map을 직접 받도록 오버로드를 추가하는 것이 더 좋습니다.

app/src/main/java/com/spoony/spoony/core/analytics/events/MypageEvents.kt (1)

10-18: JSONArray 사용은 올바른 접근입니다.

JSONArray를 사용하여 리스트를 직렬화하는 것은 raw string interpolation보다 안전한 방식입니다. 하지만 다른 이벤트 클래스들과 일관성이 부족합니다.

코드베이스 전체에서 일관된 JSON 생성 방식을 사용하는 것을 권장합니다. 모든 이벤트 클래스에서 JSONObject/JSONArray를 사용하거나, MixPanelTracker.track 메서드가 Map을 직접 받도록 개선하는 것이 좋습니다.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f3f3fae and 67682d7.

📒 Files selected for processing (46)
  • app/build.gradle.kts (3 hunks)
  • app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt (1 hunks)
  • app/src/main/java/com/spoony/spoony/core/analytics/events/AnalyticsEvents.kt (1 hunks)
  • app/src/main/java/com/spoony/spoony/core/analytics/events/CommonEvents.kt (1 hunks)
  • app/src/main/java/com/spoony/spoony/core/analytics/events/ExploreEvents.kt (1 hunks)
  • app/src/main/java/com/spoony/spoony/core/analytics/events/MapEvents.kt (1 hunks)
  • app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelEvents.kt (1 hunks)
  • app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelUserProperties.kt (1 hunks)
  • app/src/main/java/com/spoony/spoony/core/analytics/events/MypageEvents.kt (1 hunks)
  • app/src/main/java/com/spoony/spoony/core/analytics/events/OnboardingEvents.kt (1 hunks)
  • app/src/main/java/com/spoony/spoony/core/analytics/events/RegisterEvents.kt (1 hunks)
  • app/src/main/java/com/spoony/spoony/core/analytics/events/ReviewDetailEvents.kt (1 hunks)
  • app/src/main/java/com/spoony/spoony/core/analytics/events/SpoonDrawEvents.kt (1 hunks)
  • app/src/main/java/com/spoony/spoony/core/designsystem/component/dialog/SpoonDrawDialog.kt (3 hunks)
  • app/src/main/java/com/spoony/spoony/data/mapper/LocationMapper.kt (1 hunks)
  • app/src/main/java/com/spoony/spoony/domain/entity/LocationEntity.kt (1 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/MainActivity.kt (1 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingEndScreen.kt (2 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingScreen.kt (4 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingStepOneScreen.kt (3 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingStepThreeScreen.kt (3 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingStepTwoScreen.kt (3 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/auth/signin/SignInScreen.kt (3 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/explore/ExploreScreen.kt (3 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/explore/type/SortingOption.kt (1 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/exploreSearch/ExploreSearchScreen.kt (7 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/exploreSearch/type/SearchType.kt (1 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/follow/FollowRoute.kt (1 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/follow/component/UserListScreen.kt (3 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/gourmet/map/MapScreen.kt (5 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/gourmet/map/model/LocationModel.kt (2 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/gourmet/search/MapSearchScreen.kt (3 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/placeDetail/PlaceDetailRoute.kt (12 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/profileedit/ProfileEditScreen.kt (4 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/profileedit/ProfileEditViewModel.kt (6 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/register/RegisterEndScreen.kt (3 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/register/RegisterScreen.kt (3 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/register/RegisterStartScreen.kt (3 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/splash/SplashScreen.kt (2 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/splash/SplashSideEffect.kt (1 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/splash/SplashViewModel.kt (1 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/splash/model/UserModel.kt (1 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/userpage/component/UserScreen.kt (3 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/userpage/mypage/MyPageRoute.kt (3 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/userpage/otherpage/OtherPageRoute.kt (4 hunks)
  • gradle/libs.versions.toml (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
app/src/main/java/com/spoony/spoony/presentation/exploreSearch/ExploreSearchScreen.kt (1)
app/src/main/java/com/spoony/spoony/presentation/exploreSearch/component/ExploreSearchEmptyScreen.kt (1)
  • ExploreSearchEmptyScreen (22-58)
app/src/main/java/com/spoony/spoony/presentation/placeDetail/PlaceDetailRoute.kt (1)
app/src/main/java/com/spoony/spoony/presentation/placeDetail/component/ScoopDialog.kt (1)
  • ScoopDialog (17-40)
app/src/main/java/com/spoony/spoony/presentation/splash/SplashScreen.kt (1)
app/src/main/java/com/spoony/spoony/presentation/main/MainNavigator.kt (2)
  • navigateToMap (130-150)
  • navigateToSignIn (100-108)
app/src/main/java/com/spoony/spoony/presentation/MainActivity.kt (1)
app/src/main/java/com/spoony/spoony/presentation/main/MainScreen.kt (1)
  • MainScreen (53-289)
⏰ 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: PR Checker
🔇 Additional comments (23)
app/src/main/java/com/spoony/spoony/presentation/follow/FollowRoute.kt (1)

172-173: LGTM!

type 파라미터가 UserListScreen에 올바르게 전달되고 있습니다. 코드 구조가 명확하며 문제가 없습니다.

app/src/main/java/com/spoony/spoony/data/mapper/LocationMapper.kt (1)

9-9: 직접 접근 안전: LocationResponseDto.locationType과 내부 LocationType.locationTypeName은 DTO에서 non-null로 선언되어 있어 NPE 우려가 없습니다.

app/src/main/java/com/spoony/spoony/presentation/MainActivity.kt (2)

19-20: LGTM! 애널리틱스 DI 연동이 적절합니다.

Hilt를 통한 MixPanelEvents 주입이 올바르게 구현되었습니다.


29-31: LGTM! CompositionLocal 제공이 적절합니다.

LocalTracker를 통해 하위 컴포저블에서 애널리틱스 트래커에 접근할 수 있도록 정확하게 구성되었습니다.

app/src/main/java/com/spoony/spoony/presentation/gourmet/map/MapScreen.kt (3)

136-136: LGTM! 트래커 초기화가 적절합니다.

LocalTracker.current를 통한 트래커 접근이 올바르게 구현되었습니다.


181-181: LGTM! 탭 진입 이벤트 로깅이 적절합니다.

기본 장소 목록을 로드하기 전에 "map" 탭 진입을 로깅하는 것이 적절합니다.


484-487: regionFilters에 전달되는 식별자 일관성 검토
MapScreen.kt (484–487)에서 categoryName을 넘기고 있습니다. 애널리틱스 스펙에 따라 categoryId를 사용해야 하는지 확인하고, 모든 화면에서 동일한 식별자를 전달하도록 조정하세요.

app/src/main/java/com/spoony/spoony/presentation/profileedit/ProfileEditScreen.kt (3)

65-66: LGTM! 트래커 초기화가 적절합니다.

화면 수준에서 트래커를 올바르게 초기화했습니다.


131-134: LGTM! 스푼 캐릭터 조회 이벤트 로깅이 적절합니다.

사용자가 프로필 이미지 도움말을 조회할 때 이벤트를 정확하게 추적합니다.


206-211: LGTM! 프로필 업데이트 이벤트 로깅이 적절합니다.

수정된 필드 정보와 함께 프로필 업데이트를 추적하는 것이 적절합니다.

app/src/main/java/com/spoony/spoony/presentation/exploreSearch/ExploreSearchScreen.kt (2)

78-78: LGTM! 트래커 초기화가 적절합니다.

라우트 수준에서 트래커를 올바르게 초기화했습니다.


109-115: LGTM! 탐색 검색 이벤트 로깅이 적절합니다.

검색 실행 후 searchType.trackingCode와 검색어를 포함하여 이벤트를 정확하게 로깅합니다. 기존 검색 로직을 유지하면서 애널리틱스 부가효과만 추가했습니다.

app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingStepThreeScreen.kt (2)

26-26: LGTM! 트래커 초기화가 적절합니다.

온보딩 라우트에서 트래커를 올바르게 초기화했습니다.


35-38: LGTM! 온보딩 완료 이벤트 로깅이 적절합니다.

회원가입 실행과 함께 자기소개 길이를 포함한 온보딩 3단계 완료 이벤트를 정확하게 로깅합니다. 다른 온보딩 단계와 일관된 패턴입니다.

app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingScreen.kt (3)

51-51: LGTM! 트래커 초기화가 적절합니다.

온보딩 화면에서 트래커를 올바르게 초기화했습니다.


84-84: LGTM! 온보딩 2단계 건너뛰기 이벤트 로깅이 적절합니다.

사용자가 2단계를 건너뛸 때 이벤트를 정확하게 추적합니다.


96-96: LGTM! 온보딩 3단계 건너뛰기 이벤트 로깅이 적절합니다.

사용자가 3단계를 건너뛸 때 이벤트를 정확하게 추적합니다.

app/src/main/java/com/spoony/spoony/presentation/splash/SplashSideEffect.kt (1)

3-6: LGTM! 사이드 이펙트 구조가 적절합니다.

Sealed class와 data object를 사용한 타입 안전한 사이드 이펙트 정의가 훌륭합니다. 스플래시 화면의 네비게이션 흐름을 명확하게 표현합니다.

app/src/main/java/com/spoony/spoony/core/designsystem/component/dialog/SpoonDrawDialog.kt (1)

38-38: LGTM! 트래커 초기화가 적절합니다.

다이얼로그 컴포저블 내에서 트래커를 올바르게 초기화했습니다.

app/src/main/java/com/spoony/spoony/presentation/register/RegisterEndScreen.kt (1)

27-27: 분석 이벤트 통합이 올바르게 구현되었습니다.

LocalTracker를 통한 이벤트 추적이 적절히 통합되었으며, 비즈니스 로직 실행 후 이벤트를 기록하는 패턴이 올바릅니다.

Also applies to: 52-52, 72-95

app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelUserProperties.kt (1)

6-11: 래퍼 클래스 구현이 깔끔합니다.

MixPanelTracker를 간결하게 감싼 구조이며, Map<String, Any> 타입을 사용하여 타입 안전성을 제공합니다.

app/src/main/java/com/spoony/spoony/presentation/explore/ExploreScreen.kt (1)

48-48: 탐색 화면에 분석 이벤트가 적절히 통합되었습니다.

LaunchedEffect를 사용한 탭 진입 추적과 정렬 옵션 선택 추적이 올바르게 구현되었습니다.

Also applies to: 93-93, 98-100, 135-138

app/src/main/java/com/spoony/spoony/presentation/userpage/otherpage/OtherPageRoute.kt (1)

14-14: 사용자 페이지 분석 이벤트가 올바르게 구현되었습니다.

팔로우/언팔로우 및 필터 적용 이벤트 추적이 적절하게 통합되었으며, entryPoint 매개변수도 올바르게 전달됩니다.

Also applies to: 39-39, 79-102

Comment on lines +58 to +61
"String",
"MIXPANEL_KEY",
properties["mixpanelDevKey"] as? String ?: ""
)
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

빈 키 fallback이 BuildConfig 생성을 깨뜨립니다.

buildConfigField의 세 번째 인자는 리터럴이어야 하는데, fallback으로 전달한 ""는 따옴표가 없어 public static final String MIXPANEL_KEY = ; 형태로 생성돼 바로 컴파일 에러가 납니다. 최소한 "" 대신 "\"\""을 넘기거나, 값이 없을 때는 명시적으로 에러를 던지도록 처리해 주세요.

             buildConfigField(
                 "String",
                 "MIXPANEL_KEY",
-                properties["mixpanelDevKey"] as? String ?: ""
+                (properties["mixpanelDevKey"] as? String) ?: "\"\""
             )
@@
             buildConfigField(
                 "String",
                 "MIXPANEL_KEY",
-                properties["mixpanelProdKey"] as? String ?: ""
+                (properties["mixpanelProdKey"] as? String) ?: "\"\""
             )

Also applies to: 72-75

🤖 Prompt for AI Agents
In app/build.gradle.kts around lines 58-61 (and similarly lines 72-75), the
third argument to buildConfigField must be a literal Java expression; passing an
unquoted empty string from the Kotlin expression produces invalid generated code
(e.g. public static final String MIXPANEL_KEY = ;). Change the call to provide a
properly quoted string literal when the property is absent (e.g. return "\"\""
for empty) or explicitly throw a Gradle exception when the required property is
missing so buildConfigField always receives a valid literal; apply the same fix
to the other occurrence at lines 72-75.

@Hyobeen-Park Hyobeen-Park marked this pull request as draft October 5, 2025 07:46
…anel

# Conflicts:
#	app/src/main/java/com/spoony/spoony/presentation/explore/ExploreScreen.kt
#	app/src/main/java/com/spoony/spoony/presentation/exploreSearch/ExploreSearchScreen.kt
#	gradle/libs.versions.toml
@Hyobeen-Park Hyobeen-Park marked this pull request as ready for review October 5, 2025 20:00
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: 0

♻️ Duplicate comments (4)
app/src/main/java/com/spoony/spoony/core/analytics/events/CommonEvents.kt (1)

13-219: JSON 직렬화 방식을 안전하게 바꿔주세요

문자열 보간으로 JSON을 만드는 현재 방식은 tabName, placeName, entryPoint 등에 따옴표·줄바꿈 같은 문자가 들어오면 바로 깨진 JSON이 되어 Mixpanel 적재가 실패합니다. 이전 리뷰에서 이미 동일 지적이 있었는데 아직 반영되지 않았습니다. JSONObject(또는 mutableMapOfJSONObject)에 값을 넣은 뒤 toString()으로 넘기도록 전체 이벤트를 교체해 주세요. 예시는 아래와 같습니다.

+import org.json.JSONObject
 ...
-        tracker.track(
-            eventName = "tab_entered",
-            properties = """
-                {
-                    "tab_name": "$tabName"
-                }
-            """.trimIndent()
-        )
+        val properties = JSONObject().apply {
+            put("tab_name", tabName)
+        }
+        tracker.track(
+            eventName = "tab_entered",
+            properties = properties.toString()
+        )

다른 이벤트들도 같은 방식으로 정리해야 안전하게 직렬화됩니다.

app/src/main/java/com/spoony/spoony/presentation/placeDetail/PlaceDetailRoute.kt (1)

143-157: 재구성마다 review_viewed 이벤트가 반복 전송됩니다

Composable 본문에서 바로 tracker.commonEvents.reviewViewed를 호출하고 있어 재구성마다 이벤트가 중복 적재됩니다. 이전 리뷰에서 LaunchedEffect(postId) 등으로 감싸 달라는 피드백이 있었는데 아직 반영되지 않았습니다. 아래처럼 사이드이펙트 스코프로 옮겨 한 번만 호출되도록 수정해 주세요.

-            tracker.commonEvents.reviewViewed(
-                reviewId = postId,
-                ...
-            )
+            LaunchedEffect(postId) {
+                tracker.commonEvents.reviewViewed(
+                    reviewId = postId,
+                    ...
+                )
+            }

postId 외에 의존 값이 필요하면 키에 추가로 넣어주면 됩니다.

app/src/main/java/com/spoony/spoony/core/analytics/events/ReviewDetailEvents.kt (1)

22-175: 이전 리뷰에서 지적된 JSON 직렬화 취약점이 아직 수정되지 않았습니다.

placeName, category 등의 문자열 속성에 따옴표, 백슬래시, 개행 등이 포함될 경우 문자열 보간 방식으로 생성한 JSON이 깨져 Mixpanel로 이벤트가 전송되지 않습니다. 모든 메서드(spoonUseIntent, spoonUsed, placeMapSaved, placeMapRemoved, directionClicked)에서 동일한 문제가 발생합니다.

이전 리뷰에서 제안된 대로 JSONObject 또는 Map 기반 접근으로 안전하게 직렬화하도록 수정해 주세요.

다음 diff를 적용하여 안전한 JSON 빌더를 사용하도록 변경하세요:

 import com.spoony.spoony.core.analytics.MixPanelTracker
 import jakarta.inject.Inject
+import org.json.JSONObject

 class ReviewDetailEvents @Inject constructor(
     private val tracker: MixPanelTracker
 ) {
     fun spoonUseIntent(
         reviewId: Int,
         authorUserId: Int,
         placeName: String,
         category: String,
         menuCount: Int,
         satisfactionScore: Double,
         reviewLength: Int,
         photoCount: Int,
         hasDisappointment: Boolean,
         savedCount: Int,
         isFollowingAuthor: Boolean
     ) {
+        val properties = JSONObject().apply {
+            put("review_id", reviewId)
+            put("author_user_id", authorUserId)
+            put("place_name", placeName)
+            put("category", category)
+            put("menu_count", menuCount)
+            put("satisfaction_score", satisfactionScore)
+            put("review_length", reviewLength)
+            put("photo_count", photoCount)
+            put("has_disappointment", hasDisappointment)
+            put("saved_count", savedCount)
+            put("is_following_author", isFollowingAuthor)
+        }
         tracker.track(
             eventName = "spoon_use_intent",
-            properties = """
-                {
-                    "review_id" : $reviewId,
-                    "author_user_id" : $authorUserId,
-                    "place_name" : "$placeName",
-                    "category" : "$category",
-                    "menu_count" : $menuCount,
-                    "satisfaction_score" : $satisfactionScore,
-                    "review_length" : $reviewLength,
-                    "photo_count" : $photoCount,
-                    "has_disappointment" : $hasDisappointment,
-                    "saved_count" : $savedCount,
-                    "is_following_author" : $isFollowingAuthor
-                }
-            """.trimIndent()
+            properties = properties.toString()
         )
     }

나머지 메서드들(spoonUsed, placeMapSaved, placeMapRemoved, directionClicked)도 동일한 패턴으로 수정 필요합니다.

app/build.gradle.kts (1)

61-64: 빈 문자열 fallback으로 다시 빌드 깨집니다.

buildConfigField에 넘기는 값은 유효한 자바 리터럴이어야 하는데, 현재 분기에서는 키가 없을 때 ""(따옴표 없는 빈 문자열) 그대로 전달하고 있어서 public static final String MIXPANEL_KEY = ; 형태가 생성됩니다. 이전 코멘트와 동일한 문제이므로, 최소한 \"\"로 감싸 주거나 아예 필수 프로퍼티로 강제해 즉시 실패하도록 처리해 주세요. 디버그/릴리스 블록 모두 동일하게 수정이 필요합니다.

-            buildConfigField(
-                "String",
-                "MIXPANEL_KEY",
-                properties["mixpanelDevKey"] as? String ?: ""
-            )
+            val mixpanelDevKey = properties["mixpanelDevKey"] as? String
+                ?: error("`mixpanelDevKey`가 설정돼 있지 않습니다.")
+            buildConfigField("String", "MIXPANEL_KEY", "\"$mixpanelDevKey\"")
@@
-            buildConfigField(
-                "String",
-                "MIXPANEL_KEY",
-                properties["mixpanelProdKey"] as? String ?: ""
-            )
+            val mixpanelProdKey = properties["mixpanelProdKey"] as? String
+                ?: error("`mixpanelProdKey`가 설정돼 있지 않습니다.")
+            buildConfigField("String", "MIXPANEL_KEY", "\"$mixpanelProdKey\"")

Also applies to: 75-78

🧹 Nitpick comments (2)
app/src/main/java/com/spoony/spoony/presentation/register/model/PlaceReviewModel.kt (1)

22-23: 새 필드의 용도를 문서화하세요.

addMapCount 필드가 추가되었지만, 이름만으로는 정확한 용도를 파악하기 어렵습니다. KDoc 주석을 추가하여 이 필드가 무엇을 추적하는지, -1 기본값의 의미(예: 초기화되지 않음)를 명시하는 것을 권장합니다.

예시:

 data class PlaceReviewModel(
     val reviewId: Int,
     val userId: Int,
     val photoUrls: ImmutableList<String>,
     val date: String,
     val menuList: ImmutableList<String>,
     val description: String,
     val value: Double,
     val cons: String?,
     val placeName: String,
     val placeAddress: String,
     val latitude: Double,
     val longitude: Double,
     val category: CategoryState,
+    /** 지도에 추가된 횟수. -1은 초기화되지 않은 상태를 나타냄 */
     val addMapCount: Int
 )
app/src/main/java/com/spoony/spoony/presentation/register/model/RegisterState.kt (1)

27-30: 새 필드에 KDoc 주석 추가 권장

  • userIdaddMapCount 기본값 -1의 의미(초기화되지 않음)를 명시하는 KDoc 주석 추가
  • (선택) analytics/트래킹 로직에서 해당 값이 그대로 전송되지 않도록 처리 여부 검토
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c6de6e6 and 0056ac3.

📒 Files selected for processing (13)
  • .github/workflows/pr_checker.yml (2 hunks)
  • app/build.gradle.kts (3 hunks)
  • app/src/main/java/com/spoony/spoony/core/analytics/events/CommonEvents.kt (1 hunks)
  • app/src/main/java/com/spoony/spoony/core/analytics/events/ReviewDetailEvents.kt (1 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/explore/ExploreScreen.kt (2 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/explore/component/ExploreFilterSection.kt (4 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/exploreSearch/ExploreSearchScreen.kt (7 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/placeDetail/PlaceDetailRoute.kt (12 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/placeDetail/model/PlaceDetailModel.kt (3 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/register/RegisterEndScreen.kt (3 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/register/model/PlaceReviewModel.kt (3 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/register/model/RegisterState.kt (1 hunks)
  • gradle/libs.versions.toml (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • .github/workflows/pr_checker.yml
  • app/src/main/java/com/spoony/spoony/presentation/register/RegisterEndScreen.kt
🧰 Additional context used
🧬 Code graph analysis (2)
app/src/main/java/com/spoony/spoony/presentation/placeDetail/PlaceDetailRoute.kt (1)
app/src/main/java/com/spoony/spoony/presentation/placeDetail/component/ScoopDialog.kt (1)
  • ScoopDialog (17-40)
app/src/main/java/com/spoony/spoony/presentation/exploreSearch/ExploreSearchScreen.kt (1)
app/src/main/java/com/spoony/spoony/presentation/exploreSearch/component/ExploreSearchEmptyScreen.kt (1)
  • ExploreSearchEmptyScreen (22-58)
⏰ 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: PR Checker
🔇 Additional comments (4)
app/src/main/java/com/spoony/spoony/presentation/exploreSearch/ExploreSearchScreen.kt (2)

42-42: LGTM! 애널리틱스 통합 준비가 올바릅니다.

CompositionLocal을 통해 tracker를 정확하게 주입하고 있습니다.

Also applies to: 124-125


173-176: LGTM! 검색 이벤트 트래킹이 잘 구현되었습니다.

검색 액션 시점에 exploreSearched 이벤트를 적절히 로깅하고 있으며, searchType.trackingCodesearchText를 올바르게 전달하고 있습니다.

app/src/main/java/com/spoony/spoony/presentation/register/model/PlaceReviewModel.kt (2)

40-41: LGTM!

addMapCount 필드 매핑이 올바르게 구현되었습니다. null 값에 대한 -1 기본값 처리도 다른 필드들의 패턴과 일관성 있게 적용되었습니다.


61-63: RegisterState 기본값(-1) 처리 검증

  • userId, addMapCount가 RegisterState로 올바르게 복사됨을 확인했습니다.
  • 초기값 -1이 UI나 비즈니스 로직에서 노출되지 않도록 RegisterScreen/ViewModel 단에서 기본값 대체(예: 0) 또는 guard 처리 여부를 검증하세요.

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 (4)
app/src/main/java/com/spoony/spoony/core/analytics/events/MapEvents.kt (1)

1-22: 이전 리뷰 이슈가 해결되었습니다!

이전 리뷰에서 지적된 JSON 직렬화 문제가 JSONObject를 사용하여 안전하게 해결되었습니다. 사용자 입력이 포함될 수 있는 locationTypesearchTerm이 이제 안전하게 이스케이프되어 전송됩니다.

app/src/main/java/com/spoony/spoony/core/analytics/events/ExploreEvents.kt (1)

1-31: 이전 리뷰 이슈가 해결되었습니다!

이전 리뷰에서 지적된 JSON 주입 위험이 JSONObject를 사용하여 안전하게 해결되었습니다. 특히 사용자 입력인 searchTerm이 안전하게 이스케이프되어 전송됩니다.

app/src/main/java/com/spoony/spoony/presentation/placeDetail/PlaceDetailRoute.kt (1)

136-158: 이전 리뷰 이슈가 해결되었습니다!

이전 리뷰에서 지적된 재구성 시 중복 트래킹 문제가 LaunchedEffect를 사용하여 해결되었습니다. state.reviewIduserProfile.userId를 키로 사용하여 필요한 경우에만 이벤트가 발생합니다.

app/src/main/java/com/spoony/spoony/core/analytics/events/ReviewDetailEvents.kt (1)

1-168: 이전 리뷰 이슈가 해결되었습니다!

이전 리뷰에서 지적된 JSON 직렬화 문제가 JSONObject를 사용하여 안전하게 해결되었습니다. 사용자 데이터가 포함된 placeName 등의 속성이 안전하게 이스케이프되어 전송됩니다.

🧹 Nitpick comments (3)
app/src/main/java/com/spoony/spoony/core/analytics/events/ReviewDetailEvents.kt (1)

10-167: 코드 중복을 줄일 수 있습니다.

5개의 메서드(spoonUseIntent, spoonUsed, placeMapSaved, placeMapRemoved, directionClicked)가 동일한 11개의 파라미터와 유사한 JSONObject 빌딩 로직을 공유하고 있습니다. 공통 속성을 data class로 추출하면 유지보수가 쉬워집니다.

다음과 같이 리팩토링을 고려해 보세요:

data class ReviewDetailProperties(
    val reviewId: Int,
    val authorUserId: Int,
    val placeName: String,
    val category: String,
    val menuCount: Int,
    val satisfactionScore: Double,
    val reviewLength: Int,
    val photoCount: Int,
    val hasDisappointment: Boolean,
    val savedCount: Int,
    val isFollowingAuthor: Boolean
) {
    fun toJSONObject() = JSONObject().apply {
        put("review_id", reviewId)
        put("author_user_id", authorUserId)
        put("place_name", placeName)
        put("category", category)
        put("menu_count", menuCount)
        put("satisfaction_score", satisfactionScore)
        put("review_length", reviewLength)
        put("photo_count", photoCount)
        put("has_disappointment", hasDisappointment)
        put("saved_count", savedCount)
        put("is_following_author", isFollowingAuthor)
    }
}

fun spoonUseIntent(properties: ReviewDetailProperties) {
    tracker.track(
        eventName = "spoon_use_intent",
        properties = properties.toJSONObject()
    )
}

이렇게 하면 속성 이름 변경이나 새 속성 추가 시 한 곳만 수정하면 됩니다.

app/src/main/java/com/spoony/spoony/presentation/setting/account/AccountDeleteScreen.kt (1)

52-57: 에러 처리 추가 권장

resetUserProfile()이 실패하더라도 앱 재시작은 진행되어야 할 수 있습니다. 분석 호출이 실패해도 사용자 흐름이 차단되지 않도록 try-catch로 감싸는 것을 고려해주세요.

 LaunchedEffect(viewModel.restartTrigger, lifecycleOwner) {
     viewModel.restartTrigger.flowWithLifecycle(lifecycleOwner.lifecycle).collect { effect ->
-        tracker.userProperties.resetUserProfile()
+        runCatching {
+            tracker.userProperties.resetUserProfile()
+        }
         ProcessPhoenix.triggerRebirth(context)
     }
 }
app/src/main/java/com/spoony/spoony/presentation/setting/account/AccountManagementScreen.kt (1)

43-43: 코드 중복 - 공통 유틸리티 함수 추출 권장

AccountDeleteScreen.kt와 동일한 패턴(LocalTracker 가져오기 + LaunchedEffect에서 resetUserProfile 호출)이 반복됩니다. 재사용 가능한 composable 또는 확장 함수로 추출하면 유지보수성이 향상됩니다.

예시로 공통 함수를 만들 수 있습니다:

// 새 파일: AccountUtils.kt
@Composable
fun rememberAccountResetEffect(
    restartTrigger: Flow<Unit>,
    tracker: MixPanelTracker = LocalTracker.current
) {
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    
    LaunchedEffect(restartTrigger, lifecycleOwner) {
        restartTrigger.flowWithLifecycle(lifecycleOwner.lifecycle).collect {
            runCatching {
                tracker.userProperties.resetUserProfile()
            }
            ProcessPhoenix.triggerRebirth(context)
        }
    }
}

그런 다음 두 화면에서 다음과 같이 사용:

rememberAccountResetEffect(viewModel.restartTrigger)

참고: AccountDeleteScreen.kt의 리뷰 코멘트에서 언급한 에러 처리도 함께 적용되었습니다.

Also applies to: 45-50

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0056ac3 and fbe4eca.

📒 Files selected for processing (15)
  • app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt (1 hunks)
  • app/src/main/java/com/spoony/spoony/core/analytics/events/AnalyticsEvents.kt (1 hunks)
  • app/src/main/java/com/spoony/spoony/core/analytics/events/CommonEvents.kt (1 hunks)
  • app/src/main/java/com/spoony/spoony/core/analytics/events/ExploreEvents.kt (1 hunks)
  • app/src/main/java/com/spoony/spoony/core/analytics/events/MapEvents.kt (1 hunks)
  • app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelUserProperties.kt (1 hunks)
  • app/src/main/java/com/spoony/spoony/core/analytics/events/MypageEvents.kt (1 hunks)
  • app/src/main/java/com/spoony/spoony/core/analytics/events/OnboardingEvents.kt (1 hunks)
  • app/src/main/java/com/spoony/spoony/core/analytics/events/RegisterEvents.kt (1 hunks)
  • app/src/main/java/com/spoony/spoony/core/analytics/events/ReviewDetailEvents.kt (1 hunks)
  • app/src/main/java/com/spoony/spoony/core/analytics/events/SpoonDrawEvents.kt (1 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/placeDetail/PlaceDetailRoute.kt (12 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/setting/account/AccountDeleteScreen.kt (2 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/setting/account/AccountManagementScreen.kt (2 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/userpage/mypage/MyPageRoute.kt (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelUserProperties.kt
  • app/src/main/java/com/spoony/spoony/core/analytics/events/RegisterEvents.kt
  • app/src/main/java/com/spoony/spoony/core/analytics/events/MypageEvents.kt
  • app/src/main/java/com/spoony/spoony/core/analytics/events/CommonEvents.kt
  • app/src/main/java/com/spoony/spoony/core/analytics/events/AnalyticsEvents.kt
  • app/src/main/java/com/spoony/spoony/core/analytics/events/OnboardingEvents.kt
🧰 Additional context used
🧬 Code graph analysis (1)
app/src/main/java/com/spoony/spoony/presentation/placeDetail/PlaceDetailRoute.kt (1)
app/src/main/java/com/spoony/spoony/presentation/placeDetail/component/ScoopDialog.kt (1)
  • ScoopDialog (17-40)
⏰ 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: PR Checker
🔇 Additional comments (5)
app/src/main/java/com/spoony/spoony/core/analytics/events/SpoonDrawEvents.kt (1)

1-18: LGTM!

코드가 간결하고 안전하게 구현되어 있습니다. JSONObject를 사용하여 속성을 안전하게 직렬화하고 있습니다.

app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt (1)

46-53: 에러 처리가 잘 되어 있습니다.

JSON 파싱 실패 시 빈 JSONObject로 안전하게 폴백하고 에러를 로깅하는 방식이 적절합니다. 이벤트 전송 실패를 방지하면서도 디버깅을 위한 정보를 남깁니다.

app/src/main/java/com/spoony/spoony/presentation/placeDetail/PlaceDetailRoute.kt (1)

353-370: 새로운 트래킹 파라미터가 적절하게 추가되었습니다.

trackSpoonUseIntenttrackSpoonUseFailed 파라미터가 PlaceDetailScreen에 추가되어 스푼 사용 의도와 실패를 추적할 수 있게 되었습니다. 이벤트 호출 시점도 적절합니다(Lines 502, 505).

app/src/main/java/com/spoony/spoony/presentation/userpage/mypage/MyPageRoute.kt (2)

53-54: 에러 핸들링이 적절하게 추가되었습니다.

기존 ShowSnackbar 패턴과 일관성 있게 에러 처리가 구현되었습니다.


60-64: 이전 리뷰 이슈가 올바르게 해결되었습니다.

userPageState.userType을 키로 사용하는 별도의 LaunchedEffect로 분리하여, 초기 컴포지션 시점의 스냅샷에만 의존하던 문제가 해결되었습니다. 이제 userTypeMY_PAGE로 변경될 때마다 트래킹 이벤트가 정상적으로 발생합니다.

) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val tracker = LocalTracker.current
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

🧩 Analysis chain

LocalTracker 제공 여부 확인 필요

LocalTracker.current는 컴포지션에서 tracker가 제공되지 않으면 런타임 오류가 발생할 수 있습니다. Preview 함수나 테스트 환경에서 이 화면이 독립적으로 렌더링될 때 문제가 될 수 있습니다.

다음 스크립트로 LocalTracker가 모든 호출 지점에서 제공되는지 확인해주세요:


🏁 Script executed:

#!/bin/bash
# LocalTracker.current를 사용하는 모든 화면이 CompositionLocalProvider로 감싸져 있는지 확인

# AccountDeleteScreen과 AccountManagementScreen이 호출되는 곳 찾기
rg -nP --type=kotlin -C5 'AccountDeleteScreen\s*\(' -g '!**/test/**'
rg -nP --type=kotlin -C5 'AccountManagementScreen\s*\(' -g '!**/test/**'

# LocalTracker를 provide하는 곳 찾기
rg -nP --type=kotlin -C3 'CompositionLocalProvider.*LocalTracker' -g '!**/test/**'

Length of output: 446


LocalTracker.current가 제공되지 않으면 런타임 오류가 발생할 수 있습니다. 다음 스크립트로 .kt 파일 전체에서 사용 위치와 CompositionLocalProvider(LocalTracker) 제공 위치를 재확인해주세요:


🏁 Script executed:

#!/bin/bash
# 모든 .kt 파일에서 LocalTracker.current 사용 위치 확인
rg -n 'LocalTracker\.current' -g '*.kt'

# 모든 .kt 파일에서 CompositionLocalProvider 내 LocalTracker 제공 위치 확인
rg -nP 'CompositionLocalProvider.*LocalTracker' -g '*.kt'

# AccountDeleteScreen 및 AccountManagementScreen 호출 위치 확인
rg -n 'AccountDeleteScreen\(' -g '*.kt'
rg -n 'AccountManagementScreen\(' -g '*.kt'

Length of output: 4005


다음 스크립트로 AccountDeleteScreen.kt 상단(1–200행)에 @Preview 선언이 있는지 확인해주세요:


🏁 Script executed:

#!/bin/bash
sed -n '1,200p' app/src/main/java/com/spoony/spoony/presentation/setting/account/AccountDeleteScreen.kt

Length of output: 5129


AccountDeleteScreenPreview에 LocalTracker 제공 필요
AccountDeleteScreenPreviewCompositionLocalProvider(LocalTracker) 없이 호출되어 런타임 오류가 발생할 수 있습니다. Preview 함수 래핑 시 다음 중 하나를 적용하세요:

  • CompositionLocalProvider(LocalTracker provides mixPanelEvents) { AccountDeleteScreen(...) }
  • LocalTracker에 기본값(예: DefaultTracker) 제공
🤖 Prompt for AI Agents
In
app/src/main/java/com/spoony/spoony/presentation/setting/account/AccountDeleteScreen.kt
around line 50, AccountDeleteScreenPreview calls AccountDeleteScreen without
providing LocalTracker which can cause a runtime error; update the preview to
wrap the composable with CompositionLocalProvider(LocalTracker provides a mock
or real tracker instance) or give LocalTracker a sensible default (e.g.,
DefaultTracker) so the CompositionLocal is always available during previews and
runtime.

Copy link
Member

@Roel4990 Roel4990 left a comment

Choose a reason for hiding this comment

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

와우 고생 많으셨습니다. 하나하나 잘 구조화 하신것 같네요. 한가지 궁금한 부분은 믹스패널 이벤트들을 코어 안에 두셨더라구요. 저라면 사용하는 각 폴더 페이지 안에 events 를 만들어서 넣었을 것 같은데 core 안에 다 두셨네요. 생각해보니 화면별로 만든다면 유지보수가 힘들 것 같아 모으는게 더 좋아보이기도 합니다. 좋은 코드 보고 갑니다.

@Hyobeen-Park
Copy link
Collaborator Author

와우 고생 많으셨습니다. 하나하나 잘 구조화 하신것 같네요. 한가지 궁금한 부분은 믹스패널 이벤트들을 코어 안에 두셨더라구요. 저라면 사용하는 각 폴더 페이지 안에 events 를 만들어서 넣었을 것 같은데 core 안에 다 두셨네요. 생각해보니 화면별로 만든다면 유지보수가 힘들 것 같아 모으는게 더 좋아보이기도 합니다. 좋은 코드 보고 갑니다.

각 폴더 페이지라고 하면 feature쪽 말씀하시는거 맞죠??? 그렇게 할까도 잠깐 고민했었는데 말씀하신대로 이벤트들이 다 흩어져있어서 관리하기가 힘들고 filter_applied처럼 여러 화면에서 트래킹되는 이벤트들도 있어서 이 경우 같은 이벤트를 중복해서 만들어야 하는 문제도 있었어요 그리고 이벤트 트래킹이 서비스의 기능은 아니니까 feature와 아예 분리하는 것이 더 잘 어울린다고 생각했습니다!

Copy link
Member

@angryPodo angryPodo left a comment

Choose a reason for hiding this comment

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

대공사 고생만앗수~~~🚀🚀🚀

Comment on lines 20 to 33
fun reviewViewed(
reviewId: Int,
authorUserId: Int,
placeName: String,
category: String,
menuCount: Int,
satisfactionScore: Double,
reviewLength: Int,
photoCount: Int,
hasDisappointment: Boolean,
savedCount: Int,
isSelfReview: Boolean,
isFollowedUserReview: Boolean,
isSavedReview: Boolean
Copy link
Member

Choose a reason for hiding this comment

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

이런식으로 파라미터가 10개가 넘어가는 경우가 꽤 있어요. 어떤 값이 어떤 파라미터에 해당하는지 한번에 파악하기 어렵다고 생각이 들어요.
data class로 만들어서 객체로 전달하는건 어떻게 생각하십니까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

흠냐냐 사실 이벤트를 위한 모델을 만드는 것에 대해 조금 많이 회의적이긴 한데요ㅎㅎ 겹치는 파라미터가 너무 많기도 하고 해서 일단 가장 많이 겹치는 파라미터만 data class로 만들었습니다!!

Comment on lines 31 to 44
fun track(eventName: String) {
Timber.tag("mixpanel").d(eventName)
mixpanel.track(eventName)
}

fun track(eventName: String, properties: String) {
Timber.tag("mixpanel").d("$eventName $properties")
mixpanel.track(eventName, properties.toJsonObject())
}

fun track(eventName: String, properties: JSONObject) {
Timber.tag("mixpanel").d("$eventName $properties")
mixpanel.track(eventName, properties)
}
Copy link
Member

Choose a reason for hiding this comment

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

현재 Events 클래스들에서 모두 JSONObject를 직접 만들어 track 함수를 호출하고 있어요. String 타입의 properties를 받는 오버로딩은 내부에서 바로 JSONObject로 변환하는데 track(eventName: String, properties: String) 오버로딩을 제거해서 MixPanelTracker의 인터페이스를 더 단순하게 만드는 게 지금 기획의 요구에서 가능할까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

요건 구현 방식을 바꿔서 2개가 생긴건데요 혹시 나중에 쓸일이 있을까 싶어서 놔두긴 했는데 뭐.. 필요한 사람이 다시 만드는걸로 하죠ㅋㅋ

package com.spoony.spoony.core.analytics.events

import com.spoony.spoony.core.analytics.MixPanelTracker
import jakarta.inject.Inject
Copy link
Member

Choose a reason for hiding this comment

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

javax.inject.Inject 이거랑 혼용이 있어요! 하나로 통일해주세요!!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

이벤트 관련 파일들만 javax -> jakarta로 수정할게요~~

placeName: String,
category: String,
menuCount: Int,
satisfactionScore: Double,
Copy link
Member

Choose a reason for hiding this comment

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

이거 Float 타입인것도 있더라구요 (e.g. satisfactionScore: Double) 통일하면 곤란한거일까요??

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

아 이거 왜 이렇게 됐는지 알아버렸어요... 지금 reviewEdited()만 Float이고 나머지는 Double인데요! registerState에 있는 userSatisfactionValue가 Float타입이더라구요😅 명세서 확인해보니 dto도 Double이라 아요에서도 Double로 전달할 것 같아서 요건 형변환해서 보내는걸로 하겠습니다!

fun updateNickname(nickname: String) {
_profileEditModel.update { it.copy(userName = nickname) }
updateSaveButtonState()
_fieldsUpdated.add("nickname")
Copy link
Member

Choose a reason for hiding this comment

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

요거 문자열 Enum으로 하면 어떨까요~?

@Hyobeen-Park Hyobeen-Park merged commit 3da37e3 into develop Oct 16, 2025
2 of 3 checks passed
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: 3

♻️ Duplicate comments (4)
app/src/main/java/com/spoony/spoony/presentation/placeDetail/PlaceDetailRoute.kt (1)

137-161: 재구성 시 중복 트래킹 방지 처리 확인 — 잘 반영됨

LaunchedEffect(state.reviewId, userProfile.userId)review_viewed를 1회성으로 보낸 점 좋습니다. 과거 중복 적재 지적 사항이 해결되었습니다.

app/src/main/java/com/spoony/spoony/core/analytics/events/ReviewDetailEvents.kt (1)

5-5: Hilt 호환성 확인: jakarta.inject.Inject 사용

본 파일도 jakarta.inject.Inject를 사용합니다. 프로젝트의 Hilt/Dagger 버전이 Jakarta 어노테이션을 지원하는지 확인이 필요합니다. 미지원 시 javax.inject.Inject로 교체해 주세요.

app/src/main/java/com/spoony/spoony/core/analytics/events/CommonEvents.kt (2)

5-5: Hilt 호환성 확인: jakarta.inject.Inject 사용

프로젝트 전반의 DI 어노테이션 일관성/호환성 확인이 필요합니다. 미지원 버전이면 javax.inject.Inject로 교체해 주세요.


12-19: 문자열 보간 → JSONObject 전환 잘 하셨습니다

기존 문자열 보간 JSON 문제를 해소했고, 안전한 직렬화로 수정된 점 좋습니다.

🧹 Nitpick comments (8)
app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt (2)

20-25: 첫 로그인 식별 병합(alias) 고려

기존 익명 distinct_id 이벤트를 로그인 후 사용자 ID로 합치려면 최초 로그인 시점에 alias(userId)를 한 번 호출한 뒤 identify(userId)를 사용하는 패턴이 일반적입니다. 사용자 여정 분석 정확도를 높이려면 별도 진입점(예: Auth 성공 시)에서 alias를 처리하는 유틸을 도입해 주세요.


7-7: Hilt v2.57.2은 jakarta.inject.Inject를 지원합니다

  • Dagger/Hilt 2.57.x부터 Jakarta 애노테이션을 공식 지원하므로 jakarta.inject.Inject 사용은 안전합니다.
  • 코드베이스 전반의 일관성을 위해 jakarta.inject.Inject 또는 javax.inject.Inject 중 하나로 통일하세요.
app/src/main/java/com/spoony/spoony/core/analytics/model/ReviewTrackingModel.kt (1)

3-14: 모델 정의 적절 — 공통 필드 캡슐화 LGTM

리뷰 관련 공통 컨텍스트를 한 곳에 모아 재사용성이 좋습니다. 추후 백엔드 ID 스펙이 64비트로 확대될 경우를 대비해 reviewId/authorUserIdLong 전환 가능성만 염두에 두면 좋겠습니다.

app/src/main/java/com/spoony/spoony/presentation/placeDetail/PlaceDetailRoute.kt (2)

168-168: 비지역 return로 화면이 비어질 수 있음

val postId = ... ?: return은 해당 컴포저블 전체를 빠져나가 UI가 렌더링되지 않을 수 있습니다. UI는 유지하되, 포스트 ID가 필요한 액션/이벤트만 가드하는 방식으로 바꾸는 것을 권장합니다.


141-159: ReviewTrackingModel 생성 중복 제거

동일한 모델을 여러 번 구성하고 있어 유지보수/오타 리스크가 큽니다. 성공 분기에서 한 번 계산해 재사용하세요. 또한 (state.reviewId as UiState.Success).data 캐스팅도 반복 대신 postId를 사용하세요.

예시 패턴:

-            val postId = (state.reviewId as? UiState.Success)?.data ?: return
+            val postId = (state.reviewId as? UiState.Success)?.data ?: return
+            val tracking = ReviewTrackingModel(
+                reviewId = postId,
+                authorUserId = userProfile.userId,
+                placeName = uiState.data.placeName,
+                category = uiState.data.category.categoryName,
+                menuCount = uiState.data.menuList.size,
+                satisfactionScore = uiState.data.value,
+                reviewLength = uiState.data.description.length,
+                photoCount = uiState.data.photoUrlList.size,
+                hasDisappointment = uiState.data.cons.isNotEmpty(),
+                savedCount = state.addMapCount
+            )
...
-                        tracker.reviewDetailEvents.spoonUsed(
-                            reviewTrackingModel = ReviewTrackingModel( ... ),
-                            isFollowingAuthor = state.isFollowing
-                        )
+                        tracker.reviewDetailEvents.spoonUsed(
+                            reviewTrackingModel = tracking,
+                            isFollowingAuthor = state.isFollowing
+                        )

위 패턴을 다른 이벤트 호출에도 동일 적용해 주세요.

Also applies to: 176-189, 237-251, 263-276, 281-294, 327-356, 369-383

app/src/main/java/com/spoony/spoony/core/analytics/events/ReviewDetailEvents.kt (1)

15-30: 프로퍼티 빌드 중복 — 헬퍼로 추출

동일 키 매핑이 반복됩니다. 공통 빌더로 추출하면 유지보수가 쉬워집니다.

예시:

 class ReviewDetailEvents @Inject constructor(
     private val tracker: MixPanelTracker
 ) {
+    private fun buildProps(model: ReviewTrackingModel, isFollowingAuthor: Boolean? = null) =
+        JSONObject().apply {
+            put("review_id", model.reviewId)
+            put("author_user_id", model.authorUserId)
+            put("place_name", model.placeName)
+            put("category", model.category)
+            put("menu_count", model.menuCount)
+            put("satisfaction_score", model.satisfactionScore)
+            put("review_length", model.reviewLength)
+            put("photo_count", model.photoCount)
+            put("has_disappointment", model.hasDisappointment)
+            put("saved_count", model.savedCount)
+            if (isFollowingAuthor != null) put("is_following_author", isFollowingAuthor)
+        }
 
     fun spoonUseIntent(reviewTrackingModel: ReviewTrackingModel, isFollowingAuthor: Boolean) {
-        tracker.track(eventName = "spoon_use_intent", properties = JSONObject().apply { ... })
+        tracker.track("spoon_use_intent", buildProps(reviewTrackingModel, isFollowingAuthor))
     }
 
     fun spoonUsed(reviewTrackingModel: ReviewTrackingModel, isFollowingAuthor: Boolean) {
-        tracker.track(eventName = "spoon_used", properties = JSONObject().apply { ... })
+        tracker.track("spoon_used", buildProps(reviewTrackingModel, isFollowingAuthor))
     }
 
     fun placeMapSaved(reviewTrackingModel: ReviewTrackingModel, isFollowingAuthor: Boolean) {
-        tracker.track(eventName = "place_map_saved", properties = JSONObject().apply { ... })
+        tracker.track("place_map_saved", buildProps(reviewTrackingModel, isFollowingAuthor))
     }
 
     fun placeMapRemoved(reviewTrackingModel: ReviewTrackingModel, isFollowingAuthor: Boolean) {
-        tracker.track(eventName = "place_map_removed", properties = JSONObject().apply { ... })
+        tracker.track("place_map_removed", buildProps(reviewTrackingModel, isFollowingAuthor))
     }
 
     fun directionClicked(reviewTrackingModel: ReviewTrackingModel, isFollowingAuthor: Boolean) {
-        tracker.track(eventName = "direction_clicked", properties = JSONObject().apply { ... })
+        tracker.track("direction_clicked", buildProps(reviewTrackingModel, isFollowingAuthor))
     }
 }

이벤트명/키 문자열도 상수로 모듈 내에 모아두면 오타를 방지할 수 있습니다.

Also applies to: 37-52, 63-78, 85-100, 107-122

app/src/main/java/com/spoony/spoony/core/analytics/events/CommonEvents.kt (2)

27-45: ReviewTrackingModel → JSON 매핑 중복 제거

여러 메서드에서 동일 매핑이 반복됩니다. 공통 헬퍼로 추출해 중복을 제거하세요.

예시:

 class CommonEvents @Inject constructor(
     private val tracker: MixPanelTracker
 ) {
+    private fun reviewProps(m: ReviewTrackingModel) = JSONObject().apply {
+        put("review_id", m.reviewId)
+        put("author_user_id", m.authorUserId)
+        put("place_name", m.placeName)
+        put("category", m.category)
+        put("menu_count", m.menuCount)
+        put("satisfaction_score", m.satisfactionScore)
+        put("review_length", m.reviewLength)
+        put("photo_count", m.photoCount)
+        put("has_disappointment", m.hasDisappointment)
+        put("saved_count", m.savedCount)
+    }
 
     fun reviewViewed(reviewTrackingModel: ReviewTrackingModel, isSelfReview: Boolean, isFollowedUserReview: Boolean, isSavedReview: Boolean) {
-        tracker.track(eventName = "review_viewed", properties = JSONObject().apply { ... })
+        val props = reviewProps(reviewTrackingModel).apply {
+            put("is_self_review", isSelfReview)
+            put("is_followed_user_review", isFollowedUserReview)
+            put("is_saved_review", isSavedReview)
+        }
+        tracker.track("review_viewed", props)
     }
 
     fun reviewEdited(reviewTrackingModel: ReviewTrackingModel) {
-        tracker.track(eventName = "review_edited", properties = JSONObject().apply { ... })
+        tracker.track("review_edited", reviewProps(reviewTrackingModel))
     }
 
     fun followUserFromReview(reviewTrackingModel: ReviewTrackingModel) {
-        tracker.track(eventName = "follow_user_from_review", properties = JSONObject().apply { ... })
+        tracker.track("follow_user_from_review", reviewProps(reviewTrackingModel).apply { put("entry_point", "review") })
     }
 
     fun unfollowUserFromReview(reviewTrackingModel: ReviewTrackingModel) {
-        tracker.track(eventName = "unfollow_user_from_review", properties = JSONObject().apply { ... })
+        tracker.track("unfollow_user_from_review", reviewProps(reviewTrackingModel).apply { put("entry_point", "review") })
     }
 }

Also applies to: 50-64, 112-128, 134-149


83-94: entryPoint 문자열 하드코딩 대신 타입화 권장

entryPoint는 오타/불일치가 생기기 쉬우므로 enum class EntryPoint { Review, ... } 등으로 타입을 고정하면 안정적입니다. 직렬화 시 name.lowercase() 등으로 변환하세요.

Also applies to: 96-107

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3842fbc and ec840e5.

📒 Files selected for processing (6)
  • app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt (1 hunks)
  • app/src/main/java/com/spoony/spoony/core/analytics/events/CommonEvents.kt (1 hunks)
  • app/src/main/java/com/spoony/spoony/core/analytics/events/ReviewDetailEvents.kt (1 hunks)
  • app/src/main/java/com/spoony/spoony/core/analytics/model/ReviewTrackingModel.kt (1 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/placeDetail/PlaceDetailRoute.kt (12 hunks)
  • app/src/main/java/com/spoony/spoony/presentation/register/RegisterEndScreen.kt (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/src/main/java/com/spoony/spoony/presentation/placeDetail/PlaceDetailRoute.kt (1)
app/src/main/java/com/spoony/spoony/presentation/placeDetail/component/ScoopDialog.kt (1)
  • ScoopDialog (17-40)
⏰ 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: PR Builder

Comment on lines +31 to +39
fun track(eventName: String) {
Timber.tag("mixpanel").d(eventName)
mixpanel.track(eventName)
}

fun track(eventName: String, properties: JSONObject) {
Timber.tag("mixpanel").d("$eventName $properties")
mixpanel.track(eventName, properties)
}
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

Release 로그에 PII 노출 위험: Timber에 이벤트/프로퍼티 출력

review_id, author_user_id, place_name 등 민감 정보가 릴리스에서도 Logcat으로 노출될 수 있습니다. 디버그에서만 로깅하거나 완전히 제거해 주세요.

아래처럼 디버그 빌드에서만 로깅하도록 가드하는 것을 권장합니다:

 import org.json.JSONObject
 import timber.log.Timber
+import com.spoony.spoony.BuildConfig

 ...
     fun track(eventName: String) {
-        Timber.tag("mixpanel").d(eventName)
+        if (BuildConfig.DEBUG) {
+            Timber.tag("mixpanel").d(eventName)
+        }
         mixpanel.track(eventName)
     }

     fun track(eventName: String, properties: JSONObject) {
-        Timber.tag("mixpanel").d("$eventName $properties")
+        if (BuildConfig.DEBUG) {
+            Timber.tag("mixpanel").d("$eventName $properties")
+        }
         mixpanel.track(eventName, properties)
     }
📝 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
fun track(eventName: String) {
Timber.tag("mixpanel").d(eventName)
mixpanel.track(eventName)
}
fun track(eventName: String, properties: JSONObject) {
Timber.tag("mixpanel").d("$eventName $properties")
mixpanel.track(eventName, properties)
}
import org.json.JSONObject
import timber.log.Timber
import com.spoony.spoony.BuildConfig
// … other imports and class boilerplate …
fun track(eventName: String) {
if (BuildConfig.DEBUG) {
Timber.tag("mixpanel").d(eventName)
}
mixpanel.track(eventName)
}
fun track(eventName: String, properties: JSONObject) {
if (BuildConfig.DEBUG) {
Timber.tag("mixpanel").d("$eventName $properties")
}
mixpanel.track(eventName, properties)
}
🤖 Prompt for AI Agents
In app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt around
lines 31 to 39, the current Timber.debug calls output full event names and
JSONObject properties (which can contain PII) to Logcat; wrap or remove these
logs so they run only in debug builds (e.g., guard with BuildConfig.DEBUG) and
avoid logging raw properties — log only non-sensitive metadata or the event name
(or a sanitized/hashed representation) inside the debug-only guard; apply this
change to both track(eventName: String) and track(eventName: String, properties:
JSONObject) overloads.

Comment on lines +73 to +81
onRegisterPost = {
viewModel.registerPost(it)

tracker.registerEvents.review2Completed(
reviewLength = state.detailReview.length,
photoCount = state.selectedPhotos.size,
hasDisappointment = state.optionalReview.isNotEmpty()
)
},
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

이벤트 타이밍 이슈: 등록 성공 전에 완료 이벤트가 로깅됩니다.

현재 review2Completed 이벤트가 registerPost 호출 직후 바로 로깅되고 있습니다. 하지만 registerPost는 비동기 작업이며, 실제 등록 성공 여부는 success 콜백이 호출될 때 확인됩니다.

등록이 실패하더라도 이벤트가 로깅되어 분석 데이터가 부정확해질 수 있습니다.

다음과 같이 수정하여 실제 성공 시점에 이벤트를 로깅하도록 개선하세요:

 onRegisterPost = {
+    val onSuccessWithTracking = {
+        tracker.registerEvents.review2Completed(
+            reviewLength = state.detailReview.length,
+            photoCount = state.selectedPhotos.size,
+            hasDisappointment = state.optionalReview.isNotEmpty()
+        )
+        it()
+    }
-    viewModel.registerPost(it)
-
-    tracker.registerEvents.review2Completed(
-        reviewLength = state.detailReview.length,
-        photoCount = state.selectedPhotos.size,
-        hasDisappointment = state.optionalReview.isNotEmpty()
-    )
+    viewModel.registerPost(onSuccessWithTracking)
 },
🤖 Prompt for AI Agents
In
app/src/main/java/com/spoony/spoony/presentation/register/RegisterEndScreen.kt
around lines 73 to 81, the review2Completed event is logged immediately after
calling viewModel.registerPost, but registerPost is asynchronous so the event
must only be logged on actual success; update the code to move the
tracker.registerEvents.review2Completed call into the registerPost success
callback (or observe the viewModel's success state/LiveData/Flow and call the
tracker when that indicates success), ensuring failure paths do not emit the
completion event and include the same review data when emitting on success.

Comment on lines +83 to +99
onEditComplete = { postId ->
onEditComplete(postId)
tracker.commonEvents.reviewEdited(
reviewTrackingModel = ReviewTrackingModel(
reviewId = postId,
authorUserId = state.userId,
placeName = state.selectedPlace.placeName,
category = state.selectedCategory.categoryName,
menuCount = state.menuList.size,
satisfactionScore = state.userSatisfactionValue.toDouble(),
reviewLength = state.detailReview.length,
photoCount = state.selectedPhotos.size,
hasDisappointment = state.optionalReview.isNotEmpty(),
savedCount = state.addMapCount
)
)
},
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

이벤트 타이밍 이슈: 수정 성공 확인 전에 편집 이벤트가 로깅됩니다.

현재 reviewEdited 이벤트가 onEditComplete(postId) 호출 직후 바로 로깅되고 있습니다. 백엔드 수정 작업의 성공 여부를 확인하기 전에 이벤트가 기록되어, 실패한 수정도 성공으로 집계될 수 있습니다.

수정 작업의 성공을 확인한 후 이벤트를 로깅하도록 개선하거나, ViewModel에서 수정 성공 시점을 알 수 있도록 구조를 조정하는 것을 권장합니다. 예를 들어:

onEditComplete = { postId ->
    // ViewModel에서 수정 성공 콜백을 제공하거나
    // State로 수정 성공 여부를 관리하여
    // 성공 확인 후 이벤트를 로깅하도록 수정
}

또는 registerPost와 유사하게 success 콜백 패턴을 적용할 수 있습니다.

🤖 Prompt for AI Agents
In
app/src/main/java/com/spoony/spoony/presentation/register/RegisterEndScreen.kt
around lines 83-99, the reviewEdited event is logged immediately after calling
onEditComplete(postId) which means failures can be logged as successes; change
the flow so the event is only emitted after the edit is confirmed successful —
e.g., modify onEditComplete to accept a success callback or a result object (or
have ViewModel expose an editSuccess state) and move the
tracker.commonEvents.reviewEdited call into the success branch (only invoke when
the backend confirms the edit succeeded).

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

Labels

FEAT✨ 새로운 기능 구현 ❤️효빈❤️ ❤️효빈❤️

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] 믹스패널 붙이기

4 participants