Skip to content

Comments

[Feat] 홈 api 구현#88

Merged
t1nm1ksun merged 13 commits intodevelopfrom
feat-home-api
Oct 10, 2025
Merged

[Feat] 홈 api 구현#88
t1nm1ksun merged 13 commits intodevelopfrom
feat-home-api

Conversation

@t1nm1ksun
Copy link
Collaborator

@t1nm1ksun t1nm1ksun commented Sep 17, 2025

Related issue 🛠

Work Description 📝

기존 홈 데이터 API 관련 로직을 최대한 활용해 새로운 홈 API를 연동합니다

  • 홈 데이터 조회 API 구현
  • 제보하기 다이얼로그 구현
  • 전화 신고 기능 추가

Screenshot 📸

Screen_recording_20250917_201332.mp4

Uncompleted Tasks 😅

  • N/A

To Reviewers 📢

아 임시로 하드코딩해둔 카카오아이디 수정해두겠습니다!

Summary by CodeRabbit

  • 신기능
    • 홈 화면 통계 개편: 주/3개월/1년 기간별 지표와 인디케이터 제공.
    • 신고 다이얼로그 추가: 목격 신고, 실종 신고, 전화(120) 바로 연결 지원.
    • 보호/신고 리스트 화면으로 이동 기능 추가.
    • 위치 기반 홈 데이터 지원 및 권한 상태에 따른 안내 문구/목록 표시.
  • 개선
    • 로그인/온보딩 흐름 개선: 카카오 ID 연동, 프로필 이미지 선택 지원, 닉네임 저장·표시.
    • 홈 UI 문구 다국어 문자열 추가 및 게스트 닉네임 기본값 적용.

@coderabbitai
Copy link

coderabbitai bot commented Sep 17, 2025

Walkthrough

홈 API 연동을 중심으로 데이터/도메인/UI 전반을 갱신. 홈 API가 위치 파라미터(lat/lon)를 받도록 확장되고, 응답/매퍼/도메인 모델이 통계(Statistics/PeriodStatistics) 기반으로 재구성. 로컬 기기/닉네임 저장을 위한 UserInfoRepository 계층과 DI 바인딩 추가. 로그인/온보딩/홈 화면 흐름 및 컴포저블, 권한 유틸, 리소스 갱신. 구(legacy) 다이얼로그 제거.

Changes

Cohort / File(s) Summary
Local user info storage
app/src/main/java/com/example/findu/data/datalocal/datasource/DeviceLocalDataSource.kt, .../datasourceimpl/DeviceLocalDataSourceImpl.kt, .../data/repositoryimpl/UserInfoRepositoryImpl.kt, app/src/main/java/com/example/findu/domain/repository/UserInfoRepository.kt
로컬 저장소 인터페이스/구현 추가(deviceId, nickname, clear) 및 도메인 리포지토리/구현 신설. SharedPreferences 기반 저장, clear 시 deviceId 보존.
Home remote API (with location)
.../dataremote/datasource/HomeRemoteDataSource.kt, .../dataremote/datasourceimpl/HomeRemoteDataSourceImpl.kt, .../dataremote/service/HomeService.kt, .../dataremote/util/Constraints.kt
getHome 시그니처를 lat/lon 옵션 파라미터로 확장. Retrofit 경로를 ApiConstraints 상수로 교체하고 @query 추가. HOME 상수 추가.
Home DTO and mapping
.../data/dataremote/model/response/HomeResponseDto.kt, .../data/mapper/todomain/HomeResponseDtoMapper.kt
응답을 statistics/리스트 구조로 개편. 새 타입(Statistics, PeriodStatisticsDto, ProtectingAnimal, WitnessedOrMissingAnimal) 추가 및 매퍼 확장. 구 카드/필드 제거 및 리네이밍 반영.
Domain: Home models/use cases
app/src/main/java/com/example/findu/domain/model/HomeData.kt, .../domain/repository/HomeRepository.kt, .../data/repositoryimpl/HomeRepositoryImpl.kt, .../domain/usecase/GetHomeUseCase.kt
HomeData에 statistics 필드 추가. 저장소/유즈케이스 getHome에 lat/lon 추가. 구 HomeReportData 관련 타입/매핑 제거.
Auth/Login/Signup flow refactor
.../domain/usecase/PostLoginUseCase.kt, .../domain/usecase/PostGuestLoginUseCase.kt, .../domain/usecase/PostSignupUseCase.kt, .../presentation/ui/login/LoginActivity.kt, .../presentation/ui/login/viewmodel/LoginViewModel.kt, .../presentation/ui/onboarding/OnboardingActivity.kt, .../presentation/ui/onboarding/viewmodel/OnboardingViewModel.kt
UserInfoRepository 도입으로 deviceId 내부 획득. 로그인은 kakaoId 기반, 게스트 로그인은 파라미터 제거. 회원가입은 프로필 이미지/기본이미지/닉네임/kakaoId 및 UserInfo 반환으로 변경. 온보딩은 kakaoId 전달/보관 및 Uri 직접 처리, 가입 후 메인 시작 플로우 추가.
User info use cases
.../domain/usecase/GetNicknameUseCase.kt, .../domain/usecase/SetNicknameUseCase.kt, .../domain/usecase/SetDeviceIdUseCase.kt
닉네임/디바이스ID 조회·설정 유즈케이스 추가.
DI modules
app/src/main/java/com/example/findu/di/RepositoryModule.kt, .../di/UseCaseModule.kt
UserInfoRepository 바인딩/주입 추가. 로그인/게스트/회원가입/닉네임 유즈케이스 프로바이더 시그니처 갱신.
Home UI state and flows
.../presentation/ui/home/viewmodel/HomeViewModel.kt, .../presentation/ui/home/HomeFragment.kt
닉네임/위치권한/리포트 다이얼로그 상태 추가. 리스트/디테일 내비게이션 이펙트 개편, 다이얼(전화) 이펙트 추가. 프래그먼트에서 권한 체크 및 새 이펙트 처리.
Home Compose components
.../presentation/ui/home/composeview/HomeScreen.kt, .../presentation/ui/home/component/HomeReportCard.kt, .../presentation/ui/home/component/HomeProtectAnimalList.kt, .../presentation/ui/home/component/HomeReportedAnimalList.kt
통계 기반 UI로 전환(HomeStatistics/PeriodStatistics). 인디케이터 콜백명 변경. 사용자 상태(HomeUserStatusType)별 문구 처리 및 내비게이션 콜백 분리.
New Home report dialog (Compose) & legacy removal
.../presentation/ui/home/component/HomeReportDialog.kt, .../presentation/ui/home/dialog/HomeReportDialog.kt
새 Compose 다이얼로그 추가(목격/실종/전화). 기존 Dialog 클래스 제거.
Permissions and utils
.../presentation/util/permission/LocationPermissionManager.kt, .../presentation/util/Constraints.kt
위치 권한 체크/요청/설정 이동 유틸 추가. GUEST_NAME 상수 추가.
Resources
app/src/main/res/drawable/ic_dialog_home_report_48_*.xml, app/src/main/res/drawable/ic_home_report_dialog_phone_24.xml, app/src/main/res/values/strings.xml
홈 리포트 다이얼로그 아이콘 추가(blue/red/phone). 홈 리스트 타이틀용 문자열 6종 추가.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant HomeFragment
  participant HomeViewModel as VM
  participant HomeRepository as Repo
  participant HomeRemote as Remote
  participant HomeService as Retrofit

  rect rgba(230,240,255,0.4)
  note over HomeFragment,VM: 홈 진입/갱신
  User->>HomeFragment: 화면 표시
  HomeFragment->>HomeFragment: hasLocationPermission()
  HomeFragment->>VM: SetLocationPermission(permission)
  VM->>Repo: getHome(lat?, lon?)
  Repo->>Remote: getHome(lat?, lon?)
  Remote->>HomeService: GET /{API}/{VERSION}/home?lat&lon
  HomeService-->>Remote: BaseResponse<HomeResponseDto>
  Remote-->>Repo: BaseResponse
  Repo-->>VM: Result<HomeData(statistics,... )>
  VM-->>HomeFragment: uiState 갱신
  end

  rect rgba(255,245,230,0.4)
  note over User,HomeFragment: 제보하기 다이얼로그
  User->>HomeFragment: 제보 버튼 클릭
  HomeFragment->>VM: OnReportDialogClick
  VM-->>HomeFragment: isReportDialogVisible = true
  User->>HomeFragment: 전화로 신고 클릭
  HomeFragment->>VM: dial()
  VM-->>HomeFragment: Effect.Dial
  HomeFragment->>HomeFragment: call120() (ACTION_DIAL)
  end
Loading
sequenceDiagram
  autonumber
  actor User as KakaoUser
  participant LoginActivity as Login
  participant LoginVM as VM
  participant Auth as AuthRepo
  participant UserInfo as UserInfoRepo
  participant Onboarding as OnboardingVM

  KakaoUser->>Login: 카카오 로그인 완료(kakaoId)
  Login->>VM: postLogin(kakaoId)
  VM->>Auth: postLogin(LoginInfo(kakaoId, deviceId=UserInfo.getDeviceId()))
  alt 첫 로그인
    VM-->>Login: startOnboardingActivity(kakaoId)
    Login->>Onboarding: setKakaoId(kakaoId)
    Onboarding->>Auth: postSignup(profileImage?, defaultImage?, nickname, kakaoId, deviceId=UserInfo.getDeviceId())
    Onboarding-->>Login: startMainActivity
  else 재로그인
    VM->>UserInfo: setNickname(nickname)
    VM-->>Login: startMainActivity
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • ikseong00
  • nasohee
  • MinseoSONG

Poem

새벽 좌표 lat·lon, 바람결에 실려오고,
통계는 일주·분기·일년을 조곤조곤 펼치네.
닉네임 저장, 기기 ID는 살짝 품고,
카카오는 태그만 쥐고 문을 연다.
탭 한 번, 링 한 번—☎️ 120로 이어지는 길,
파랑·빨강 아이콘 아래 오늘 홈은 완성!

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning 이 PR에는 홈 API 연동 이외에도 DeviceLocalDataSource, UserInfoRepository, 로그인 및 온보딩 흐름 변경, 권한 관리, 다이얼로그 컴포넌트 등 범위를 벗어난 다양한 기능이 함께 포함되어 있어 단일 목적 PR로 보기에는 변경 범위가 과도합니다. 홈 API 연동과 관련 없는 로그인·온보딩, 사용자 정보 저장, 권한 관리 등의 변경은 별도 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 제목 '[Feat] 홈 api 구현'은 이 PR의 핵심 변경 사항인 홈 API 연동을 간결하고 명확하게 요약하고 있어 팀원이 히스토리를 훑을 때 주요 기능을 쉽게 파악할 수 있습니다.
Linked Issues Check ✅ Passed 제공된 #83 이슈의 홈 화면 데이터 조회 API 연동 목적에 따라 Remote 데이터 소스와 서비스, DTO, 매퍼, 레포지토리, 도메인 모델, UseCase, 리파지토리 구현 등 일관된 층간 변경이 이루어져 있어 이슈 요구사항을 충족합니다.
Description Check ✅ Passed PR 설명은 관련 이슈 번호, 작업 내용, 스크린샷 링크, 미완료 태스크, 리뷰어 안내 등 템플릿의 모든 필수 섹션이 명확히 채워져 있어 요구되는 정보를 충분히 제공합니다.
✨ 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-home-api

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

Caution

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

⚠️ Outside diff range comments (1)
app/src/main/java/com/example/findu/presentation/ui/login/viewmodel/LoginViewModel.kt (1)

29-35: NPE 위험: userInfo!! 강제 언래핑 제거

서버 응답 계약이 깨질 경우 크래시 납니다. 토큰 존재 여부를 체크하고 없으면 온보딩/재로그인 등으로 우회하는 편이 안전합니다.

-            loginUseCase.postLogin(kakaoId = kakaoId).onSuccess { loginData ->
-                if (loginData.isFirstLogin) {
-                    startOnboardingActivity()
-                } else {
-                    setAccessTokenUseCase(accessToken = loginData.userInfo!!.accessToken)
-                    startMainActivity()
-                }
-            }
+            loginUseCase.postLogin(kakaoId = kakaoId).onSuccess { loginData ->
+                val token = loginData.userInfo?.accessToken
+                when {
+                    loginData.isFirstLogin -> {
+                        startOnboardingActivity()
+                    }
+                    token != null -> {
+                        setAccessTokenUseCase(accessToken = token)
+                        startMainActivity()
+                    }
+                    else -> {
+                        Log.e("http", "Missing access token for non-first login")
+                        // TODO: 토큰 없음 처리(재시도/재로그인 유도 등) 결정
+                    }
+                }
+            }

온보딩(firstLogin) 경로에서도 토큰이 필요한지(예: API 호출) 확인 부탁드립니다. 필요하다면 해당 경로에서도 토큰 세팅이 맞는지 검토 필요해요.

🧹 Nitpick comments (59)
app/src/main/res/drawable/ic_dialog_home_report_48_red.xml (1)

1-21: clip-path와 뷰포트 밖 좌표가 많아 렌더링 비용이 약간 증가할 수 있어요

가능하면 불필요한 clip-path 제거 및 path 단순화(뷰포트 내 좌표로 정규화)로 드로어블 경량화를 고려해 주세요. 저사양 기기에서 다이얼로그 표시 애니메이션 시 미세한 이득이 있습니다.

app/src/main/java/com/example/findu/domain/model/AuthData.kt (1)

3-6: LoginInfo.deviceId의 빈 문자열 처리 계약 명확화 필요

deviceId: String이 빈 문자열을 허용하는지 도메인 계약을 명시해 주세요. 비허용이라면 require(deviceId.isNotBlank()) 또는 @JvmInline value class DeviceId로 모델링을 권장합니다.

app/src/main/res/drawable/ic_dialog_home_report_48_blue.xml (1)

1-20: 벡터 리소스 최적화(레드 아이콘과 동일 의견)

clip-path 제거 및 path 단순화로 약간의 렌더링 비용을 줄일 수 있습니다. 두 컬러 아이콘을 동일한 구조로 유지하면 유지보수에도 유리합니다.

app/src/main/java/com/example/findu/data/datalocal/datasource/DeviceLocalDataSource.kt (1)

1-6: clear()의 의미가 광범위 — 키 단위 API로 축소 권장

현 구현은 prefs 전체를 clear()합니다. 장래 확장성을 위해 clearDeviceId()로 의미를 좁히거나, 구현에서 remove(DEVICE_ID)만 수행하도록 합시다. 또한 빈 문자열을 초기값으로 쓸지(null 사용)도 인터페이스 수준에서 명확히 해두면 좋아요.

app/src/main/res/drawable/ic_home_report_dialog_phone_24.xml (1)

1-16: 경량화 소소 팁

clip-path 제거 및 path 단순화 여지도 있어 보입니다. 다이얼로그 오픈/닫힘 시 미세한 성능 여유를 줄 수 있습니다. 기능적 문제는 없습니다.

app/src/main/java/com/example/findu/presentation/type/DefaultProfileType.kt (1)

3-8: enum 프로퍼티 네이밍과 불필요한 세미콜론 정리 제안

  • string 보다는 value/key처럼 의미 있는 이름이 읽기 좋아요.
  • enum 항목 뒤의 ;는 추가 멤버가 없으면 불필요합니다.
-enum class DefaultProfileType(val string: String) {
+enum class DefaultProfileType(val value: String) {
     DEFAULT("default"),
     PUPPY("puppy"),
     CHICK("chick"),
-    PANDA("panda");
+    PANDA("panda")
 }
app/src/main/java/com/example/findu/domain/repository/DeviceRepository.kt (1)

3-7: 도메인 계약의 의도 명확화(속성 vs 함수, null 처리)

  • Kotlin 관용적으로 get/set 대신 var deviceId: String? 속성 형태를 고려해 보세요. 저장소에 값이 없을 때 빈 문자열("")보다 null이 의도를 더 분명히 드러냅니다.
  • 삭제 동작도 clear() 대신 clearDeviceId()가 더 구체적입니다.

저장/조회 호출부가 빈 문자열을 “미설정”으로 가정하지 않는지 확인 부탁드립니다.

app/src/main/java/com/example/findu/di/SharedPreferencesModule.kt (1)

17-23: 프로바이더 명확화(함수명)와 상수 추출

  • provideSharedPreferencesprovideTokenSharedPreferences로 이름을 구체화하면 의도가 더 분명합니다.
  • "token_prefs" 문자열은 상수로 추출해 재사용 시 오타를 줄일 수 있어요.
 @TokenPrefs
 @Provides
 @Singleton
-fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences =
-    context.getSharedPreferences("token_prefs", Context.MODE_PRIVATE)
+fun provideTokenSharedPreferences(@ApplicationContext context: Context): SharedPreferences =
+    context.getSharedPreferences(TOKEN_PREFS, Context.MODE_PRIVATE)

추가(파일 상단 또는 object 내부):

private const val TOKEN_PREFS = "token_prefs"
app/src/main/java/com/example/findu/data/dataremote/model/response/CheckEmailResponseDto.kt (1)

7-10: 닉네임 중복 응답 DTO 전환 깔끔해요

이름/필드가 서버 스펙과 일치하면 그대로 가도 좋습니다. @SerialName("isDuplicate")는 프로퍼티명과 동일하므로 유지/제거는 팀 규칙에 맞춰 결정하세요.

app/src/main/java/com/example/findu/presentation/ui/home/component/HomeReportDialog.kt (4)

55-58: 문자열 하드코딩 제거(i18n)

"제보하기"는 string 리소스로 이동하는 게 좋아요.

-                Text(
-                    text = "제보하기",
+                Text(
+                    text = stringResource(R.string.home_report_title),
                     style = FindUTheme.typography.head1SemiBold24,
                     color = FindUTheme.colors.gray6
                 )

필요 import:

import androidx.compose.ui.res.stringResource

63-78: 접근성: 버튼 역할/라벨 추가 + i18n

  • 클릭 가능한 Column에 접근성 역할(Role.Button) 부여.
  • 아이콘/텍스트에 string 리소스 사용과 contentDescription 추가.
-                Column(
-                    modifier = Modifier.noRippleClickable(onFindReportButtonClicked),
+                Column(
+                    modifier = Modifier
+                        .noRippleClickable(onFindReportButtonClicked)
+                        .semantics { role = Role.Button },
                     horizontalAlignment = Alignment.CenterHorizontally
                 ) {
                     BaseVectorIcon(
                         modifier = Modifier
                             .roundedBackgroundWithPadding(
                                 backgroundColor = FindUTheme.colors.blue2,
                                 padding = PaddingValues(15.dp),
                                 cornerRadius = 10.dp
                             ),
-                        vectorResource = R.drawable.ic_dialog_home_report_48_blue
+                        vectorResource = R.drawable.ic_dialog_home_report_48_blue,
+                        contentDescription = stringResource(R.string.home_report_witness)
                     )
                     Spacer(modifier = Modifier.height(10.dp))
-
-                    Text(text = "목격신고", style = FindUTheme.typography.body1SemiBold16, color = FindUTheme.colors.gray6)
+                    Text(
+                        text = stringResource(R.string.home_report_witness),
+                        style = FindUTheme.typography.body1SemiBold16,
+                        color = FindUTheme.colors.gray6
+                    )
                 }

필요 import:

import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.semantics

82-97: 접근성: 두 번째 액션도 동일하게 보완

위와 동일한 개선을 "실종신고"에도 적용합니다.

-                Column(
-                    modifier = Modifier.noRippleClickable(onLostReportButtonClicked),
+                Column(
+                    modifier = Modifier
+                        .noRippleClickable(onLostReportButtonClicked)
+                        .semantics { role = Role.Button },
                     horizontalAlignment = Alignment.CenterHorizontally
                 ) {
                     BaseVectorIcon(
                         modifier = Modifier
                             .roundedBackgroundWithPadding(
                                 backgroundColor = FindUTheme.colors.red2,
                                 padding = PaddingValues(15.dp),
                                 cornerRadius = 10.dp
                             ),
-                        vectorResource = R.drawable.ic_dialog_home_report_48_red
+                        vectorResource = R.drawable.ic_dialog_home_report_48_red,
+                        contentDescription = stringResource(R.string.home_report_missing)
                     )
                     Spacer(modifier = Modifier.height(10.dp))
-
-                    Text(text = "실종신고", style = FindUTheme.typography.body1SemiBold16, color = FindUTheme.colors.gray6)
+                    Text(
+                        text = stringResource(R.string.home_report_missing),
+                        style = FindUTheme.typography.body1SemiBold16,
+                        color = FindUTheme.colors.gray6
+                    )
                 }

102-121: 클릭 타깃 확장 및 i18n/라벨 추가

  • 전화 영역은 Text만 클릭 가능 → Row 전체를 클릭 가능하게 바꿔 터치 영역을 넉넉히.
  • 아이콘/텍스트 모두 string 리소스 사용 및 아이콘 라벨 추가.
-            Row(
-                modifier = Modifier
-                    .fillMaxWidth()
-                    .padding(horizontal = 20.dp),
+            Row(
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .padding(horizontal = 20.dp)
+                    .noRippleClickable(onPhoneClicked)
+                    .semantics { role = Role.Button },
                 verticalAlignment = Alignment.CenterVertically
             ) {
                 Spacer(modifier = Modifier.weight(1f))
                 BaseVectorIcon(
-                    vectorResource = R.drawable.ic_home_report_dialog_phone_24
+                    vectorResource = R.drawable.ic_home_report_dialog_phone_24,
+                    contentDescription = stringResource(R.string.home_report_call)
                 )
                 Spacer(modifier = Modifier.width(5.dp))
                 Text(
-                    text = "전화로 신고하기",
+                    text = stringResource(R.string.home_report_call),
                     style = FindUTheme.typography.tag1SemiBold12.copy(
                         textDecoration = TextDecoration.Underline
                     ),
-                    color = FindUTheme.colors.gray5,
-                    modifier = Modifier.noRippleClickable(onPhoneClicked)
+                    color = FindUTheme.colors.gray5
                 )
             }

추가 string 리소스 예시:

<string name="home_report_title">제보하기</string>
<string name="home_report_witness">목격신고</string>
<string name="home_report_missing">실종신고</string>
<string name="home_report_call">전화로 신고하기</string>
app/src/main/java/com/example/findu/data/mapper/torequest/TextRequestBodyMapper.kt (2)

10-11: 고정 미디어 타입은 nullable 변환 불필요

"text/plain"은 유효하므로 toMediaType() 사용으로 널 제거하면 안전성이 올라갑니다.

-fun String.toPlainTextRequestBody(): RequestBody =
-    toRequestBody("text/plain".toMediaTypeOrNull())
+fun String.toPlainTextRequestBody(): RequestBody =
+    toRequestBody("text/plain".toMediaType())

13-18: 이미지 MIME 타입 명시 가능하도록 확장

서버가 구체적 MIME(image/png 등)을 요구할 수 있어요. 기본값은 유지하되 전달 가능하도록 확장하면 재사용성이 좋아집니다.

서버가 image/*를 허용하는지 API 스펙 확인 부탁드려요.

-fun File.toImageMultipart(name: String): MultipartBody.Part =
-    MultipartBody.Part.createFormData(
-        name,
-        this.name,
-        asRequestBody("image/*".toMediaTypeOrNull())
-    )
+fun File.toImageMultipart(
+    name: String,
+    mimeType: String = "image/*"
+): MultipartBody.Part =
+    MultipartBody.Part.createFormData(
+        name = name,
+        filename = this.name,
+        body = asRequestBody(mimeType.toMediaType())
+    )
app/src/main/java/com/example/findu/di/DataSourceModule.kt (1)

3-3: DeviceLocalDataSource 바인딩 추가 OK, 네이밍 살짝 제안

기능은 문제 없습니다. 다른 바인딩과 통일성을 위해 메서드명을 bindsDeviceLocalDataSource로 바꾸는 걸 고려해 주세요.

-abstract fun bindsDeviceDataSource(deviceLocalDataSourceImpl: DeviceLocalDataSourceImpl): DeviceLocalDataSource
+abstract fun bindsDeviceLocalDataSource(deviceLocalDataSourceImpl: DeviceLocalDataSourceImpl): DeviceLocalDataSource

Also applies to: 6-6, 94-96

app/src/main/java/com/example/findu/presentation/ui/main/MainActivity.kt (1)

26-30: ANDROID_ID null 대체값 개선 및 동기 여부 확인

  • "unknown_device_id" 저장은 장기적으로 식별 불가 상태를 고착화합니다. UUID로 대체 생성 권장.
  • SetDeviceIdUseCase가 suspend라면 현재 호출은 컴파일/런타임 문제를 야기합니다. 동기 여부 확인 부탁드립니다.
+import java.util.UUID
@@
-        val deviceId = Settings.Secure.getString(this.contentResolver, Settings.Secure.ANDROID_ID)
-            ?: "unknown_device_id"
+        val deviceId = Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID)
+            ?: UUID.randomUUID().toString()
@@
-        setDeviceIdUseCase(deviceId=deviceId)
+        setDeviceIdUseCase(deviceId = deviceId) // suspend가 아니라는 전제. suspend면 lifecycleScope.launch { ... }

또한 기존에 저장된 값이 있으면 재설정하지 않도록 리포지토리에서 id 존재 체크 후 set을 생략하는 방식을 고려해 주세요.

app/src/main/java/com/example/findu/domain/repository/HomeRepository.kt (1)

6-9: lat/lon 전달 규칙 명시/강제 필요

한쪽만 null인 상태는 서버 오류 가능성이 있습니다. “둘 다 null 또는 둘 다 non-null”을 상위 UseCase나 Impl에서 require로 강제하는 걸 권장합니다. 기본값 제공도 고려해 보세요.

예: Impl에서

require((lat == null) == (lon == null)) { "lat/lon must be provided together or not at all" }
app/src/main/java/com/example/findu/data/repositoryimpl/HomeRepositoryImpl.kt (1)

13-17: lat/lon은 쌍으로만 받도록 사전 검증 + 사소한 스타일 정리

한쪽만 전달되면 서버 쿼리 의미가 애매해집니다. 둘 다 null이거나 둘 다 non‑null인 경우만 허용하도록 가드하면 오용을 줄일 수 있어요. 또한 named argument와 공백 정리 제안드립니다.

-    override suspend fun getHome( lat: Double?,
-                                  lon: Double?): Result<HomeData> =
-        runCatching {
-            homeRemoteDataSource.getHome(lat,lon).handleBaseResponse().getOrThrow().toDomain()
-        }
+    override suspend fun getHome(lat: Double?, lon: Double?): Result<HomeData> {
+        require((lat == null) == (lon == null)) { "lat/lon must be both null or both non-null" }
+        return runCatching {
+            homeRemoteDataSource
+                .getHome(lat = lat, lon = lon)
+                .handleBaseResponse()
+                .getOrThrow()
+                .toDomain()
+        }
+    }
app/src/main/java/com/example/findu/domain/usecase/SetDeviceIdUseCase.kt (1)

7-12: 빈 deviceId 저장 방지 가드 추가 제안

공백/빈 문자열이 저장되면 이후 로그인 플로우가 실패할 수 있어요. 간단한 require로 방지해주세요.

 class SetDeviceIdUseCase @Inject constructor(
     private val deviceRepository: DeviceRepository
 ) {
-    operator fun invoke(deviceId: String) = deviceRepository.setDeviceId(deviceId = deviceId)
+    operator fun invoke(deviceId: String) {
+        require(deviceId.isNotBlank()) { "deviceId must not be blank" }
+        deviceRepository.setDeviceId(deviceId = deviceId)
+    }
 }
app/src/main/java/com/example/findu/data/dataremote/datasource/HomeRemoteDataSource.kt (1)

7-10: 기본값 제거로 부분 전달 오용 방지 + 계약 명시(KDoc) 제안

현재 기본값(null) 때문에 호출 측에서 lat만 주거나 lon만 주는 실수가 생길 수 있습니다. 기본값을 제거하고 “둘 다 제공 또는 둘 다 생략”을 인터페이스 계약으로 명시하면 안전성이 올라갑니다.

 interface HomeRemoteDataSource {
-    suspend fun getHome(
-        lat: Double? = null,
-        lon: Double? = null
-    ): BaseResponse<HomeResponseDto>
+    /**
+     * 홈 데이터 조회.
+     * - 좌표 필터링이 필요 없으면 lat/lon 모두 null
+     * - 좌표 필터링이 필요하면 lat/lon 모두 non-null
+     */
+    suspend fun getHome(
+        lat: Double?,
+        lon: Double?
+    ): BaseResponse<HomeResponseDto>
 }

호출부에서 한쪽만 넘기는 호출이 없는지 한 번만 확인 부탁드립니다.

app/src/main/java/com/example/findu/presentation/ui/login/viewmodel/LoginViewModel.kt (1)

36-38: 에러 로깅 레벨 조정

실패 케이스는 Log.e로 남기는 게 탐색 및 크래시 분석에 유리합니다.

-            }.onFailure { e ->
-                Log.d("http", "Error Message: : $e")
-            }
+            }.onFailure { e ->
+                Log.e("http", "Error Message: $e", e)
+            }
...
-                .onFailure { e ->
-                    Log.d("http", "Error Message: : $e")
-                }
+                .onFailure { e ->
+                    Log.e("http", "Error Message: $e", e)
+                }

Also applies to: 51-53

app/src/main/java/com/example/findu/presentation/ui/onboarding/OnboardingActivity.kt (4)

36-37: Uri 직접 전달 전환은 굿. 다만 “장기 보관” 필요 시 접근 권한/선택 계약 재검토 필요

GetContent는 퍼시스터블 권한을 주지 않아서 프로세스 재시작 후 동일 Uri 접근이 필요하면 실패할 수 있어요. 요구사항이 “온보딩 세션 내에서만 사용”이 아니라면 다음 중 하나로 바꾸는 걸 추천합니다:

  • OpenDocument + takePersistableUriPermission
  • 선택만 필요하면 PickVisualMedia 사용

참고 적용 예(선택지 2):

- val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
+ val launcher = rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri ->
    uri?.let {
      onboardingViewModel.setProfileImage(it)
    }
  }
...
- cameraIconClicked = {launcher.launch(MIME_TYPE_IMAGE)},
+ cameraIconClicked = { launcher.launch(ActivityResultContracts.PickVisualMedia.ImageOnly) },

온보딩 이후(앱 재실행/프로세스 종료)에도 동일 이미지를 재표시해야 하나요? 필요하면 위 변경 또는 내부 저장소로 사본 저장이 필요합니다.

Also applies to: 52-53


33-34: collectAsStateWithLifecycle로 수명 주기 안전 수집 권장

Compose에서 Flow 수집은 lifecycle-aware 버전을 쓰는 게 안전합니다.

- val uiState by onboardingViewModel.uiState.collectAsState()
+ val uiState by onboardingViewModel.uiState.collectAsStateWithLifecycle()

필요 import: androidx.lifecycle.compose.collectAsStateWithLifecycle


60-61: 메인 진입 시 백스택 정리로 뒤로가기 이슈 예방

finish() 대신 Intent 플래그로 백스택 제거하면 더 일관적입니다.

- startActivity(Intent(this@OnboardingActivity, MainActivity::class.java))
- finish()
+ startActivity(
+   Intent(this@OnboardingActivity, MainActivity::class.java)
+     .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
+ )

23-26: 미사용 TAG 정리

TAG가 사용되지 않습니다. 사용 계획이 없으면 제거해 주세요.

app/src/main/java/com/example/findu/data/dataremote/service/HomeService.kt (1)

12-16: Retrofit 경로 앞의 슬래시는 Base URL 경로 세그먼트를 무시합니다

@GET("/...")는 baseUrl의 path 세그먼트를 리셋합니다. baseUrl에 경로 프리픽스가 있다면 의도와 다를 수 있어요. 보통 선행 슬래시 없이 작성합니다.

- @GET("/$API/$VERSION/$HOME")
+ @GET("$API/$VERSION/$HOME")

기타 lat/lon 옵셔널 쿼리 설계는 적절합니다.

app/src/main/java/com/example/findu/domain/usecase/GetHomeUseCase.kt (1)

9-12: 시그니처 포맷팅 + 불필요한 map 제거로 간소화

동일 결과이므로 단순 위임이 더 깔끔합니다.

- suspend operator fun invoke(lat: Double?=null,lon:Double?=null): Result<HomeData> =
-     homeRepository.getHome(lat,lon).map { homeData ->
-         homeData
-     }
+ suspend operator fun invoke(
+     lat: Double? = null,
+     lon: Double? = null
+ ): Result<HomeData> = homeRepository.getHome(lat, lon)
app/src/main/java/com/example/findu/domain/usecase/PostLoginUseCase.kt (1)

12-19: deviceId 공란 가드 및 네이밍 일관성

  • 동일 레이어의 다른 유즈케이스와 맞추려면 operator fun invoke 네이밍도 고려해 주세요.
  • deviceId가 비어 있을 가능성(초기화 실패 등)에 대한 가드를 권장합니다.
- suspend fun postLogin(
-     kakaoId:Long
- ): Result<LoginData> = authRepository.postLogin(
+ suspend fun postLogin(
+     kakaoId: Long
+ ): Result<LoginData> {
+   val deviceId = deviceRepository.getDeviceId()
+   require(deviceId.isNotBlank()) { "deviceId is blank; ensure it is initialized before login" }
+   return authRepository.postLogin(
      loginInfo = LoginInfo(
        kakaoId = kakaoId,
-       deviceId = deviceRepository.getDeviceId()
+       deviceId = deviceId
      )
-   )
+   )
+ }

해당 가드가 실제로 트리거될 수 있는지(초기 진입 플로우에서 deviceId 세팅 시점) 한 번만 확인 부탁드립니다.

app/src/main/java/com/example/findu/data/repositoryimpl/DeviceRepositoryImpl.kt (1)

7-17: Hilt 스코프 지정으로 싱글턴 보장

DeviceRepositoryImpl은 로컬 단말 식별자 소스로 동작하므로 싱글턴이 자연스러워 보입니다.

+ import javax.inject.Singleton
...
- class DeviceRepositoryImpl @Inject constructor(
+ @Singleton
+ class DeviceRepositoryImpl @Inject constructor(

DI 모듈에서 이미 싱글턴 바인딩이라면 중복 지정은 무해합니다.

app/src/main/java/com/example/findu/domain/repository/AuthRepository.kt (2)

18-21: 불리언 반환 의미를 명확히(가용/중복 여부) — 네이밍 또는 KDoc 권장

Result<Boolean>true가 “사용 가능”인지 “중복”인지 혼동 소지가 있습니다. 메서드명을 checkNicknameAvailable로 하거나 KDoc에 의미를 명시해 주세요.

UI/VM 쪽에서 해석이 뒤집히지 않았는지 한번만 점검 부탁드립니다.


22-29: 도메인 경계에 java.io.File 침투 — 전송 매체 추상화 권장

도메인 인터페이스가 File에 의존하면 플랫폼 세부 구현이 새어 나옵니다. 바이트/미디어 타입(또는 도메인 모델)로 추상화하는 편이 좋습니다.

간단 대안 예:

- suspend fun postSignup(
-     profileImageFile: File?,
-     defaultImageName: String?,
-     nickname: String,
-     kakaoId: Long,
-     deviceId: String
- ): Result<UserInfo>
+ suspend fun postSignup(
+     profileImageBytes: ByteArray?,   // 원본 이미지 바이트
+     profileImageMimeType: String?,   // "image/jpeg" 등
+     defaultImageName: String?,       // 기본 이미지 사용 시 식별자
+     nickname: String,
+     kakaoId: Long,
+     deviceId: String
+ ): Result<UserInfo>

또는 SignupInfo 도메인 모델로 파라미터를 묶는 것도 안전합니다.

app/src/main/java/com/example/findu/presentation/ui/onboarding/composeview/OnboardingScreen.kt (2)

44-49: derivedStateOf 사용은 OK — remember 키는 불필요

derivedStateOf가 내부 종속 상태를 추적하므로 remember(key1, key2)는 생략 가능해요. 간결하게 정리 추천합니다.

- val isNextButtonEnabled by remember(uiState.pageState, uiState.nickNameValidState) {
-     derivedStateOf {
-         uiState.pageState == 1 ||
-                 (uiState.pageState == 2 && uiState.nickNameValidState == NicknameValidType.VALID)
-     }
- }
+ val isNextButtonEnabled by remember {
+     derivedStateOf {
+         uiState.pageState == 1 ||
+             (uiState.pageState == 2 && uiState.nickNameValidState == NicknameValidType.VALID)
+     }
+ }

66-76: when 분기에 기본 분기 추가 고려(안전망)

pageState가 1/2 이외 값이 들어올 경우를 방어하려면 else -> { /* no-op or fallback */ } 추가를 고려해 주세요.

app/src/main/java/com/example/findu/presentation/ui/onboarding/component/OnboardingProfile.kt (1)

72-91: 클릭 가능한 이미지의 접근성(contentDescription) 보완

네 개의 프로필 옵션 이미지는 클릭 동작이 있으나 contentDescription이 null로 되어 있어 스크린리더 이용자에게 목적이 전달되지 않을 수 있어요. 의미 있는 설명을 부여해 주세요(예: “기본 이미지 선택”, “강아지 이미지 선택” 등). noRippleClickable가 접근성 속성을 유지하는지 여부도 확인 부탁드립니다.

TalkBack으로 포커스 및 라벨 읽힘을 한번만 확인해 주세요.

app/src/main/java/com/example/findu/presentation/ui/home/component/HomeReportCard.kt (1)

71-99: getAllStatistics 중복 호출 제거로 리컴포지션 비용 줄이기

현재 루프 내부에서 getAllStatistics()를 반복 호출합니다. 리스트 생성을 1회로 줄여 미세한 비용을 줄일 수 있어요.

-        Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
-            currentPeriodStatistics.getAllStatistics().forEachIndexed { index, (label, count) ->
+        Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
+            val stats = currentPeriodStatistics.getAllStatistics()
+            stats.forEachIndexed { index, (label, count) ->
                 Column(
                     modifier = Modifier.weight(1f),
                     horizontalAlignment = Alignment.CenterHorizontally,
                 ) {
                     Text(
-                        text = count.toStringWithComma(),
+                        text = count.toStringWithComma(),
                         style = FindUTheme.typography.head3SemiBold18,
                         color = FindUTheme.colors.gray6
                     )
                     Spacer(modifier = Modifier.height(10.dp))
                     Text(
-                        text = label,
+                        text = label,
                         style = FindUTheme.typography.captionRegular12,
                         color = FindUTheme.colors.gray5
                     )
                 }
 
-                if (index < currentPeriodStatistics.getAllStatistics().lastIndex) {
+                if (index < stats.lastIndex) {
                     Spacer(
                         modifier = Modifier
                             .width(1.dp)
                             .height(55.dp)
                             .background(FindUTheme.colors.gray3)
                     )
                 }
             }
         }
app/src/main/java/com/example/findu/presentation/ui/onboarding/component/OnboardingProfileBox.kt (2)

31-41: 기본 이미지도 원형 클리핑 적용

Uri가 null일 때 표시하는 기본 이미지에는 CircleShape 클리핑이 없어 실제 사진일 때와 형태가 달라 보여요. 동일한 원형/크롭을 적용해 주세요.

-            Image(painter = painterResource(drawableRes), contentDescription = null,modifier = Modifier.fillMaxSize())
+            Image(
+                painter = painterResource(drawableRes),
+                contentDescription = null,
+                modifier = Modifier
+                    .fillMaxSize()
+                    .clip(CircleShape),
+                contentScale = ContentScale.Crop
+            )

53-58: 카메라 아이콘 접근성 라벨 제안

BaseVectorIcon에 contentDescription(또는 semantics)을 전달해 “프로필 사진 변경” 등 라벨을 읽어주면 좋아요. 컴포넌트가 파라미터를 지원하지 않으면 선택적으로 확장해 주셔도 됩니다.

TalkBack으로 버튼 포커스/라벨을 한번만 확인 부탁드려요.

app/src/main/java/com/example/findu/data/datalocal/datasourceimpl/DeviceLocalDataSourceImpl.kt (4)

21-21: clear()가 전체 프리퍼런스를 비웁니다 — 키 단위 삭제 권장

향후 동일 파일에 다른 키가 추가되면 예기치 않게 모두 삭제됩니다. deviceId만 제거하도록 바꾸는 게 안전합니다.

-    override fun clear() = sharedPreferences.edit { clear() }
+    override fun clear() = sharedPreferences.edit { remove(DEVICE_ID) }

17-19: getter의 .toString()는 “null” 문자열 리스크 — orEmpty() 권장

기본값이 있어도 .toString()는 null을 “null”로 변환할 수 있어 혼란을 줍니다.

-        get() = sharedPreferences.getString(DEVICE_ID, INITIAL_VALUE).toString()
+        get() = sharedPreferences.getString(DEVICE_ID, INITIAL_VALUE).orEmpty()

25-25: 키 이름 ‘token’은 혼동 소지 — ‘device_id’로 명확화

액세스 토큰과 오해될 수 있어요. 키 명을 의미 있는 값으로 바꾸는 것을 추천합니다.

-        const val DEVICE_ID = "token"
+        const val DEVICE_ID = "device_id"

10-16: SharedPreferences 직접 생성 대신 DI로 주입 추천

테스트 용이성과 일관된 스코프 관리를 위해, Hilt 모듈에서 제공하는(예: @DeviceIdPrefs 또는 @nAmed("device_id_prefs")) SharedPreferences를 주입받는 편이 좋습니다.

원하시면 DI 모듈/퀄리파이어 포함한 변경 패치를 만들어 드릴게요.

예시:

-class DeviceLocalDataSourceImpl @Inject constructor(
-    @ApplicationContext context: Context
-) : DeviceLocalDataSource {
-
-    private val sharedPreferences: SharedPreferences =
-        context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
+class DeviceLocalDataSourceImpl @Inject constructor(
+    @DeviceIdPrefs private val sharedPreferences: SharedPreferences
+) : DeviceLocalDataSource {
app/src/main/java/com/example/findu/data/repositoryimpl/AuthRepositoryImpl.kt (3)

24-29: 오류 메시지 정확도 보정

게스트 로그인에서 null 처리 메시지가 “Login data is null”로 동일합니다. 구분되면 디버깅이 편해요.

-                ?.toDomain() ?: error("Login data is null")
+                ?.toDomain() ?: error("Guest login data is null")

31-34: 닉네임 중복 API 반환 의미 확인

Boolean이 “중복 여부”인지 “사용 가능 여부”인지 혼동 소지가 있습니다. 현재는 isDuplicate를 그대로 리턴하므로, 상위 호출부에서 해석이 바뀌지 않았는지 확인해 주세요. 필요 시 메서드명/도메인 주석으로 명확화 권장.

호출부에서 true/false 의미가 올바르게 사용되는지 한 번 점검 부탁드립니다(예: true → “중복”).


20-22: 도메인 매핑 가드 👍

handleBaseResponse 이후 null 가드를 넣은 점 좋습니다. 에러 메시지 일관성도 유지되면 더 깔끔해요.

Also applies to: 43-50

app/src/main/java/com/example/findu/di/UseCaseModule.kt (1)

192-195: 함수명과 반환 타입 불일치

providePostCheckEmailUseCasePostCheckNicknameUseCase를 반환합니다. 혼동 방지를 위해 이름을 닉네임 기준으로 변경하는 게 깔끔합니다.

-    fun providePostCheckEmailUseCase(
+    fun providePostCheckNicknameUseCase(
         authRepository: AuthRepository
     ): PostCheckNicknameUseCase = PostCheckNicknameUseCase(authRepository)
app/src/main/java/com/example/findu/data/dataremote/datasource/AuthRemoteDataSource.kt (1)

27-34: 회원가입 멀티파트 시그니처 OK — 파라미터 명 일치 여부 확인 필요

defaultImageName 키 이름이 서버/Service 인터페이스의 파라미터명과 정확히 일치하는지 확인해주세요(일부 코드/요약에 defaultProfileImageName로 표기된 흔적이 있습니다).

app/src/main/java/com/example/findu/domain/model/HomeData.kt (1)

3-9: today 필드 의미와 실제 매핑 불일치 가능성*

현재 매퍼에서 recent7days 합계를 todayRescued/Report에 채우는 것으로 보입니다. UI 표기가 “오늘”이면 오해 소지 큼 — 명칭 정정 또는 매핑 수정 필요.

app/src/main/java/com/example/findu/data/mapper/todomain/HomeResponseDtoMapper.kt (1)

28-33: lost → reported 매핑 합리적

서버의 lostAnimalCount를 도메인의 신고 수로 해석하는 매핑은 OK입니다. 주석 한 줄 정도로 의도만 남겨두면 더 좋습니다.

app/src/main/java/com/example/findu/presentation/ui/home/HomeFragment.kt (3)

51-58: uiEffect 수집은 repeatOnLifecycle로 단순화 가능

flowWithLifecycle+LaunchedEffect 조합 대신 repeatOnLifecycle로 수집 수명주기를 명확히 할 수 있습니다.

- LaunchedEffect(homeViewModel.uiEffect, lifecycleOwner) {
-     homeViewModel.uiEffect.flowWithLifecycle(lifecycle = lifecycleOwner.lifecycle)
-         .collect { sideEffect -> … }
- }
+ LaunchedEffect(lifecycleOwner) {
+   lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
+     homeViewModel.uiEffect.collect { sideEffect -> … }
+   }
+ }

178-181: Dial 인텐트 예외/Resolver 체크 추가 권장

일부 기기에서 다이얼 앱 미존재 시 크래시 가능. resolveActivity 확인 또는 try/catch로 방어하세요.

 private fun call120() {
-    val intent = Intent(Intent.ACTION_DIAL, Uri.parse("tel:120"))
-    startActivity(intent)
+    val intent = Intent(Intent.ACTION_DIAL, Uri.parse("tel:120"))
+    if (intent.resolveActivity(requireContext().packageManager) != null) {
+        startActivity(intent)
+    } else {
+        Toast.makeText(requireContext(), "전화 앱을 찾을 수 없습니다.", Toast.LENGTH_SHORT).show()
+    }
 }

104-104: 하드코딩 닉네임 제거 권장

userNickname = "사용자"는 임시값으로 보입니다. 실제 사용자명 소스로 교체하거나 로딩 전 표시용 플레이스홀더로 명시하세요.

app/src/main/java/com/example/findu/data/dataremote/service/AuthService.kt (1)

3-23: 불필요한 import 정리

SignupRequestDto, retrofit2.Response는 현재 미사용으로 보입니다. 정리하면 가독성과 정적 분석 품질이 좋아집니다.

 import com.example.findu.data.dataremote.model.request.LoginRequestDto
-import com.example.findu.data.dataremote.model.request.SignupRequestDto
+// removed: SignupRequestDto (multipart로 대체됨)-import retrofit2.Response
+// removed: retrofit2.Response (직접 사용하지 않음)
app/src/main/java/com/example/findu/presentation/ui/onboarding/viewmodel/OnboardingViewModel.kt (1)

44-51: 가입 전 닉네임 유효성 재검증 필요

signUp() 진입 시 nickNameValidStateVALID인지 확인하지 않아 서버 에러를 초래할 수 있습니다.

 fun onNextClicked() {
   viewModelScope.launch {
     if (_uiState.value.pageState == LAST_PAGE) {
-        signUp()
+        if (_uiState.value.nickNameValidState == NicknameValidType.VALID) signUp()
+        else _uiState.update { it.copy(nickNameValidState = NicknameValidType.EMPTY_INVALID) }
     } else {
         _uiState.update { it.copy(pageState = LAST_PAGE) }
     }
   }
 }
app/src/main/java/com/example/findu/presentation/ui/home/composeview/HomeScreen.kt (1)

85-91: 배너 리스트 비어있을 때 예외 방지

bannerList.size == 0이면 모듈로 연산에서 예외 발생합니다. 가드 추가 권장.

 LaunchedEffect(pagerState) {
-    while (true) {
+    while (bannerList.isNotEmpty()) {
         delay(3000)
         val nextPage = (pagerState.currentPage + 1) % bannerList.size
         pagerState.animateScrollToPage(nextPage)
     }
 }
app/src/main/java/com/example/findu/presentation/ui/home/viewmodel/HomeViewModel.kt (4)

51-60: UI 이펙트는 Channel → SharedFlow로 전환하면 더 안전하고 간단합니다

Channel은 수신자가 없을 때 send가 suspend되어 UI 액션이 지연될 수 있습니다. MutableSharedFlow(replay=0, extraBufferCapacity=…) + tryEmit을 쓰면 비차단(one-shot) 이벤트 전송이 가능하고, 불필요한 launch도 제거됩니다.

아래와 같이 바꾸는 것을 제안드립니다:

- import kotlinx.coroutines.channels.Channel
- import kotlinx.coroutines.flow.receiveAsFlow
+ import kotlinx.coroutines.flow.MutableSharedFlow
+ import kotlinx.coroutines.flow.asSharedFlow
-    private val _uiEffect = Channel<HomeUiEffect>()
-    val uiEffect = _uiEffect.receiveAsFlow()
+    private val _uiEffect = MutableSharedFlow<HomeUiEffect>(replay = 0, extraBufferCapacity = 64)
+    val uiEffect = _uiEffect.asSharedFlow()

이펙트 전송부도 비동기 전송으로 단순화:

-    fun navigateToProtectList() {
-        viewModelScope.launch {
-            _uiEffect.send(HomeUiEffect.NavigateToProtectList)
-        }
-    }
+    fun navigateToProtectList() {
+        _uiEffect.tryEmit(HomeUiEffect.NavigateToProtectList)
+    }
-    fun navigateToReportList() {
-        viewModelScope.launch {
-            _uiEffect.send(HomeUiEffect.NavigateToReportList)
-        }
-    }
+    fun navigateToReportList() {
+        _uiEffect.tryEmit(HomeUiEffect.NavigateToReportList)
+    }
-    fun navigateToProtectDetail(animal: ProtectAnimal) {
-        viewModelScope.launch {
-            _uiEffect.send(HomeUiEffect.NavigateToProtectDetail(animal))
-        }
-    }
+    fun navigateToProtectDetail(animal: ProtectAnimal) {
+        _uiEffect.tryEmit(HomeUiEffect.NavigateToProtectDetail(animal))
+    }
-    fun navigateToReportDetail(animal: ReportAnimal) {
-        viewModelScope.launch {
-            _uiEffect.send(HomeUiEffect.NavigateToReportDetail(animal))
-        }
-    }
+    fun navigateToReportDetail(animal: ReportAnimal) {
+        _uiEffect.tryEmit(HomeUiEffect.NavigateToReportDetail(animal))
+    }
-    fun dial() {
-        viewModelScope.launch {
-            _uiEffect.send(HomeUiEffect.Dial)
-        }
-    }
+    fun dial() {
+        _uiEffect.tryEmit(HomeUiEffect.Dial)
+    }

Also applies to: 168-197


28-29: 다이얼로그 가시성 토글 공통화(nit)

중복을 줄이기 위해 헬퍼 메서드로 묶으면 가독성이 좋아집니다.

-            is HomeUiEvent.OnReportDialogClick -> {
-                _uiState.value = _uiState.value.copy(isReportDialogVisible = true)
-            }
+            is HomeUiEvent.OnReportDialogClick -> setReportDialogVisible(true)
 ...
-            is HomeUiEvent.OnReportDialogDismiss -> {
-                _uiState.value = _uiState.value.copy(isReportDialogVisible = false)
-            }
+            is HomeUiEvent.OnReportDialogDismiss -> setReportDialogVisible(false)

추가:

private fun setReportDialogVisible(visible: Boolean) {
    _uiState.update { it.copy(isReportDialogVisible = visible) }
}

Also applies to: 93-99


54-55: 네비게이션 payload는 객체 전체 대신 식별자만 전달 고려(옵션)

도메인 객체 전체 전달은 Nav 인자/프로세스 복원/딥링크 호환성에 불리할 수 있습니다. id만 넘기고 상세 화면에서 조회하는 패턴이 더 안정적입니다.

-    data class NavigateToProtectDetail(val animal: ProtectAnimal) : HomeUiEffect()
-    data class NavigateToReportDetail(val animal: ReportAnimal) : HomeUiEffect()
+    data class NavigateToProtectDetail(val animalId: Long) : HomeUiEffect()
+    data class NavigateToReportDetail(val animalId: Long) : HomeUiEffect()
-    fun navigateToProtectDetail(animal: ProtectAnimal) {
-        _uiEffect.tryEmit(HomeUiEffect.NavigateToProtectDetail(animal))
-    }
+    fun navigateToProtectDetail(animalId: Long) {
+        _uiEffect.tryEmit(HomeUiEffect.NavigateToProtectDetail(animalId))
+    }
-    fun navigateToReportDetail(animal: ReportAnimal) {
-        _uiEffect.tryEmit(HomeUiEffect.NavigateToReportDetail(animal))
-    }
+    fun navigateToReportDetail(animalId: Long) {
+        _uiEffect.tryEmit(HomeUiEffect.NavigateToReportDetail(animalId))
+    }

확인 부탁드립니다: 상세 화면에서 id로 재조회가 가능한지, 또는 객체 전체 전달이 반드시 필요한 요구사항이 있는지요?

Also applies to: 180-190


24-24: changeReportDuration은 동기 업데이트로 바꾸고 불필요한 launch 제거

GetHomeUseCase.invoke(lat: Double?=null, lon: Double?=null)로 기간 파라미터가 없습니다 — 기간 토글은 로컬 상태만 바꾸면 되고 서버 재조회는 기본적으로 불필요합니다. 필요 시 서버 필터 추가 후 loadHomeData() 호출을 별도으로 결정하세요.

위치: app/src/main/java/com/example/findu/presentation/ui/home/viewmodel/HomeViewModel.kt (약 157-161)

+ import kotlinx.coroutines.flow.update
-    private fun changeReportDuration(duration: HomeReportDurationType) {
-        viewModelScope.launch {
-            _uiState.value = _uiState.value.copy(reportDataDuration = duration)
-        }
-    }
+    private fun changeReportDuration(duration: HomeReportDurationType) {
+        _uiState.update { it.copy(reportDataDuration = duration) }
+        // 필요 시: 기간 파라미터를 반영해 재조회 호출
+        // loadHomeData()
+    }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a8bdb9d and 4f72096.

📒 Files selected for processing (61)
  • app/src/main/java/com/example/findu/data/datalocal/datasource/DeviceLocalDataSource.kt (1 hunks)
  • app/src/main/java/com/example/findu/data/datalocal/datasourceimpl/DeviceLocalDataSourceImpl.kt (1 hunks)
  • app/src/main/java/com/example/findu/data/datalocal/datasourceimpl/TokenLocalDataSourceImpl.kt (1 hunks)
  • app/src/main/java/com/example/findu/data/dataremote/datasource/AuthRemoteDataSource.kt (1 hunks)
  • app/src/main/java/com/example/findu/data/dataremote/datasource/HomeRemoteDataSource.kt (1 hunks)
  • app/src/main/java/com/example/findu/data/dataremote/datasourceimpl/AuthRemoteDataSourceImpl.kt (1 hunks)
  • app/src/main/java/com/example/findu/data/dataremote/datasourceimpl/HomeRemoteDataSourceImpl.kt (1 hunks)
  • app/src/main/java/com/example/findu/data/dataremote/model/base/BaseResponse.kt (1 hunks)
  • app/src/main/java/com/example/findu/data/dataremote/model/request/CheckEmailRequestDto.kt (1 hunks)
  • app/src/main/java/com/example/findu/data/dataremote/model/response/CheckEmailResponseDto.kt (1 hunks)
  • app/src/main/java/com/example/findu/data/dataremote/model/response/HomeResponseDto.kt (3 hunks)
  • app/src/main/java/com/example/findu/data/dataremote/service/AuthService.kt (1 hunks)
  • app/src/main/java/com/example/findu/data/dataremote/service/HomeService.kt (1 hunks)
  • app/src/main/java/com/example/findu/data/dataremote/util/ApiResponseHandler.kt (1 hunks)
  • app/src/main/java/com/example/findu/data/dataremote/util/Constraints.kt (1 hunks)
  • app/src/main/java/com/example/findu/data/mapper/toDomain/CheckEmailResponseDtoMapper.kt (0 hunks)
  • app/src/main/java/com/example/findu/data/mapper/todomain/HomeResponseDtoMapper.kt (1 hunks)
  • app/src/main/java/com/example/findu/data/mapper/todomain/LoginResponseDtoMapper.kt (1 hunks)
  • app/src/main/java/com/example/findu/data/mapper/todomain/UserInfoDtoMapper.kt (1 hunks)
  • app/src/main/java/com/example/findu/data/mapper/torequest/TextRequestBodyMapper.kt (1 hunks)
  • app/src/main/java/com/example/findu/data/repositoryimpl/AuthRepositoryImpl.kt (1 hunks)
  • app/src/main/java/com/example/findu/data/repositoryimpl/DeviceRepositoryImpl.kt (1 hunks)
  • app/src/main/java/com/example/findu/data/repositoryimpl/HomeRepositoryImpl.kt (1 hunks)
  • app/src/main/java/com/example/findu/di/DataSourceModule.kt (2 hunks)
  • app/src/main/java/com/example/findu/di/RepositoryModule.kt (3 hunks)
  • app/src/main/java/com/example/findu/di/SharedPreferencesModule.kt (2 hunks)
  • app/src/main/java/com/example/findu/di/UseCaseModule.kt (3 hunks)
  • app/src/main/java/com/example/findu/di/qualifier/Qualifier.kt (1 hunks)
  • app/src/main/java/com/example/findu/domain/model/AuthData.kt (1 hunks)
  • app/src/main/java/com/example/findu/domain/model/HomeData.kt (2 hunks)
  • app/src/main/java/com/example/findu/domain/repository/AuthRepository.kt (2 hunks)
  • app/src/main/java/com/example/findu/domain/repository/DeviceRepository.kt (1 hunks)
  • app/src/main/java/com/example/findu/domain/repository/HomeRepository.kt (1 hunks)
  • app/src/main/java/com/example/findu/domain/usecase/GetHomeUseCase.kt (1 hunks)
  • app/src/main/java/com/example/findu/domain/usecase/PostCheckEmailUseCase.kt (0 hunks)
  • app/src/main/java/com/example/findu/domain/usecase/PostCheckNicknameUseCase.kt (1 hunks)
  • app/src/main/java/com/example/findu/domain/usecase/PostGuestLoginUseCase.kt (1 hunks)
  • app/src/main/java/com/example/findu/domain/usecase/PostLoginUseCase.kt (1 hunks)
  • app/src/main/java/com/example/findu/domain/usecase/PostSignupUseCase.kt (1 hunks)
  • app/src/main/java/com/example/findu/domain/usecase/SetDeviceIdUseCase.kt (1 hunks)
  • app/src/main/java/com/example/findu/presentation/type/DefaultProfileType.kt (1 hunks)
  • app/src/main/java/com/example/findu/presentation/ui/home/HomeFragment.kt (4 hunks)
  • app/src/main/java/com/example/findu/presentation/ui/home/component/HomeReportCard.kt (4 hunks)
  • app/src/main/java/com/example/findu/presentation/ui/home/component/HomeReportDialog.kt (1 hunks)
  • app/src/main/java/com/example/findu/presentation/ui/home/composeview/HomeScreen.kt (6 hunks)
  • app/src/main/java/com/example/findu/presentation/ui/home/dialog/HomeReportDialog.kt (0 hunks)
  • app/src/main/java/com/example/findu/presentation/ui/home/viewmodel/HomeViewModel.kt (4 hunks)
  • app/src/main/java/com/example/findu/presentation/ui/login/LoginActivity.kt (0 hunks)
  • app/src/main/java/com/example/findu/presentation/ui/login/viewmodel/LoginViewModel.kt (2 hunks)
  • app/src/main/java/com/example/findu/presentation/ui/main/MainActivity.kt (1 hunks)
  • app/src/main/java/com/example/findu/presentation/ui/onboarding/OnboardingActivity.kt (2 hunks)
  • app/src/main/java/com/example/findu/presentation/ui/onboarding/component/OnboardingProfile.kt (6 hunks)
  • app/src/main/java/com/example/findu/presentation/ui/onboarding/component/OnboardingProfileBox.kt (3 hunks)
  • app/src/main/java/com/example/findu/presentation/ui/onboarding/composeview/OnboardingScreen.kt (4 hunks)
  • app/src/main/java/com/example/findu/presentation/ui/onboarding/viewmodel/OnboardingViewModel.kt (4 hunks)
  • app/src/main/java/com/example/findu/presentation/ui/signup/SignupActivity.kt (0 hunks)
  • app/src/main/java/com/example/findu/presentation/ui/signup/SignupSuccessActivity.kt (0 hunks)
  • app/src/main/java/com/example/findu/presentation/ui/signup/viewmodel/SignupViewModel.kt (0 hunks)
  • app/src/main/res/drawable/ic_dialog_home_report_48_blue.xml (1 hunks)
  • app/src/main/res/drawable/ic_dialog_home_report_48_red.xml (1 hunks)
  • app/src/main/res/drawable/ic_home_report_dialog_phone_24.xml (1 hunks)
💤 Files with no reviewable changes (7)
  • app/src/main/java/com/example/findu/presentation/ui/signup/SignupSuccessActivity.kt
  • app/src/main/java/com/example/findu/presentation/ui/login/LoginActivity.kt
  • app/src/main/java/com/example/findu/domain/usecase/PostCheckEmailUseCase.kt
  • app/src/main/java/com/example/findu/presentation/ui/home/dialog/HomeReportDialog.kt
  • app/src/main/java/com/example/findu/data/mapper/toDomain/CheckEmailResponseDtoMapper.kt
  • app/src/main/java/com/example/findu/presentation/ui/signup/SignupActivity.kt
  • app/src/main/java/com/example/findu/presentation/ui/signup/viewmodel/SignupViewModel.kt
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-16T15:20:00.175Z
Learnt from: t1nm1ksun
PR: FindYou-Kuit/FindYou-Android#77
File: app/src/main/java/com/example/findu/data/mapper/todomain/LoginResponseDtoMapper.kt:1-1
Timestamp: 2025-08-16T15:20:00.175Z
Learning: LoginResponseDtoMapper.kt와 관련된 패키지/디렉터리 구조는 이미 올바르게 com.example.findu.data.mapper.todomain으로 통일되어 있다. 초기 분석 시 더 신중하게 확인해야 한다.

Applied to files:

  • app/src/main/java/com/example/findu/data/mapper/todomain/LoginResponseDtoMapper.kt
  • app/src/main/java/com/example/findu/data/mapper/todomain/UserInfoDtoMapper.kt
  • app/src/main/java/com/example/findu/data/dataremote/model/response/CheckEmailResponseDto.kt
  • app/src/main/java/com/example/findu/data/repositoryimpl/AuthRepositoryImpl.kt
🧬 Code graph analysis (6)
app/src/main/java/com/example/findu/data/datalocal/datasourceimpl/TokenLocalDataSourceImpl.kt (4)
app/src/main/java/com/example/findu/data/datalocal/datasource/DeviceLocalDataSource.kt (1)
  • clear (5-5)
app/src/main/java/com/example/findu/data/datalocal/datasourceimpl/DeviceLocalDataSourceImpl.kt (1)
  • clear (21-21)
app/src/main/java/com/example/findu/data/repositoryimpl/DeviceRepositoryImpl.kt (1)
  • clear (16-16)
app/src/main/java/com/example/findu/domain/repository/DeviceRepository.kt (1)
  • clear (6-6)
app/src/main/java/com/example/findu/presentation/ui/home/component/HomeReportDialog.kt (2)
app/src/main/java/com/example/findu/presentation/ui/base/BaseVectorIcon.kt (1)
  • BaseVectorIcon (11-24)
app/src/main/java/com/example/findu/ui/theme/Theme.kt (1)
  • FindUTheme (39-59)
app/src/main/java/com/example/findu/presentation/ui/onboarding/viewmodel/OnboardingViewModel.kt (1)
app/src/main/java/com/example/findu/presentation/ui/login/viewmodel/LoginViewModel.kt (1)
  • startMainActivity (59-63)
app/src/main/java/com/example/findu/data/datalocal/datasourceimpl/DeviceLocalDataSourceImpl.kt (3)
app/src/main/java/com/example/findu/data/datalocal/datasource/DeviceLocalDataSource.kt (1)
  • clear (5-5)
app/src/main/java/com/example/findu/data/repositoryimpl/DeviceRepositoryImpl.kt (1)
  • clear (16-16)
app/src/main/java/com/example/findu/domain/repository/DeviceRepository.kt (1)
  • clear (6-6)
app/src/main/java/com/example/findu/presentation/ui/home/composeview/HomeScreen.kt (2)
app/src/main/java/com/example/findu/presentation/ui/home/component/HomeReportCard.kt (1)
  • HomeReportCard (37-101)
app/src/main/java/com/example/findu/presentation/ui/home/component/HomeReportDialog.kt (1)
  • HomeReportDialog (28-124)
app/src/main/java/com/example/findu/presentation/ui/home/HomeFragment.kt (1)
app/src/main/java/com/example/findu/presentation/ui/home/viewmodel/HomeViewModel.kt (2)
  • navigateToProtectDetail (180-184)
  • navigateToReportDetail (186-190)
🔇 Additional comments (37)
app/src/main/java/com/example/findu/data/dataremote/model/base/BaseResponse.kt (1)

7-12: 결론: BaseResponse.data를 nullable로 변경할 필요 없음

레포 검증 결과 BaseResponse(data non-null)와 NullableBaseResponse(data nullable)를 의도적으로 분리해 사용하고 있습니다. ApiResponseHandler도 두 타입을 분리 처리하며, DELETE/빈 페이로드 엔드포인트는 NullableBaseResponse로 선언되어 있어 역직렬화 실패 위험이 없습니다. 참고 경로: app/src/main/java/com/example/findu/data/dataremote/model/base/BaseResponse.kt, app/src/main/java/com/example/findu/data/dataremote/model/base/NullableBaseResponse.kt, app/src/main/java/com/example/findu/data/dataremote/util/ApiResponseHandler.kt, (예: ReportService/InterestService의 NullableBaseResponse 사용).

Likely an incorrect or invalid review comment.

app/src/main/java/com/example/findu/data/datalocal/datasourceimpl/TokenLocalDataSourceImpl.kt (1)

20-22: 확인: @TokenPrefs는 별도 'token_prefs'라서 clear() 사용 적절

app/src/main/java/com/example/findu/di/SharedPreferencesModule.kt의 provideSharedPreferences가 context.getSharedPreferences("token_prefs", ...)로 제공하므로 해당 SharedPreferences는 토큰 전용 파일이며 clear()로 전체 삭제해도 다른 키에 영향이 없습니다. 즉시 일관성이 필요하면 sharedPreferences.edit(commit = true) 사용 고려.

app/src/main/java/com/example/findu/di/qualifier/Qualifier.kt (1)

7-11: Qualifier 구성 깔끔합니다

@TokenPrefs, @DeviceIdPrefs 모두 BINARY 보존으로 일관되어 있고 용도 분리가 명확합니다. LGTM.

app/src/main/java/com/example/findu/data/dataremote/util/Constraints.kt (1)

11-12: HOME 상수 추가 좋습니다

경로 하드코딩을 줄여 리팩터링 내구성이 좋아졌어요. LGTM.

app/src/main/java/com/example/findu/data/mapper/todomain/LoginResponseDtoMapper.kt (1)

10-10: 중첩 매핑 위임 좋습니다

userInfo?.toDomain()로 위임해 중복 제거/일관성 개선된 점 👍

app/src/main/java/com/example/findu/data/dataremote/datasourceimpl/HomeRemoteDataSourceImpl.kt (1)

12-16: 위임 구현 깔끔합니다

서비스 시그니처와 일치하며 불필요한 로직 없이 위임되어 있어 👍

app/src/main/java/com/example/findu/di/RepositoryModule.kt (1)

6-6: DeviceRepository 바인딩 추가 — 중복 바인딩 없음 확인

DI 그래프에 잘 묶였고, 전역 rg 검색 결과 바인딩은 app/src/main/java/com/example/findu/di/RepositoryModule.kt:76에서 한 번만 발견되었습니다.

app/src/main/java/com/example/findu/domain/usecase/PostGuestLoginUseCase.kt (1)

11-13: deviceId 미설정 시 네트워크 호출 전에 즉시 실패 반환 추가 권장

DeviceRepository.getDeviceId()는 String을 반환하며 현재 PostGuestLoginUseCase에서는 빈값 검사를 하지 않음 — 빈 deviceId일 경우 네트워크 호출 전에 즉시 실패를 반환하는 방어 로직을 추가하세요.

파일: app/src/main/java/com/example/findu/domain/usecase/PostGuestLoginUseCase.kt

-    suspend fun postGuestLogin(): Result<GuestLoginData> = authRepository.postGuestLogin(
-        deviceId = deviceRepository.getDeviceId()
-    )
+    suspend fun postGuestLogin(): Result<GuestLoginData> {
+        val deviceId = deviceRepository.getDeviceId()
+        if (deviceId.isBlank()) {
+            return Result.failure(IllegalStateException("DeviceId is missing. Ensure SetDeviceIdUseCase ran before login."))
+        }
+        return authRepository.postGuestLogin(deviceId = deviceId)
+    }

확인 요청:

  • 앱 시작(MainActivity/Application 등)에서 SetDeviceIdUseCase가 항상 한 번 실행되는지(비동기 타이밍 포함) 확인 필요.
app/src/main/java/com/example/findu/presentation/ui/onboarding/composeview/OnboardingScreen.kt (1)

72-74: Uri 기반 프로필 바인딩 전환 👍

profileImgUri로의 전환 및 클리어 콜백 연동이 일관적입니다.

app/src/main/java/com/example/findu/domain/usecase/PostCheckNicknameUseCase.kt (1)

5-10: 단순 위임 구조 — 닉네임 공백(트림) 가드 권장

파일: app/src/main/java/com/example/findu/domain/usecase/PostCheckNicknameUseCase.kt (lines 5–10)
구현 깔끔. 닉네임은 도메인에서 trim 후 공백을 방어하면 안정적입니다(선택적으로 VM에서 처리 가능).

- suspend operator fun invoke(nickname: String): Result<Boolean> =
-     authRepository.postCheckNickname(nickname = nickname)
+ suspend operator fun invoke(nickname: String): Result<Boolean> {
+     val trimmed = nickname.trim()
+     require(trimmed.isNotEmpty()) { "nickname must not be blank" }
+     return authRepository.postCheckNickname(nickname = trimmed)
+ }

추가 확인: PostCheckEmailUseCase / postCheckEmail 호출 잔존 여부 — 리포지터리 검색이 실패해 확인 불가. 로컬에서 아래 스크립트로 확인 필요:

#!/bin/bash
set -euo pipefail
rg -nP --hidden -uu --glob '!**/build/**' 'PostCheckEmailUseCase|postCheckEmail\s*\(' || echo "no matches"
app/src/main/java/com/example/findu/domain/usecase/PostSignupUseCase.kt (1)

17-25: LGTM — 새 사인업 플로우 연결 좋습니다

도메인 반환 타입을 UserInfo로 올린 점과 deviceId 주입 흐름이 일관적입니다. 상위 호출부에서 Result를 그대로 다루는 구조도 괜찮아요.

혹시 deviceId가 어떻게 초기화되는지(앱 최초 실행 시점) 보장 경로를 한번만 공유 부탁드립니다. 중복 초기화/레이스가 없으면 이대로 충분합니다.

app/src/main/java/com/example/findu/presentation/ui/onboarding/component/OnboardingProfile.kt (2)

60-64: LGTM — Uri 기반 파라미터 전환

profileImgUri로의 전환과 기본 타입 DEFAULT 적용이 주변 호출부와 일관됩니다.


118-126: 서명 변경 사용처 일괄 점검 필요 — 자동 검색 실패(로컬 확인 필요)

rg 실행 결과 "No files were searched" 반환되어 자동 검증 불가.
로컬에서 아래 명령 중 하나를 실행해 잔존 식별자(profileImgUrl)와 OnboardingProfile 호출을 확인하세요:

rg --debug -n -C2 'profileImgUrl|OnboardingProfile'
rg -n -C2 --hidden --glob '**/*.kt' 'profileImgUrl|OnboardingProfile'

검색 결과(파일:라인:내용)를 첨부하면 확인 후 코멘트 갱신함.

app/src/main/java/com/example/findu/presentation/ui/home/component/HomeReportCard.kt (2)

44-49: LGTM — 기간별 통계 분기 매핑 OK

HomeReportDurationType ↔ HomeStatistics 매핑이 직관적입니다.


156-173: 프리뷰 데이터 👍

미리보기에서 WEEK/3개월/1년 전환 흐름 검증에 유용합니다.

app/src/main/java/com/example/findu/presentation/ui/onboarding/component/OnboardingProfileBox.kt (1)

24-30: 확인: profileImgUrl → profileImgUri 전역 반영됨
레포에서 profileImgUri만 사용됨(예: app/src/main/java/com/example/findu/presentation/ui/onboarding/component/OnboardingProfileBox.kt, OnboardingProfile.kt, composeview/OnboardingScreen.kt); profileImgUrl 발견되지 않음.

app/src/main/java/com/example/findu/di/UseCaseModule.kt (3)

177-181: 로그인 UseCase에 DeviceRepository 주입 — DI 변경 LGTM

구성 적절합니다. 아래 바인딩 존재/스코프만 확인해 주세요: DeviceRepository 구현(RepositoryModule)과 DeviceLocalDataSource 바인딩.


184-189: 게스트 로그인 UseCase도 동일하게 OK

구성 동일하게 문제 없어 보입니다. Runtime 시 순환 의존만 없는지 체크 부탁.


198-202: 회원가입 UseCase에 DeviceRepository 주입 — OK

도메인 생성자 변경과 맞물린 DI 반영으로 보입니다. 기존 TokenRepository 의존 제거에 따른 호출부(테스트/팩토리) 빌드 확인만 해 주세요.

app/src/main/java/com/example/findu/data/dataremote/datasource/AuthRemoteDataSource.kt (2)

15-22: Login/GuestLogin 응답을 NullableBaseResponse로 전환 — 주의사항

Repository 계층의 null data 처리와 에러 매핑이 일관되게 바뀌었는지 확인 필요합니다.


23-26: 닉네임 중복 체크로의 전환 OK

서버 규격과 일치하면 그대로 진행해도 됩니다. 레이트리밋/디바운스는 VM/UI에서 처리 중인지 확인 부탁.

app/src/main/java/com/example/findu/data/dataremote/datasourceimpl/AuthRemoteDataSourceImpl.kt (3)

22-27: Login/GuestLogin: NullableBaseResponse 반환으로 정합성 OK

Service 호출부와 타입 정합성 문제 없어 보입니다.


28-30: 닉네임 체크 요청 래핑 깔끔합니다

단일 문자열을 DTO로 감싸는 방식 적절합니다.


31-43: 확인 — 멀티파트 필드명 일치 (수정 불필요)
AuthService.kt의 postSignup은 @part("defaultProfileImageName") defaultImageName으로 선언되어 있고 호출부는 동일한 defaultImageName 파라미터에 값을 전달합니다. profileImage는 toImageMultipart("profileImage")로 MultipartBody.Part 이름이 설정되므로 필드명 불일치 문제 없음.

app/src/main/java/com/example/findu/domain/model/HomeData.kt (1)

17-29: 통계 모델/라벨 구성 깔끔합니다

getAllStatistics()의 라벨/값 페어 구성 일관적이며 사용성 좋아 보입니다.

app/src/main/java/com/example/findu/data/mapper/todomain/HomeResponseDtoMapper.kt (2)

44-51: 검증 결과: 현재 매핑이 DTO 필드와 일치합니다
HomeResponseDto.kt의 WitnessedOrMissingAnimal에 @SerialName("happenDate") happenDate 및 @SerialName("careAddress") careAddress가 정의되어 있어, mapper의 registerDate = happenDate / happenLocation = careAddress 매핑은 적절합니다. 제안한 registerDate/registerLocation 필드 사용 변경은 DTO에 해당 필드가 없어 부적절합니다.

Likely an incorrect or invalid review comment.


35-42: 매핑은 의도된 대로 되어 있습니다 — 변경 불필요

ProtectingAnimal DTO에 reportId/happenDate가 있고 도메인 ProtectAnimal은 protectId/noticeStartDate를 사용하므로 toDomain의 매핑(reportId → protectId, happenDate → noticeStartDate)은 올바릅니다. 참조: app/src/main/java/com/example/findu/data/dataremote/model/response/HomeResponseDto.kt, app/src/main/java/com/example/findu/domain/model/HomeData.kt

app/src/main/java/com/example/findu/presentation/ui/home/HomeFragment.kt (2)

56-61: 상세 이동 파라미터 변경 반영 LGTM

sideEffect.animal에서 id/tag/title을 안전하게 꺼내 NavArgs로 넘기는 흐름이 일관적입니다.


72-76: 긴급: TODO() 즉시 제거/구현 필요 — 런타임 크래시 유발

파일: app/src/main/java/com/example/findu/presentation/ui/home/HomeFragment.kt (72–76행) — HomeUiEffect.NavigateToProtectList / NavigateToReportList 처리에 TODO()가 남아 있습니다. TODO()는 NotImplementedError로 런타임 즉시 크래시합니다.

                                is HomeUiEffect.NavigateToProtectList -> TODO()
                                is HomeUiEffect.NavigateToReportList -> TODO()

                                is HomeUiEffect.Dial -> call120()
                            }

조치(짧고 굵게):

  • 단기: ViewModel에서 해당 UI effect 발행 중단 또는 Fragment에서 TODO() 대신 안전한 no-op/토스트로 대체해 크래시 차단.
  • 중기: 실제 리스트 화면으로 네비게이션 구현(navigateToProtectList()/navigateToReportList()).
  • 장기: 네비 실패 시 토스트 출력 + 로그/리포트 남기기.

검증 요청: 레포 내 파일 검색이 실패해 직접 확인이 필요합니다 — 로컬에서 아래 명령으로 HomeFragment.kt와 TODO() 사용처를 확인해 주세요:
rg -n -C2 'NavigateToProtectList|NavigateToReportList|TODO()' app/src/main -g '!/build/'

app/src/main/java/com/example/findu/data/dataremote/service/AuthService.kt (2)

28-34: NullableBaseResponse로 변경 시 에러 처리 확인

로그인/게스트 로그인 응답이 NullableBaseResponse로 변경되었습니다. Null payload/에러 코드 분기 로직이 데이터소스/리포지토리/유즈케이스까지 일관되게 반영됐는지 확인 바랍니다.


40-49: 백엔드와 멀티파트 파트명·profileImage null 처리 재확인

app/src/main/java/com/example/findu/data/dataremote/service/AuthService.kt (40–49)

  • defaultProfileImageName, nickname, kakaoId, deviceId 파트명/타입이 서버 스펙과 1:1로 일치하는지 확인. profileImage가 null일 때 기본 이미지 적용 방식(서버 처리/클라이언트 처리)을 명확히 합의할 것.
  • 자동검증 실패: rg가 "unrecognized file type: kt" 오류로 검색이 불가했습니다. 로컬에서 아래 명령으로 재검증하거나 서버 스펙(정확한 파라미터명·타입·옵션)을 회신해 주세요:
    rg -n -g '!/build/' 'defaultProfileImageName|kakaoId|deviceId|profileImage' -C2
app/src/main/java/com/example/findu/presentation/ui/home/composeview/HomeScreen.kt (2)

200-213: 신규 제보 다이얼로그 연동 LGTM

Dismiss 연쇄 처리와 전화 클릭 콜백 분리가 깔끔합니다.

UI 상호작용에서 실제 Fragment 콜백이 구현되었는지(특히 리스트 네비게이션) 확인 부탁드립니다. 현재 Fragment 쪽은 리스트 네비가 TODO() 상태입니다.


161-180: uiState.homeData!! — 안전 접근으로 변경 필요

네트워크/매핑 실패 시 즉시 NPE로 크래시합니다. 안전 호출(?.) 또는 let/guard로 보호하세요.
파일: app/src/main/java/com/example/findu/presentation/ui/home/composeview/HomeScreen.kt (약 161–180행)

- HomeProtectAnimalList(
+ uiState.homeData?.let { data ->
+   HomeProtectAnimalList(
      nickname = userNickname,
-     navigationToSearch = navigationToProtectAnimal,
-     animalCards = uiState.homeData!!.protectAnimalCards,
+     navigationToSearch = navigationToProtectAnimal,
+     animalCards = data.protectAnimalCards,
      navigateToProtectDetail = navigateToProtectDetail
- )
+   )
+ }- HomeReportedAnimalList(
+ uiState.homeData?.let { data ->
+   HomeReportedAnimalList(
      nickname = userNickname,
-     navigationToSearch = navigationToReportAnimal,
-     animalCards = uiState.homeData!!.reportAnimalCards,
+     navigationToSearch = navigationToReportAnimal,
+     animalCards = data.reportAnimalCards,
      navigateToReportDetail = navigateToReportDetail
- )
+   )
+ }
app/src/main/java/com/example/findu/data/dataremote/model/response/HomeResponseDto.kt (3)

16-24: 통계 스키마 정합성 확인

recent7days/recent3months/recent1Year 키와 lostAnimalCount 명칭이 BE 스펙과 정확히 일치하는지 확인 바랍니다(도메인에선 reported 계열로 매핑된다고 들었습니다).

Also applies to: 26-36


6-14: DTO 스키마 전환 LGTM

상위 statistics + 리스트 2종으로의 개편이 명확합니다. 시리얼라이저 태그도 일관적입니다.

Also applies to: 54-68


38-46: 확인 완료 — DTO의 reportId가 도메인 protectId로 매핑되어 있음

app/src/main/java/com/example/findu/data/dataremote/model/response/HomeResponseDto.kt에 @SerialName("reportId") val reportId: Int로 정의되어 있고, app/src/main/java/com/example/findu/data/mapper/todomain/HomeResponseDtoMapper.kt의 ProtectingAnimal.toDomain()에서 protectId = reportId로 매핑됩니다. 따라서 백엔드 계약과 매퍼가 일치하므로 변경 불필요합니다.

app/src/main/java/com/example/findu/presentation/ui/home/viewmodel/HomeViewModel.kt (1)

39-40: OnReportDialogDismiss 이벤트 추가 좋습니다

명시적인 dismiss 이벤트로 상태 전이가 분명해졌습니다. 👍

Comment on lines 7 to 10
data class CheckNicknameRequestDto(
@SerialName("nickname")
val nickname: String
)
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

🧩 Analysis chain

email → nickname 전환 LGTM. 레거시 잔재 스캔만 부탁

DTO/필드 전환은 일관적입니다. 남아있을 수 있는 CheckEmail/isDuplicateEmail/@SerialName("email") 레거시를 정리해 주세요.


🏁 Script executed:

#!/bin/bash
# 레거시 식별자/필드명 잔존 여부 점검
rg -n -C2 'CheckEmail|postCheckEmail|@SerialName\("email"\)|isDuplicateEmail|CheckEmailRequestDto|CheckEmailResponseDto'

Length of output: 1043


providePostCheckEmailUseCase → providePostCheckNicknameUseCase로 이름 수정 필요

UseCaseModule.kt에 @provides 함수명이 아직 providePostCheckEmailUseCase로 남아있습니다 — 반환타입/생성자는 PostCheckNicknameUseCase입니다. 함수명(또는 바인딩)을 일관되게 수정하세요. (app/src/main/java/com/example/findu/di/UseCaseModule.kt: ~192)

SignupRequestDto@SerialName("email") val email: String은 회원가입용 필드로 정상적이므로 유지하면 됩니다. (app/src/main/java/com/example/findu/data/dataremote/model/request/SignupRequestDto.kt:8)

🤖 Prompt for AI Agents
In app/src/main/java/com/example/findu/di/UseCaseModule.kt around line ~192, the
@Provides function is still named providePostCheckEmailUseCase while its return
type/constructor is PostCheckNicknameUseCase; rename the provider function to
providePostCheckNicknameUseCase (and update any DI bindings/usages referencing
the old name) so the function name matches the provided type, keeping
SignupRequestDto's @SerialName("email") val email: String unchanged.

Comment on lines 9 to 10
Result.success(this.data)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

BaseResponse 2xx에서 null 성공 처리 → 계약 위반 가능성

BaseResponse<T>는 비nullable 데이터 계약을 의미하는데, 현재 구현은 data == null도 성공으로 반환합니다. 이는 상위 계층에서 예상치 못한 NPE/로직 오류를 유발할 수 있습니다. 기존의 null 가드 복원 또는 반환 타입을 Result<T?>로 바꾸는 둘 중 하나가 필요합니다. 이미 NullableBaseResponse가 있으므로, BaseResponse는 엄격 모드 유지가 더 타당합니다.

-        in 200..299 -> { // 성공
-            Result.success(this.data)
-        }
+        in 200..299 -> { // 성공
+            this.data?.let { Result.success(it) }
+                ?: Result.failure(IllegalStateException("data is null"))
+        }
📝 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
Result.success(this.data)
}
in 200..299 -> { // 성공
this.data?.let { Result.success(it) }
?: Result.failure(IllegalStateException("data is null"))
}
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/findu/data/dataremote/util/ApiResponseHandler.kt
around lines 9-10, the BaseResponse success branch currently returns
Result.success(this.data) even when data == null, violating the non-null
contract; restore the null guard so BaseResponse never yields a successful
Result with null: check data for null and return
Result.failure(IllegalStateException("Expected non-null data for BaseResponse"))
(or an appropriate exception) when null, keeping NullableBaseResponse for
nullable payloads instead of changing the return type.

Comment on lines 10 to +20
fun HomeResponseDto.toDomain() = HomeData(
todayRescuedAnimalCount = yesterdayRescuedAnimalCount,
todayReportAnimalCount = yesterdayReportedAnimalCount,
protectAnimalCards = protectAnimalCards.map { it.toDomain() },
reportAnimalCards = reportAnimalCards.map { it.toDomain() }
todayRescuedAnimalCount = statistics.recent7days.rescuedAnimalCount,
todayReportAnimalCount = statistics.recent7days.lostAnimalCount,
protectAnimalCards = protectingAnimals.map { it.toDomain() },
reportAnimalCards = witnessedOrMissingAnimals.map { it.toDomain() },
statistics = statistics.toDomain()
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

🧩 Analysis chain

“오늘” 지표 매핑 오류 의심: 7일 누계 사용 중

todayRescuedAnimalCount/todayReportAnimalCountstatistics.recent7days.*를 그대로 넣고 있습니다. 실제 당일 지표가 필요하다면 서버 필드를 사용하거나 도메인 필드명을 기간 기반으로 변경해야 합니다.


🏁 Script executed:

#!/bin/bash
# HomeResponseDto/Statistics/PeriodStatistics 원본 필드 확인
rg -n -C3 -P 'data\s+class\s+HomeResponseDto'
rg -n -C3 -P 'data\s+class\s+Statistics\b'
rg -n -C3 -P 'data\s+class\s+PeriodStatistics\b'
rg -n -C2 -P '\btoday(Rescued|Lost|Reported)' --type=kt

Length of output: 2997


오늘 지표 매핑 오류 — recent7days를 today로 사용 중

Mapper가 statistics.recent7days.rescuedAnimalCount / lostAnimalCount를 todayRescuedAnimalCount / todayReportAnimalCount에 할당해 명칭·의미 불일치가 발생함.

  • 위치: app/src/main/java/com/example/findu/data/mapper/todomain/HomeResponseDtoMapper.kt (현재 14–20행). DTO 정의: app/src/main/java/com/example/findu/data/dataremote/model/response/HomeResponseDto.kt (Statistics.recent7days / PeriodStatistics), 도메인: app/src/main/java/com/example/findu/domain/model/HomeData.kt (PeriodStatistics).
  • 조치(택1): 서버에서 ‘오늘’ 지표를 제공하면 DTO/API에 당일 필드 추가 후 매핑 수정.
    조치(택2): 서버가 7일 누계만 제공하면 도메인 필드명(today*)을 recent7days*로 변경해 의미 일치.
🤖 Prompt for AI Agents
app/src/main/java/com/example/findu/data/mapper/todomain/HomeResponseDtoMapper.kt
lines 14–20: the mapper assigns statistics.recent7days.* to fields named
todayRescuedAnimalCount/todayReportAnimalCount which is a semantic mismatch; fix
by either (A) if the API/DTO will provide true "today" metrics, add those fields
to HomeResponseDto/Statistics, map statistics.today.rescuedAnimalCount and
statistics.today.lostAnimalCount to the domain today* fields, and update
usages/tests accordingly, or (B) if only 7-day aggregates exist, rename the
domain HomeData fields (todayRescuedAnimalCount/todayReportAnimalCount) to
recent7daysRescuedAnimalCount/recent7daysReportAnimalCount (and update HomeData,
any consumers and tests) so names reflect recent7days values; apply whichever
path matches server contract and keep types/mapping consistent.

Comment on lines 6 to 11
fun UserInfoDto.toDomain(): UserInfo =
UserInfo(
userId = this.userId,
nickname = this.nickname,
accessToken = this.accessToken
) No newline at end of file
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

🧩 Analysis chain

accessToken의 도메인 노출 재고 권장

토큰을 UserInfo 도메인에 포함시키면 UI/로그/캐시 경로로 확산될 리스크가 큽니다. 토큰은 별도 보관소(예: TokenRepository, 안전 저장소)에만 저장하고 도메인 모델에서는 제거하는 쪽을 권장합니다.


🏁 Script executed:

#!/bin/bash
# accessToken이 로그/디버그로 노출되는지 점검
rg -nP --glob '!**/build/**' -C2 '(accessToken|UserInfo\()'

Length of output: 148


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "PWD: $(pwd)"
echo "Checking ripgrep and fd availability..."
command -v rg >/dev/null 2>&1 || { echo "rg not found"; exit 0; }
command -v fd >/dev/null 2>&1 || echo "fd not found, continuing with rg only"

echo "--- rg: search 'accessToken' with context ---"
rg --hidden --no-ignore-vcs -S -n -C3 'accessToken' || true

echo "--- rg: search '\\.accessToken' ---"
rg --hidden --no-ignore-vcs -S -n '\.accessToken\b' || true

echo "--- rg: search 'UserInfo(' usages ---"
rg --hidden --no-ignore-vcs -S -n -C2 'UserInfo\(' || true

echo "--- rg: search 'data class UserInfo' declaration ---"
rg --hidden --no-ignore-vcs -S -n -C3 'data class UserInfo' || true

echo "--- fd: locate UserInfoDtoMapper.kt ---"
if command -v fd >/dev/null 2>&1; then
  fd -H --type f 'UserInfoDtoMapper.kt' || true
fi

if [ -f "app/src/main/java/com/example/findu/data/mapper/todomain/UserInfoDtoMapper.kt" ]; then
  echo "=== Dump mapper file ==="
  sed -n '1,240p' app/src/main/java/com/example/findu/data/mapper/todomain/UserInfoDtoMapper.kt || true
fi

echo "--- Search for logging calls near 'accessToken' ---"
rg --hidden --no-ignore-vcs -S -n -C3 'accessToken' || true

echo "--- Search for token storage/repository names ---"
rg --hidden --no-ignore-vcs -S -n -C2 'TokenRepository|tokenRepository|TokenStore|SecureStore|EncryptedSharedPreferences|Encrypted' || true

exit 0

Length of output: 41384


UserInfo 도메인에서 accessToken 제거 — 토큰은 TokenRepository로만 관리

검증 결과 UserInfo 도메인에 accessToken이 포함되어 있고 매퍼와 뷰모델을 통해 TokenRepository에 전달되고 있습니다(로그/UI/캐시 노출 위험).

  • 도메인: app/src/main/java/com/example/findu/domain/model/AuthData.kt — UserInfo에서 accessToken 제거
  • 매퍼: app/src/main/java/com/example/findu/data/mapper/todomain/UserInfoDtoMapper.kt — accessToken 매핑 제외
  • 뷰모델: app/src/main/java/com/example/findu/presentation/ui/login/viewmodel/LoginViewModel.kt — loginData.userInfo!!.accessToken 대신 loginData.accessToken(또는 별도 Auth 도메인 객체)으로 SetAccessTokenUseCase 호출하도록 변경
  • 토큰 저장/사용 책임은 TokenRepository/TokenLocalDataSource(app/src/.../TokenLocalDataSourceImpl.kt)와 AuthInterceptor(app/src/.../AuthInterceptor.kt)에만 둬서 UI/로그/캐시로의 확산을 차단
🤖 Prompt for AI Agents
In app/src/main/java/com/example/findu/data/mapper/todomain/UserInfoDtoMapper.kt
around lines 6 to 11, the mapper currently maps accessToken into the UserInfo
domain model; remove accessToken from the mapping so UserInfo is constructed
only with userId and nickname, matching the domain change
(app/src/main/java/com/example/findu/domain/model/AuthData.kt) where accessToken
was removed; then update LoginViewModel to call SetAccessTokenUseCase using
loginData.accessToken (or a separate Auth domain object) instead of
loginData.userInfo!!.accessToken, and ensure token persistence/usage remains
solely in TokenRepository/TokenLocalDataSource and AuthInterceptor to prevent
token exposure to UI/logs/caches.

Comment on lines 24 to 29
@DeviceIdPrefs
@Provides
@Singleton
fun provideDeviceIdSharedPreference(@ApplicationContext context: Context): SharedPreferences {
return context.getSharedPreferences("device_id_prefs", Context.MODE_PRIVATE)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

🧩 Analysis chain

DeviceId SharedPreferences 사용처 일치 여부 확인

DeviceLocalDataSourceImpl가 직접 이름을 하드코딩하지 말고 @DeviceIdPrefs 주입을 사용하고 있는지 확인 부탁드립니다.


🏁 Script executed:

#!/bin/bash
# DeviceIdPrefs 기반 주입 여부
rg -n -C3 -iP 'DeviceLocalDataSourceImpl|@DeviceIdPrefs|SharedPreferences' app/src/main/java
# device_id_prefs 문자열 하드코딩 여부
rg -n -i 'device_id_prefs' app/src/main/java

Length of output: 14010


DeviceId SharedPreferences 주입 불일치 — 수정 필요

SharedPreferencesModule은 @DeviceIdPrefs로 "device_id_prefs"를 제공하는 반면, DeviceLocalDataSourceImpl은 ApplicationContext에서 직접 getSharedPreferences(PREFERENCES_NAME)로 "device_preferences"를 하드코딩하고 있음 — 불일치 및 하드코딩 문제.

  • 조치: DeviceLocalDataSourceImpl을 @DeviceIdPrefs로 SharedPreferences를 주입받아 사용하도록 변경하거나, 모듈에서 제공하는 파일명을 "device_preferences"로 일치시켜라.
  • 위치: app/src/main/java/com/example/findu/data/datalocal/datasourceimpl/DeviceLocalDataSourceImpl.kt (getSharedPreferences 호출 — line 14; companion object PREFERENCES_NAME = "device_preferences" — line 24), app/src/main/java/com/example/findu/di/SharedPreferencesModule.kt (provideDeviceIdSharedPreference -> "device_id_prefs" — line 28).
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/findu/data/datalocal/datasourceimpl/DeviceLocalDataSourceImpl.kt
(getSharedPreferences call at line 14; companion object PREFERENCES_NAME at line
24) and app/src/main/java/com/example/findu/di/SharedPreferencesModule.kt
(provideDeviceIdSharedPreference -> "device_id_prefs" at line 28): the
SharedPreferences filename is inconsistent and hardcoded; fix by changing
DeviceLocalDataSourceImpl to use constructor injection of the @DeviceIdPrefs
SharedPreferences provided by the module (add @Inject constructor(private val
prefs: @DeviceIdPrefs SharedPreferences) and remove direct
context.getSharedPreferences(...) and the PREFERENCES_NAME constant), or
alternatively change the module to return SharedPreferences with name
"device_preferences" so both match; ensure the chosen approach updates
imports/annotations and removes the hardcoded getSharedPreferences usage.

Comment on lines 3 to 7
interface DeviceRepository {
fun getDeviceId(): String
fun setDeviceId(deviceId: String)
fun clear()
} No newline at end of file
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

🧩 Analysis chain

DeviceId 로컬 저장 키/프리퍼런스 충돌 점검 필요

AI 요약상 DeviceLocalDataSourceImpl이 "device_preferences"/키 "token"을 사용한다고 되어 있어 토큰/디바이스ID 저장소가 뒤섞일 가능성이 있어요. Hilt 모듈의 @DeviceIdPrefs("device_id_prefs")와 일치하는지 확인해 주세요.

다음 스크립트로 구현을 확인할 수 있어요:


🏁 Script executed:

#!/bin/bash
# DeviceLocalDataSourceImpl에서 SharedPreferences 제공 및 키 사용 확인
rg -n -C2 -iP 'class\s+DeviceLocalDataSourceImpl|getSharedPreferences\(|putString\(|getString\('
# device-id용 Qualifier 주입 사용 여부
rg -n -C3 -i '@DeviceIdPrefs|DeviceIdPrefs' app/src/main/java

Length of output: 15532


DeviceId 로컬 저장소/Prefs 충돌 — 수정 필요

SharedPreferencesModule은 "token_prefs"와 "device_id_prefs"를 별도 제공하지만 DeviceLocalDataSourceImpl/DummyLocalDataSourceImpl이 ApplicationContext로 동일한 PREFERENCES_NAME에 직접 getSharedPreferences(...)를 호출하고 있어 토큰↔디바이스ID가 섞일 가능성이 있음.

  • 조치: DeviceLocalDataSourceImpl은 @DeviceIdPrefs로 주입된 SharedPreferences를 사용하도록 변경하거나 PREFERENCES_NAME을 명확히 "device_id_prefs"로 고정. DummyLocalDataSourceImpl(토큰 저장)은 token 전용 prefs("token_prefs")를 사용하도록 분리.
  • 확인/수정 대상 파일:
    • app/src/main/java/com/example/findu/data/datalocal/datasourceimpl/DeviceLocalDataSourceImpl.kt
    • app/src/main/java/com/example/findu/data/datalocal/datasourceimpl/DummyLocalDataSourceImpl.kt
    • app/src/main/java/com/example/findu/di/SharedPreferencesModule.kt

Comment on lines 10 to 25
suspend fun postSignup(
email: String,
password: String,
nickname: String
): Result<Unit> =
authRepository.postSignup(email, password, nickname).mapCatching { accessToken ->
tokenRepository.setAccessToken(accessToken)
}
profileImageFile: File?,
defaultImageName: String?,
nickname: String,
kakaoId: Long,
): Result<UserInfo> = authRepository.postSignup(
profileImageFile = profileImageFile,
defaultImageName = if (profileImageFile != null) {
null
} else defaultImageName,
nickname = nickname,
kakaoId = kakaoId,
deviceId = deviceRepository.getDeviceId()
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

deviceId 공백 대비 + defaultImageName 처리 간결화 제안

  • deviceRepository.getDeviceId()가 공백("")일 가능성을 방어하지 않아 서버 요청 실패(400/422)로 번질 수 있어요. 유즈케이스에서 미리 검증하거나 실패를 명시적으로 반환하는 편이 안전합니다.
  • defaultImageName null 처리 로직은 takeUnless로 더 간결하게 표현 가능합니다.

아래처럼 바꾸면 명시적 실패와 파라미터 정리가 동시에 됩니다:

-    ): Result<UserInfo> = authRepository.postSignup(
-        profileImageFile = profileImageFile,
-        defaultImageName = if (profileImageFile != null) {
-            null
-        } else defaultImageName,
-        nickname = nickname,
-        kakaoId = kakaoId,
-        deviceId = deviceRepository.getDeviceId()
-    )
+    ): Result<UserInfo> {
+        val deviceId = deviceRepository.getDeviceId()
+        if (deviceId.isBlank()) {
+            return Result.failure(IllegalStateException("deviceId is blank"))
+        }
+        return authRepository.postSignup(
+            profileImageFile = profileImageFile,
+            defaultImageName = defaultImageName.takeUnless { profileImageFile != null },
+            nickname = nickname,
+            kakaoId = kakaoId,
+            deviceId = deviceId
+        )
+    }
📝 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
suspend fun postSignup(
email: String,
password: String,
nickname: String
): Result<Unit> =
authRepository.postSignup(email, password, nickname).mapCatching { accessToken ->
tokenRepository.setAccessToken(accessToken)
}
profileImageFile: File?,
defaultImageName: String?,
nickname: String,
kakaoId: Long,
): Result<UserInfo> = authRepository.postSignup(
profileImageFile = profileImageFile,
defaultImageName = if (profileImageFile != null) {
null
} else defaultImageName,
nickname = nickname,
kakaoId = kakaoId,
deviceId = deviceRepository.getDeviceId()
)
suspend fun postSignup(
profileImageFile: File?,
defaultImageName: String?,
nickname: String,
kakaoId: Long,
): Result<UserInfo> {
val deviceId = deviceRepository.getDeviceId()
if (deviceId.isBlank()) {
return Result.failure(IllegalStateException("deviceId is blank"))
}
return authRepository.postSignup(
profileImageFile = profileImageFile,
defaultImageName = defaultImageName.takeUnless { profileImageFile != null },
nickname = nickname,
kakaoId = kakaoId,
deviceId = deviceId
)
}
🤖 Prompt for AI Agents
In app/src/main/java/com/example/findu/domain/usecase/PostSignupUseCase.kt
around lines 12 to 25, validate deviceRepository.getDeviceId() before calling
authRepository.postSignup and return a clear failure (or throw a domain-level
exception) if it's blank to avoid sending an invalid request, and simplify the
defaultImageName argument by using Kotlin's takeUnless (i.e., pass
defaultImageName.takeUnless { profileImageFile != null }) so the parameter logic
is concise and the deviceId is protected from empty-string propagation to the
server.

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 (2)
app/src/main/java/com/example/findu/presentation/ui/onboarding/viewmodel/OnboardingViewModel.kt (2)

115-128: 메인 스레드에서 파일 I/O + ID 유효성 체크 누락

uriToFile 호출이 메인 디스패처에서 실행되고, kakaoId 가 기본값(-1)일 때도 진행됩니다. 대용량 이미지에서 UI 프리즈/ANR를 유발할 수 있고 잘못된 ID로 요청이 나갈 수 있습니다. 이전 코멘트와 동일 이슈입니다.

@@
-    private fun signUp() {
-        viewModelScope.launch {
-            postSignupUseCase.postSignup(
-                profileImageFile = uiState.value.profileImageUri?.let { uriToFile(uri = it) },
-                defaultImageName = uiState.value.defaultProfileType.string,
-                nickname = uiState.value.nickname,
-                kakaoId = uiState.value.kakaoId
-            ).onSuccess {
-                startMainActivity()
-            }.onFailure {e->
-                Log.d("http", "Error Message: : $e")
-            }
-        }
-    }
+    private fun signUp() {
+        viewModelScope.launch {
+            val id = uiState.value.kakaoId
+            if (id <= 0L) {
+                Log.e("OnboardingViewModel", "Invalid kakaoId: $id")
+                return@launch
+            }
+            val profileImage = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) {
+                uiState.value.profileImageUri?.let { uriToFile(uri = it) }
+            }
+            postSignupUseCase.postSignup(
+                profileImageFile = profileImage,
+                defaultImageName = uiState.value.defaultProfileType.string,
+                nickname = uiState.value.nickname,
+                kakaoId = id
+            ).onSuccess {
+                startMainActivity()
+            }.onFailure { e ->
+                Log.e("OnboardingViewModel", "Signup failed", e)
+            }
+        }
+    }

141-153: InputStream 누수 — use 블록으로 안전하게 닫기

openInputStreamuse {}로 감싸지 않아 스트림이 닫히지 않습니다. 이전 코멘트와 동일 이슈입니다.

-    private fun uriToFile(uri: Uri): File? {
-        return try {
-            val inputStream = context.contentResolver.openInputStream(uri) ?: return null
-            val file = File.createTempFile("profile_", ".jpg", context.cacheDir)
-            file.outputStream().use { outputStream ->
-                inputStream.copyTo(outputStream)
-            }
-            file
-        } catch (e: Exception) {
-            e.printStackTrace()
-            null
-        }
-    }
+    private fun uriToFile(uri: Uri): File? {
+        return try {
+            val file = File.createTempFile("profile_", ".jpg", context.cacheDir)
+            context.contentResolver.openInputStream(uri)?.use { input ->
+                file.outputStream().use { output -> input.copyTo(output) }
+            } ?: return null
+            file
+        } catch (e: Exception) {
+            e.printStackTrace()
+            null
+        }
+    }
🧹 Nitpick comments (7)
app/src/main/java/com/example/findu/presentation/ui/login/LoginActivity.kt (3)

29-29: Intent Extra 키 중복 정의 제거 제안

OnboardingActivity에도 동일 문자열 키가 존재합니다. 키 불일치로 인한 런타임 버그를 막기 위해 한 곳에서만 정의/공유하세요.

교차 파일 변경안:

-    companion object {
-        private const val TAG = "LoginActivity"
-        private const val KAKAO_ID = "kakaoId"
-    }
+    companion object {
+        private const val TAG = "LoginActivity"
+    }

아래 putExtra는 OnboardingActivity의 공개 상수를 참조하도록 변경:

-                                .putExtra(KAKAO_ID, kakaoId)
+                                .putExtra(OnboardingActivity.KAKAO_ID, kakaoId)

64-66: ViewModel에 UI 콜백 전달 대신 이벤트로 토스트 처리 권장

UI 사이드이펙트를 ViewModel 인자로 넘기면 수명주기/테스트가 까다롭습니다. 성공 이벤트(예: guestLoginSucceeded)를 SharedFlow로 노출하고 Activity에서 수집해 토스트를 띄우는 구조로 정리하는 것을 추천합니다.

필요하면 이벤트/수집 샘플 코드 드릴게요.


82-86: OnboardingActivity의 상수 재사용

여기서 사용하는 키도 OnboardingActivity의 공개 키를 참조하도록 바꾸면 상수 드리프트를 방지할 수 있습니다. OnboardingActivity 쪽 리뷰 코멘트에 공개 상수로 전환하는 diff를 첨부했습니다.

app/src/main/java/com/example/findu/presentation/ui/onboarding/OnboardingActivity.kt (1)

26-26: Intent Extra 키를 공개 상수로 승격 + 충돌 방지용 네임스페이스 권장

LoginActivity와 키를 공유하려면 private 제거가 필요합니다. 또한 앱 고유 네임스페이스를 포함하면 다른 Extra와의 충돌을 줄일 수 있습니다.

-        private const val KAKAO_ID = "kakaoId"
+        // 다른 모듈/Activity에서도 재사용할 수 있도록 공개 상수로 둡니다.
+        const val KAKAO_ID: String = "com.example.findu.extra.KAKAO_ID"
app/src/main/java/com/example/findu/presentation/ui/login/viewmodel/LoginViewModel.kt (2)

30-36: userInfo!! 강제 언래핑 NPE 가능성

loginData.userInfo가 null일 경우 NPE가 납니다. 계약 보장이 없다면 안전하게 처리하세요.

-                } else {
-                    setAccessTokenUseCase(accessToken = loginData.userInfo!!.accessToken)
-                    startMainActivity()
-                }
+                } else {
+                    val token = loginData.userInfo?.accessToken
+                    if (token.isNullOrBlank()) {
+                        Log.e("LoginViewModel", "AccessToken is null or blank on non-first login")
+                        return@onSuccess
+                    }
+                    setAccessTokenUseCase(accessToken = token)
+                    startMainActivity()
+                }

37-39: 에러 전달 방식 개선 제안

현재 Log만 남기고 끝입니다. UI로 에러 이벤트를 전달해 사용자 피드백(토스트/다이얼로그)을 일관되게 처리하는 것을 추천합니다. 또한 태그/메시지를 구체화하세요.

Also applies to: 52-54

app/src/main/java/com/example/findu/presentation/ui/onboarding/viewmodel/OnboardingViewModel.kt (1)

90-106: 닉네임 중복 체크 후 focusChanged(false) 호출 — 부수효과 재검토

검증 결과에 따라 포커스를 강제로 해제하는 동작이 UX에 따라는 어색할 수 있습니다. 사용자가 바로 수정하려는 흐름을 방해할 수 있어 조건부(VALID일 때만 해제 등)로 바꾸는 것을 고려해보세요.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4f72096 and e7789d1.

📒 Files selected for processing (4)
  • app/src/main/java/com/example/findu/presentation/ui/login/LoginActivity.kt (3 hunks)
  • app/src/main/java/com/example/findu/presentation/ui/login/viewmodel/LoginViewModel.kt (3 hunks)
  • app/src/main/java/com/example/findu/presentation/ui/onboarding/OnboardingActivity.kt (2 hunks)
  • app/src/main/java/com/example/findu/presentation/ui/onboarding/viewmodel/OnboardingViewModel.kt (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/src/main/java/com/example/findu/presentation/ui/onboarding/viewmodel/OnboardingViewModel.kt (1)
app/src/main/java/com/example/findu/presentation/ui/login/viewmodel/LoginViewModel.kt (1)
  • startMainActivity (60-64)
🔇 Additional comments (6)
app/src/main/java/com/example/findu/presentation/ui/onboarding/OnboardingActivity.kt (2)

45-45: Uri 직접 전달로 타입 안전성 개선 — 굿

Uri를 그대로 ViewModel에 전달하고, 초기화 시 null로 클리어하는 흐름이 명확합니다.

Also applies to: 61-61


35-38: 유효하지 않은 kakaoId일 때 조기 종료 추가

intent에서 KAKAO_ID가 -1L이면 finish() 후 return으로 조기 종료하세요 — 그렇지 않으면 잘못된 id가 회원가입으로 넘어갑니다.
파일: app/src/main/java/com/example/findu/presentation/ui/onboarding/OnboardingActivity.kt (라인 35–38)

-        val kakaoId = intent.getLongExtra(KAKAO_ID, -1L)
-        if (kakaoId != -1L) {
-            onboardingViewModel.setKakaoId(kakaoId)
-        }
+        val kakaoId = intent.getLongExtra(KAKAO_ID, -1L)
+        if (kakaoId == -1L) {
+            finish()
+            return
+        }
+        onboardingViewModel.setKakaoId(kakaoId)

참고: 제공한 스크립트 결과가 "No files were searched"라 자동 검증이 불가합니다 — 수동 확인 필요.

app/src/main/java/com/example/findu/presentation/ui/login/viewmodel/LoginViewModel.kt (1)

25-27: 온보딩 이벤트 페이로드(Long)로 전환 — 굿

온보딩 시작 이벤트에 kakaoId를 실어 보내는 변경 방향 적절합니다.

app/src/main/java/com/example/findu/presentation/ui/onboarding/viewmodel/OnboardingViewModel.kt (3)

26-29: 기본 프로필 상태 변경 확인 요청

기본값을 DEFAULT로 바꾸면서 기존 UX(예: 명시적 선택이 없을 때 처리)가 달라질 수 있습니다. 기획과 일치하는지 확인 부탁드립니다.


109-113: 프로필 이미지 상태 업데이트 — 굿

UI 상태를 단일 소스로 관리하는 패턴이 명확합니다.


155-157: kakaoId 주입 경로 정리 — 굿

외부에서 주입해 상태로 보관하는 방식이 명확합니다. 위에서 제안한 유효성 체크만 추가해 주세요.

Copy link
Collaborator

@ikseong00 ikseong00 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 67 to 68
indicatorClicked = indicatorClicked,
selected = homeReportDuration
Copy link
Collaborator

Choose a reason for hiding this comment

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

변수명이 모호한 거 같습니다!
두 변수명이 클릭 이벤트인지, Boolean 값인지, 한 눈에 파악되지 않는 것 같습니다!

Suggested change
indicatorClicked = indicatorClicked,
selected = homeReportDuration
onIndicatorSelected = onIndicatorSelected,
selectedDuration = homeReportDuration

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

🧹 Nitpick comments (2)
app/src/main/java/com/example/findu/data/datalocal/datasourceimpl/DeviceLocalDataSourceImpl.kt (2)

17-19: deviceId getter의 .toString() 호출을 정리하면 코드가 더 명확해집니다.

getString()이 기본값과 함께 호출될 때 null을 반환하지 않지만, .toString() 호출은 불필요하며 잠재적인 문제를 가릴 수 있습니다. 더 명확한 방식으로 처리하는 것을 권장합니다.

다음과 같이 수정할 수 있습니다:

     override var deviceId: String
-        get() = sharedPreferences.getString(DEVICE_ID, INITIAL_VALUE).toString()
+        get() = sharedPreferences.getString(DEVICE_ID, INITIAL_VALUE).orEmpty()
         set(value) = sharedPreferences.edit { putString(DEVICE_ID, value) }

23-27: 상수명과 키 값의 일관성을 고려해보세요.

DEVICE_ID 상수의 실제 키 값이 "token"으로 되어 있어 혼란을 줄 수 있습니다. 키 이름을 "device_id"로 변경하거나, 상수명을 TOKEN으로 변경하여 일관성을 맞추는 것이 좋을 것 같습니다.

예시:

     private companion object {
         const val PREFERENCES_NAME = "device_preferences"
-        const val DEVICE_ID = "token"
+        const val DEVICE_ID = "device_id"
         const val INITIAL_VALUE = ""
     }

또는 상수명을 변경:

     private companion object {
         const val PREFERENCES_NAME = "device_preferences"
-        const val DEVICE_ID = "token"
+        const val TOKEN = "token"
         const val INITIAL_VALUE = ""
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e7789d1 and cbc62a1.

📒 Files selected for processing (1)
  • app/src/main/java/com/example/findu/data/datalocal/datasourceimpl/DeviceLocalDataSourceImpl.kt (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/src/main/java/com/example/findu/data/datalocal/datasourceimpl/DeviceLocalDataSourceImpl.kt (3)
app/src/main/java/com/example/findu/data/datalocal/datasource/DeviceLocalDataSource.kt (1)
  • clear (5-5)
app/src/main/java/com/example/findu/data/repositoryimpl/DeviceRepositoryImpl.kt (1)
  • clear (16-16)
app/src/main/java/com/example/findu/domain/repository/DeviceRepository.kt (1)
  • clear (6-6)
🔇 Additional comments (3)
app/src/main/java/com/example/findu/data/datalocal/datasourceimpl/DeviceLocalDataSourceImpl.kt (3)

10-12: LGTM! 깔끔한 DI 구성입니다.

Hilt 의존성 주입 설정이 잘 되어 있고, ApplicationContext를 올바르게 사용하고 있습니다.


14-15: LGTM! SharedPreferences 초기화가 적절합니다.

MODE_PRIVATE을 사용하여 앱 전용 데이터로 올바르게 설정되었습니다.


21-21: LGTM! clear() 구현이 간결합니다.

androidx.core.content.edit 확장 함수를 활용한 깔끔한 구현입니다.

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

Caution

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

⚠️ Outside diff range comments (1)
app/src/main/java/com/example/findu/presentation/ui/home/composeview/HomeScreen.kt (1)

163-180: Critical: NPE 위험이 있습니다

Line 164와 179에서 uiState.homeData!!로 non-null assertion을 사용하고 있는데, Line 111에서는 homeData가 null일 수 있다고 가정하고 있습니다. 이는 런타임 크래시를 유발할 수 있습니다.

다음과 같이 수정해주세요:

                 HomeProtectAnimalList(
                     nickname = userNickname,
                     navigationToSearch = navigationToProtectAnimal,
-                    animalCards = uiState.homeData!!.protectAnimalCards,
+                    animalCards = uiState.homeData?.protectAnimalCards ?: emptyList(),
                     navigateToProtectDetail = navigateToProtectDetail
                 )
                 HomeReportedAnimalList(
                     nickname = userNickname,
                     navigationToSearch = navigationToReportAnimal,
-                    animalCards = uiState.homeData!!.reportAnimalCards,
+                    animalCards = uiState.homeData?.reportAnimalCards ?: emptyList(),
                     navigateToReportDetail = navigateToReportDetail
                 )
🧹 Nitpick comments (7)
app/src/main/java/com/example/findu/presentation/ui/home/HomeFragment.kt (1)

72-73: TODO 구현 추적 필요

NavigateToProtectListNavigateToReportList 처리가 TODO로 남아있네요. 이슈나 태스크로 추적하고 계신가요?

TODO를 추적할 이슈를 생성하거나 구현 코드를 작성하는 걸 도와드릴까요?

app/src/main/java/com/example/findu/presentation/ui/home/component/HomeReportCard.kt (1)

72-90: getAllStatistics() 중복 호출 최적화 권장

Line 72의 forEachIndexed와 Line 90의 lastIndex 체크에서 getAllStatistics()가 두 번 호출되고 있어요. 렌더링마다 불필요한 연산이 발생할 수 있으니, 로컬 변수에 캐싱하는 것을 권장합니다.

다음과 같이 리팩토링할 수 있습니다:

         Spacer(modifier = Modifier.height(20.dp))
+        val statistics = currentPeriodStatistics.getAllStatistics()
         Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
-            currentPeriodStatistics.getAllStatistics().forEachIndexed { index, (label, count) ->
+            statistics.forEachIndexed { index, (label, count) ->
                 Column(
                     modifier = Modifier.weight(1f),
                     horizontalAlignment = Alignment.CenterHorizontally,
                 ) {
                     Text(
                         text = count.toStringWithComma(),
                         style = FindUTheme.typography.head3SemiBold18,
                         color = FindUTheme.colors.gray6
                     )
                     Spacer(modifier = Modifier.height(10.dp))
                     Text(
                         text = label,
                         style = FindUTheme.typography.captionRegular12,
                         color = FindUTheme.colors.gray5
                     )
                 }
 
-                if (index < currentPeriodStatistics.getAllStatistics().lastIndex) {
+                if (index < statistics.lastIndex) {
                     Spacer(
                         modifier = Modifier
                             .width(1.dp)
app/src/main/java/com/example/findu/presentation/ui/home/composeview/HomeScreen.kt (3)

53-68: 함수 시그니처가 복잡해졌네요 - 추후 리팩토링 고려해보세요

함수 파라미터가 많아지면서 (특히 4개의 새로운 콜백) HomeScreen의 책임이 많아 보입니다. 당장은 괜찮지만, 향후 관련 콜백들을 data class나 interface로 그룹화하는 것을 고려해보세요.

예시:

data class HomeNavigationCallbacks(
    val toProtectAnimal: () -> Unit,
    val toReportAnimal: () -> Unit,
    val toProtectDetail: (ProtectAnimal) -> Unit,
    val toReportDetail: (ReportAnimal) -> Unit
)

data class HomeDialogCallbacks(
    val onDismiss: () -> Unit,
    val onLostReport: () -> Unit,
    val onFindReport: () -> Unit,
    val onPhoneClick: () -> Unit
)

200-213: 다이얼로그 dismiss 로직 중복이 있어요

각 버튼 클릭 핸들러 내부에서 이미 onReportDialogDismiss()를 호출하고 있는데, 이게 꼭 필요한지 확인해보세요. 일반적으로 액션 완료 후 다이얼로그를 닫는 건 상위 레벨(ViewModel 등)에서 처리하는 게 더 깔끔할 수 있습니다.

현재 구조가 의도된 거라면 괜찮지만, 만약 불필요한 중복이라면 다음과 같이 간소화할 수 있습니다:

         if (uiState.isReportDialogVisible) {
             HomeReportDialog(
                 onDismissRequest = onReportDialogDismiss,
-                onLostReportButtonClicked = {
-                    onLostReportClick()
-                    onReportDialogDismiss()
-                },
+                onLostReportButtonClicked = onLostReportClick,
-                onFindReportButtonClicked = {
-                    onFindReportClick()
-                    onReportDialogDismiss()
-                },
+                onFindReportButtonClicked = onFindReportClick,
                 onPhoneClicked = onPhoneClicked
             )
         }

그리고 ViewModel에서 액션 처리 후 isReportDialogVisible = false로 설정하는 방식도 고려해보세요.


219-238: Preview에 실제 데이터를 추가하면 더 좋을 것 같아요

Preview가 새로운 시그니처를 반영하고 있지만, uiState에 실제 샘플 데이터를 전달하지 않아서 화면이 제대로 렌더링되지 않을 수 있습니다.

개발 시 더 나은 프리뷰를 위해 샘플 데이터를 추가하는 것을 추천드립니다:

HomeScreen(
    // ... other params
    uiState = HomeUiState(
        homeData = HomeData(
            statistics = HomeStatistics(/* sample data */),
            protectAnimalCards = listOf(/* sample data */),
            reportAnimalCards = listOf(/* sample data */)
        )
    )
)
app/src/main/java/com/example/findu/data/dataremote/model/response/HomeResponseDto.kt (2)

17-24: 네이밍 일관성을 고려해보세요.

기간 필드명에서 숫자 표기가 혼재되어 있습니다 (recent7days, recent3months, recent1Year). recent7Days처럼 통일하거나 recentSevenDays처럼 영문으로 통일하면 더 일관성 있을 것 같습니다.

백엔드 API 스펙을 따라야 한다면 현재 상태도 괜찮지만, 향후 리팩토링 시 고려해볼 만한 포인트입니다.


39-68: 중복 구조를 검토해보세요.

ProtectingAnimalWitnessedOrMissingAnimal이 동일한 필드 구조를 가지고 있습니다. 두 가지 접근 방법을 고려해볼 수 있습니다:

  1. 공통 데이터 클래스 사용: 하나의 AnimalReport 클래스로 통합하고 타입 구분자 추가
  2. 현재 구조 유지: 백엔드 API가 명시적으로 구분하고 있다면 타입 안정성을 위해 현재처럼 분리 유지

백엔드 API 스펙상 구조가 항상 동일하다면 통합을 고려해보고, 향후 각각 다른 필드가 추가될 가능성이 있다면 현재 구조를 유지하는 것이 좋습니다.

통합하는 경우 예시:

@Serializable
data class AnimalReport(
    @SerialName("reportId")
    val reportId: Int,
    @SerialName("thumbnailImageUrl")
    val thumbnailImageUrl: String,
    @SerialName("title")
    val title: String,
    @SerialName("tag")
    val tag: String,
    @SerialName("happenDate")
    val happenDate: String,
    @SerialName("careAddress")
    val careAddress: String
)

// HomeResponseDto에서 사용
@Serializable
data class HomeResponseDto(
    @SerialName("statistics")
    val statistics: Statistics,
    @SerialName("protectingAnimals")
    val protectingAnimals: List<AnimalReport>,
    @SerialName("witnessedOrMissingAnimals")
    val witnessedOrMissingAnimals: List<AnimalReport>
)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e7789d1 and d4b2507.

📒 Files selected for processing (6)
  • app/src/main/java/com/example/findu/data/datalocal/datasourceimpl/DeviceLocalDataSourceImpl.kt (1 hunks)
  • app/src/main/java/com/example/findu/data/dataremote/model/response/HomeResponseDto.kt (3 hunks)
  • app/src/main/java/com/example/findu/data/mapper/todomain/HomeResponseDtoMapper.kt (1 hunks)
  • app/src/main/java/com/example/findu/presentation/ui/home/HomeFragment.kt (4 hunks)
  • app/src/main/java/com/example/findu/presentation/ui/home/component/HomeReportCard.kt (7 hunks)
  • app/src/main/java/com/example/findu/presentation/ui/home/composeview/HomeScreen.kt (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/main/java/com/example/findu/data/mapper/todomain/HomeResponseDtoMapper.kt
🔇 Additional comments (17)
app/src/main/java/com/example/findu/presentation/ui/home/HomeFragment.kt (4)

75-75: 긴급 신고 기능 구현 확인

Dial 사이드 이펙트로 call120()을 호출하도록 구현되었습니다. 아래 구현부를 확인했을 때 ACTION_DIAL을 사용하여 다이얼러를 여는 방식이라 안전합니다.


120-121: 빈 람다 구현 확인

onLostReportClickonFindReportClick이 빈 람다로 구현되어 있는데, 의도된 건가요? 아니면 추후 구현 예정인가요?

나중에 구현할 예정이라면 TODO 주석을 추가하거나 이슈로 트래킹하는 게 좋을 것 같습니다.


178-181: 120 긴급 전화 기능 구현 완료

ACTION_DIAL을 사용하여 120으로 전화를 걸 수 있도록 다이얼러를 여는 방식으로 구현되었습니다. 사용자가 직접 통화 버튼을 눌러야 하므로 안전하고 적절한 구현입니다.


56-56: animal 객체 기반 내비게이션으로 리팩토링 완료

sideEffect.animal의 프로퍼티들(protectId, reportId, tag, title)을 사용하는 것으로 변경되었네요. 구조가 더 명확해졌습니다.

참고로 protectIdreportId가 null이 아님을 보장하는지 확인해보시면 좋을 것 같아요. .toString()을 사용하고 있어서 혹시 null이 들어오면 "null" 문자열이 될 수 있습니다.

Also applies to: 60-60

app/src/main/java/com/example/findu/presentation/ui/home/component/HomeReportCard.kt (8)

29-30: 도메인 모델 import 추가 확인 완료!

새로운 HomeStatisticsPeriodStatistics 모델로 깔끔하게 전환되었네요.


39-41: 파라미터 네이밍 개선 확인!

homeStatisticsonIndicatorSelected로 변경되면서 이전 리뷰 코멘트에서 지적된 변수명 모호함이 해결되었습니다. 클릭 이벤트 콜백임이 명확하게 드러나네요! 👍


44-48: 기간별 통계 매핑 로직 확인 완료!

when 표현식으로 기간 타입에 따라 적절한 통계 데이터를 선택하는 로직이 명확합니다.


67-68: 파라미터 네이밍 개선 확인!

onIndicatorSelectedselectedDuration으로 변경되면서 이전 리뷰에서 지적된 네이밍 이슈가 해결되었습니다!


105-107: Indicator 컴포넌트 파라미터 개선 확인!

onIndicatorSelectedselectedDuration으로 일관되게 업데이트되었습니다.


117-117: 선택 상태 비교 로직 확인 완료!

selectedDuration 비교 로직이 명확하고 정확합니다.


141-141: 클릭 핸들러 업데이트 확인!

onIndicatorSelected 콜백 사용이 적절합니다.


161-171: Preview 코드 업데이트 확인!

새로운 도메인 모델을 사용하는 Preview가 잘 구성되었습니다. 테스트 데이터도 적절하네요!

app/src/main/java/com/example/findu/presentation/ui/home/composeview/HomeScreen.kt (2)

38-38: LGTM! 다이얼로그 컴포넌트 import 추가

제보 다이얼로그 기능 구현을 위한 import가 적절하게 추가되었습니다.


111-118: LGTM! null-safe한 UI 렌더링

uiState.homeData?.let을 사용해서 안전하게 데이터가 있을 때만 카드를 렌더링하고 있습니다. 좋은 접근입니다!

app/src/main/java/com/example/findu/data/datalocal/datasourceimpl/DeviceLocalDataSourceImpl.kt (2)

10-15: LGTM!

Hilt를 통한 의존성 주입과 SharedPreferences 초기화가 올바르게 구현되어 있습니다.


21-21: LGTM!

clear() 메서드 구현이 정확합니다.

app/src/main/java/com/example/findu/data/dataremote/model/response/HomeResponseDto.kt (1)

6-14: 전체적인 구조가 깔끔합니다!

새로운 API 응답 구조가 명확하게 정의되어 있고, Kotlinx Serialization 어노테이션도 적절하게 사용되었습니다. 통계 데이터를 그룹화한 접근 방식이 좋아 보입니다.

Copy link
Collaborator

@nasohee nasohee left a comment

Choose a reason for hiding this comment

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

👍👍

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

♻️ Duplicate comments (1)
app/src/main/java/com/example/findu/domain/usecase/PostSignupUseCase.kt (1)

17-25: deviceId 공백 검증 재요청 & defaultImageName 처리 개선

이전 리뷰에서 지적된 것처럼 Line 24에서 userInfoRepository.getDeviceId() 결과가 공백이면 가입 요청이 즉시 실패합니다. 여전히 방어 로직이 없어 위험이 그대로 남아 있어요. 동시에 Line 19~21의 defaultImageName 분기는 takeUnless로 간결하게 정리할 수 있습니다.

아래처럼 적용해 주세요:

-    ): Result<UserInfo> = authRepository.postSignup(
-        profileImageFile = profileImageFile,
-        defaultImageName = if (profileImageFile != null) {
-            null
-        } else defaultImageName,
-        nickname = nickname,
-        kakaoId = kakaoId,
-        deviceId = userInfoRepository.getDeviceId()
-    )
+    ): Result<UserInfo> {
+        val deviceId = userInfoRepository.getDeviceId()
+        if (deviceId.isBlank()) {
+            return Result.failure(IllegalStateException("deviceId is blank"))
+        }
+        return authRepository.postSignup(
+            profileImageFile = profileImageFile,
+            defaultImageName = defaultImageName.takeUnless { profileImageFile != null },
+            nickname = nickname,
+            kakaoId = kakaoId,
+            deviceId = deviceId
+        )
+    }
🧹 Nitpick comments (1)
app/src/main/java/com/example/findu/di/UseCaseModule.kt (1)

194-197: 프로바이더 이름을 반환 타입에 맞춰 주세요

메서드명이 이메일 검사로 남아 있어 가독성이 떨어집니다. 닉네임 검사로 이름을 맞춰 주세요.

-    fun providePostCheckEmailUseCase(
+    fun providePostCheckNicknameUseCase(
         authRepository: AuthRepository
     ): PostCheckNicknameUseCase = PostCheckNicknameUseCase(authRepository)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d4b2507 and e340e80.

📒 Files selected for processing (15)
  • app/src/main/java/com/example/findu/data/datalocal/datasource/DeviceLocalDataSource.kt (1 hunks)
  • app/src/main/java/com/example/findu/data/datalocal/datasourceimpl/DeviceLocalDataSourceImpl.kt (1 hunks)
  • app/src/main/java/com/example/findu/data/repositoryimpl/UserInfoRepositoryImpl.kt (1 hunks)
  • app/src/main/java/com/example/findu/di/RepositoryModule.kt (3 hunks)
  • app/src/main/java/com/example/findu/di/UseCaseModule.kt (4 hunks)
  • app/src/main/java/com/example/findu/domain/repository/UserInfoRepository.kt (1 hunks)
  • app/src/main/java/com/example/findu/domain/usecase/GetNicknameUseCase.kt (1 hunks)
  • app/src/main/java/com/example/findu/domain/usecase/PostGuestLoginUseCase.kt (1 hunks)
  • app/src/main/java/com/example/findu/domain/usecase/PostLoginUseCase.kt (1 hunks)
  • app/src/main/java/com/example/findu/domain/usecase/PostSignupUseCase.kt (1 hunks)
  • app/src/main/java/com/example/findu/domain/usecase/SetDeviceIdUseCase.kt (1 hunks)
  • app/src/main/java/com/example/findu/domain/usecase/SetNicknameUseCase.kt (1 hunks)
  • app/src/main/java/com/example/findu/presentation/ui/home/HomeFragment.kt (4 hunks)
  • app/src/main/java/com/example/findu/presentation/ui/home/viewmodel/HomeViewModel.kt (7 hunks)
  • app/src/main/java/com/example/findu/presentation/ui/login/viewmodel/LoginViewModel.kt (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/main/java/com/example/findu/data/datalocal/datasource/DeviceLocalDataSource.kt
🧰 Additional context used
🧬 Code graph analysis (2)
app/src/main/java/com/example/findu/presentation/ui/home/HomeFragment.kt (1)
app/src/main/java/com/example/findu/presentation/ui/home/viewmodel/HomeViewModel.kt (2)
  • navigateToProtectDetail (185-189)
  • navigateToReportDetail (191-195)
app/src/main/java/com/example/findu/data/datalocal/datasourceimpl/DeviceLocalDataSourceImpl.kt (3)
app/src/main/java/com/example/findu/data/datalocal/datasource/DeviceLocalDataSource.kt (1)
  • clear (7-7)
app/src/main/java/com/example/findu/data/repositoryimpl/UserInfoRepositoryImpl.kt (1)
  • clear (22-22)
app/src/main/java/com/example/findu/domain/repository/UserInfoRepository.kt (1)
  • clear (8-8)

Comment on lines 17 to 24
override var deviceId: String
get() = sharedPreferences.getString(DEVICE_ID, INITIAL_VALUE).toString()
set(value) = sharedPreferences.edit { putString(DEVICE_ID, value) }


override var nickname: String
get() = sharedPreferences.getString(NICKNAME, INITIAL_VALUE).toString()
set(value) = sharedPreferences.edit { putString(NICKNAME, value) }
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

null 문자열 반환 버그 수정 필요

SharedPreferences에서 null을 받으면 "null" 문자열이 저장되어 흐름이 꼬입니다. 기본값으로 떨어지게 엘비스 처리해주세요.

-        get() = sharedPreferences.getString(DEVICE_ID, INITIAL_VALUE).toString()
+        get() = sharedPreferences.getString(DEVICE_ID, INITIAL_VALUE) ?: INITIAL_VALUE
@@
-        get() = sharedPreferences.getString(NICKNAME, INITIAL_VALUE).toString()
+        get() = sharedPreferences.getString(NICKNAME, INITIAL_VALUE) ?: INITIAL_VALUE
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/findu/data/datalocal/datasourceimpl/DeviceLocalDataSourceImpl.kt
around lines 17 to 24, the getters use
sharedPreferences.getString(...).toString() which turns a null into the literal
"null"; change each getter to use the Elvis operator to return INITIAL_VALUE
when getString returns null (e.g. sharedPreferences.getString(DEVICE_ID,
INITIAL_VALUE) ?: INITIAL_VALUE) so the default is used instead of the "null"
string, leaving setters as-is.

Comment on lines 11 to 13
suspend fun postGuestLogin(): Result<GuestLoginData> = authRepository.postGuestLogin(
deviceId = userInfoRepository.getDeviceId()
)
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

deviceId 공백 방어가 필요합니다

Line 12에서 userInfoRepository.getDeviceId()가 빈 문자열을 돌려줄 경우 그대로 API에 전달되어 게스트 로그인 요청이 400/422로 실패할 수 있습니다. 로컬 데이터가 초기화되거나 아직 세팅되지 않은 시나리오를 고려해 isBlank 검증 후 명시적으로 실패를 반환하거나, 기본값을 생성하는 처리가 필요합니다.

다음과 같이 방어 로직을 추가해 주세요:

-    suspend fun postGuestLogin(): Result<GuestLoginData> = authRepository.postGuestLogin(
-        deviceId = userInfoRepository.getDeviceId()
-    )
+    suspend fun postGuestLogin(): Result<GuestLoginData> {
+        val deviceId = userInfoRepository.getDeviceId()
+        if (deviceId.isBlank()) {
+            return Result.failure(IllegalStateException("deviceId is blank"))
+        }
+        return authRepository.postGuestLogin(
+            deviceId = deviceId
+        )
+    }
📝 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
suspend fun postGuestLogin(): Result<GuestLoginData> = authRepository.postGuestLogin(
deviceId = userInfoRepository.getDeviceId()
)
suspend fun postGuestLogin(): Result<GuestLoginData> {
val deviceId = userInfoRepository.getDeviceId()
if (deviceId.isBlank()) {
return Result.failure(IllegalStateException("deviceId is blank"))
}
return authRepository.postGuestLogin(
deviceId = deviceId
)
}

Comment on lines 11 to 18
suspend fun postLogin(
loginInfo:LoginInfo
kakaoId:Long
): Result<LoginData> = authRepository.postLogin(
loginInfo = loginInfo
loginInfo = LoginInfo(
kakaoId = kakaoId,
deviceId = userInfoRepository.getDeviceId()
)
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

deviceId 검증 없이 로그인 요청이 나가고 있어요

Line 16에서 userInfoRepository.getDeviceId() 값이 공백이면 서버로 잘못된 deviceId가 전달되어 로그인 요청이 실패할 위험이 있습니다. 게스트 로그인과 동일하게 사전 검증을 통해 빈 값이면 명시적으로 실패를 반환하거나 대체 값을 생성해야 합니다.

아래와 같은 형태로 방어 코드를 추가해 주세요:

-    ): Result<LoginData> = authRepository.postLogin(
-        loginInfo = LoginInfo(
-            kakaoId = kakaoId,
-            deviceId = userInfoRepository.getDeviceId()
-        )
-    )
+    ): Result<LoginData> {
+        val deviceId = userInfoRepository.getDeviceId()
+        if (deviceId.isBlank()) {
+            return Result.failure(IllegalStateException("deviceId is blank"))
+        }
+        return authRepository.postLogin(
+            loginInfo = LoginInfo(
+                kakaoId = kakaoId,
+                deviceId = deviceId
+            )
+        )
+    }
📝 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
suspend fun postLogin(
loginInfo:LoginInfo
kakaoId:Long
): Result<LoginData> = authRepository.postLogin(
loginInfo = loginInfo
loginInfo = LoginInfo(
kakaoId = kakaoId,
deviceId = userInfoRepository.getDeviceId()
)
suspend fun postLogin(
kakaoId:Long
): Result<LoginData> {
val deviceId = userInfoRepository.getDeviceId()
if (deviceId.isBlank()) {
return Result.failure(IllegalStateException("deviceId is blank"))
}
return authRepository.postLogin(
loginInfo = LoginInfo(
kakaoId = kakaoId,
deviceId = deviceId
)
)
}
🤖 Prompt for AI Agents
In app/src/main/java/com/example/findu/domain/usecase/PostLoginUseCase.kt around
lines 12 to 18, the code calls userInfoRepository.getDeviceId() without
validating it; add defensive validation so that if deviceId is null or blank you
either return a failed Result<LoginData> immediately (with an appropriate
error/exception) or generate/obtain a fallback deviceId before calling
authRepository.postLogin, and ensure the chosen approach matches guest-login
behavior and preserves suspend semantics.

Comment on lines +72 to +74
is HomeUiEffect.NavigateToProtectList -> TODO()
is HomeUiEffect.NavigateToReportList -> TODO()

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

TODO 그대로 두면 런타임 크래시나요

HomeViewModel.navigateToProtectList() / navigateToReportList()가 이미 호출되고 있어서 사용자가 리스트 이동 버튼을 누르면 이 분기까지 바로 들어옵니다. 여기서 TODO()가 실행되면 NotImplementedError가 터져 앱이 즉시 죽어요. 최소한 릴리스 전에 실제 내비게이션을 구현하거나, 당장 연결할 화면이 없다면 임시로라도 안전한 처리(예: 토스트나 return)로 바꿔서 크래시를 막아주세요.

🤖 Prompt for AI Agents
In app/src/main/java/com/example/findu/presentation/ui/home/HomeFragment.kt
around lines 72 to 74, the UI effect branches for
HomeUiEffect.NavigateToProtectList and HomeUiEffect.NavigateToReportList
currently call TODO(), which throws NotImplementedError at runtime; replace each
TODO() with a safe navigation action (call the appropriate NavController
navigation to the ProtectList and ReportList destinations) or, if the screens
are not ready, replace with a non-crashing fallback such as showing a
Toast/snackbar or simply returning so the app does not crash; ensure the chosen
replacement is compiled for release (no TODOs) and preserves any required
arguments from the effect.

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

Caution

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

⚠️ Outside diff range comments (3)
app/src/main/java/com/example/findu/presentation/ui/home/composeview/HomeScreen.kt (2)

162-169: NPE 위험: uiState.homeData!! 강제 언래핑 제거 필요 (보호동물 리스트)

초기 로딩/프리뷰에서 homeData가 null일 수 있어 크래시 납니다. 안전하게 처리해주세요.

-                    HomeProtectAnimalList(
+                    HomeProtectAnimalList(
                         nickname = userNickname,
                         homeUserStatusType = uiState.userHomeUserStatusType,
                         navigationToSearch = navigationToProtectAnimal,
-                        animalCards = uiState.homeData!!.protectAnimalCards,
+                        animalCards = uiState.homeData?.protectAnimalCards.orEmpty(),
                         navigateToProtectDetail = navigateToProtectDetail
                     )

86-92: 배너 자동 스크롤: bannerList가 비어있으면 산술 예외 위험(size=0, % 0)

빈 목록 방어 로직을 추가하세요.

-    LaunchedEffect(pagerState) {
+    LaunchedEffect(pagerState) {
+        if (bannerList.isEmpty()) return@LaunchedEffect
         while (true) {
             delay(3000)
             val nextPage = (pagerState.currentPage + 1) % bannerList.size
             pagerState.animateScrollToPage(nextPage)
         }
     }
app/src/main/java/com/example/findu/presentation/ui/home/viewmodel/HomeViewModel.kt (1)

85-93: 닉네임 로딩(setUserInfo) 호출 누락

setUserInfo가 정의만 되어 있고 호출되지 않습니다. 시작 시점에 호출해 UI 상태를 채워주세요.

     val uiState = _uiState
         .onStart {
             handleEvent(HomeUiEvent.LoadHomeData)
+            setUserInfo()
         }
         .stateIn(
             scope = viewModelScope,
             started = SharingStarted.WhileSubscribed(5_000),
             initialValue = HomeUiState()
         )
♻️ Duplicate comments (1)
app/src/main/java/com/example/findu/presentation/ui/home/HomeFragment.kt (1)

81-84: TODO() 그대로 두면 바로 크래시 납니다
UI에서 보호/신고 리스트 이동 액션을 누르면 ViewModel이 이미 NavigateToProtectList / NavigateToReportList 이펙트를 쏘고 있고, 여기서 TODO()가 실행되면 NotImplementedError 때문에 앱이 즉시 죽어요. 실제 내비게이션 로직을 구현하거나, 최소한 크래시 나지 않는 임시 처리(토스트/return 등)로 빨리 교체해 주세요.

🧹 Nitpick comments (5)
app/src/main/java/com/example/findu/presentation/util/permission/LocationPermissionManager.kt (1)

30-36: 앱 설정 열기 구현이 정확합니다.

Intent 설정이 올바르며, FLAG_ACTIVITY_NEW_TASK를 적절히 사용하여 다양한 컨텍스트에서 호출 가능합니다.

선택적 개선사항: 공개 함수들에 KDoc 주석을 추가하면 다른 개발자들이 사용법을 더 쉽게 이해할 수 있습니다.

+/**
+ * 앱의 위치 권한이 모두 허용되었는지 확인합니다.
+ * @return 모든 위치 권한이 허용된 경우 true, 그렇지 않으면 false
+ */
 fun Context.hasLocationPermission(): Boolean {
     return locationPermissions.all { permission ->
         ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
     }
 }

+/**
+ * 위치 권한 요청 다이얼로그를 표시합니다.
+ * @param launcher 권한 요청을 처리할 ActivityResultLauncher
+ */
 fun requestLocationPermission(launcher: ActivityResultLauncher<Array<String>>) {
     launcher.launch(locationPermissions)
 }

+/**
+ * 앱의 시스템 설정 화면을 엽니다.
+ * 사용자가 권한을 직접 변경할 수 있도록 안내합니다.
+ */
 fun Context.openAppSettings() {
     val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
         data = Uri.fromParts("package", packageName, null)
         addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
     }
     startActivity(intent)
 }
app/src/main/java/com/example/findu/presentation/ui/login/viewmodel/LoginViewModel.kt (2)

24-30: 이벤트 SharedFlow가 미구독 시 suspend될 수 있습니다 — 버퍼 설정 권장

one‑shot 이벤트는 버퍼(capacity) 또는 replay=0+extraBufferCapacity로 유실·일시중단을 피하는 게 안전합니다.

-    private val _startMainActivity = MutableSharedFlow<Unit>()
+    private val _startMainActivity = MutableSharedFlow<Unit>(replay = 0, extraBufferCapacity = 1)
     val startMainActivity: SharedFlow<Unit> = _startMainActivity
@@
-    private val _startOnboardingActivity = MutableSharedFlow<Long>()
+    private val _startOnboardingActivity = MutableSharedFlow<Long>(replay = 0, extraBufferCapacity = 1)
     val startOnboardingActivity: SharedFlow<Long> = _startOnboardingActivity

66-75: emit 대신 tryEmit 사용으로 불필요한 일시중단 방지

버퍼를 두더라도 tryEmit이 더 안전합니다.

     private fun startMainActivity() {
         viewModelScope.launch {
-            _startMainActivity.emit(Unit)
+            _startMainActivity.tryEmit(Unit)
         }
     }
@@
     private fun startOnboardingActivity(kakaoId: Long) {
         viewModelScope.launch {
-            _startOnboardingActivity.emit(kakaoId)
+            _startOnboardingActivity.tryEmit(kakaoId)
         }
     }
app/src/main/java/com/example/findu/presentation/ui/home/viewmodel/HomeViewModel.kt (2)

37-42: 게스트 판별 대소문자 무시 권장

equals에서 ignoreCase=false는 불필요할 수 있습니다. true 권장.

-            nickname.isEmpty() || nickname.equals(GUEST_NAME, ignoreCase = false) -> HomeUserStatusType.GUEST
+            nickname.isEmpty() || nickname.equals(GUEST_NAME, ignoreCase = true) -> HomeUserStatusType.GUEST

82-84: UI 효과 채널: 미구독 시 send가 suspend — BUFFERED + trySend 권장

Channel.BUFFERED와 trySend로 backpressure를 완화하세요.

-    private val _uiEffect = Channel<HomeUiEffect>()
+    private val _uiEffect = Channel<HomeUiEffect>(capacity = Channel.BUFFERED)
-            _uiEffect.send(HomeUiEffect.NavigateToProtectList)
+            _uiEffect.trySend(HomeUiEffect.NavigateToProtectList)
@@
-            _uiEffect.send(HomeUiEffect.NavigateToReportList)
+            _uiEffect.trySend(HomeUiEffect.NavigateToReportList)
@@
-            _uiEffect.send(HomeUiEffect.NavigateToProtectDetail(animal))
+            _uiEffect.trySend(HomeUiEffect.NavigateToProtectDetail(animal))
@@
-            _uiEffect.send(HomeUiEffect.NavigateToReportDetail(animal))
+            _uiEffect.trySend(HomeUiEffect.NavigateToReportDetail(animal))
@@
-            _uiEffect.send(HomeUiEffect.Dial)
+            _uiEffect.trySend(HomeUiEffect.Dial)

Also applies to: 192-221

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e340e80 and 3c2ba0e.

📒 Files selected for processing (10)
  • app/src/main/java/com/example/findu/presentation/type/HomeUserStatusType.kt (1 hunks)
  • app/src/main/java/com/example/findu/presentation/ui/home/HomeFragment.kt (7 hunks)
  • app/src/main/java/com/example/findu/presentation/ui/home/component/HomeProtectAnimalList.kt (3 hunks)
  • app/src/main/java/com/example/findu/presentation/ui/home/component/HomeReportedAnimalList.kt (3 hunks)
  • app/src/main/java/com/example/findu/presentation/ui/home/composeview/HomeScreen.kt (6 hunks)
  • app/src/main/java/com/example/findu/presentation/ui/home/viewmodel/HomeViewModel.kt (3 hunks)
  • app/src/main/java/com/example/findu/presentation/ui/login/viewmodel/LoginViewModel.kt (3 hunks)
  • app/src/main/java/com/example/findu/presentation/util/Constraints.kt (1 hunks)
  • app/src/main/java/com/example/findu/presentation/util/permission/LocationPermissionManager.kt (1 hunks)
  • app/src/main/res/values/strings.xml (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
app/src/main/java/com/example/findu/presentation/ui/home/HomeFragment.kt (1)
app/src/main/java/com/example/findu/presentation/ui/home/viewmodel/HomeViewModel.kt (2)
  • navigateToProtectDetail (204-208)
  • navigateToReportDetail (210-214)
app/src/main/java/com/example/findu/presentation/ui/home/composeview/HomeScreen.kt (2)
app/src/main/java/com/example/findu/presentation/ui/home/component/HomeReportCard.kt (1)
  • HomeReportCard (37-101)
app/src/main/java/com/example/findu/presentation/ui/home/component/HomeReportDialog.kt (1)
  • HomeReportDialog (28-124)
🔇 Additional comments (6)
app/src/main/java/com/example/findu/presentation/util/permission/LocationPermissionManager.kt (3)

15-18: 위치 권한 정의가 적절합니다.

정밀 위치(FINE)와 대략적 위치(COARSE) 권한을 모두 포함하여 다양한 위치 기반 시나리오를 지원합니다.


20-24: 권한 확인 로직이 올바르게 구현되었습니다.

모든 필수 권한을 all로 확인하고 ContextCompat을 사용하여 호환성을 보장합니다.


26-28: 권한 요청 함수가 간결하고 정확합니다.

ActivityResultLauncher를 활용한 현대적인 권한 요청 방식을 따릅니다.

app/src/main/java/com/example/findu/presentation/ui/login/viewmodel/LoginViewModel.kt (1)

28-30: Onboarding 플로우 페이로드(Long)로 변경 — 구독 측 업데이트 확인 요청

해당 Flow를 수집하는 화면/네비게이션 코드가 Long(kakaoId)을 처리하도록 모두 갱신됐는지 확인 부탁드립니다.

app/src/main/java/com/example/findu/presentation/ui/home/composeview/HomeScreen.kt (1)

164-166: 컴포넌트 시그니처(navigationToSearch) 일치 여부 수동 확인 요청
HomeProtectAnimalList 및 HomeReportedAnimalList 정의부에서 navigationToSearch 파라미터를 찾지 못했습니다. 실제 파라미터명과 타입이 호출부와 일치하는지 직접 확인해 주세요.

app/src/main/java/com/example/findu/presentation/ui/home/viewmodel/HomeViewModel.kt (1)

124-139: GetHomeUseCase invoke 기본값 OK
lat, lon 모두 기본 null이라 인자 없이 호출해도 문제없어요.

Comment on lines 177 to 183
HomeReportedAnimalList(
nickname = userNickname,
navigationToSearch = navigationToSearch,
homeUserStatusType = uiState.userHomeUserStatusType,
navigationToSearch = navigationToReportAnimal,
animalCards = uiState.homeData!!.reportAnimalCards,
navigateToReportDetail = navigateToReportDetail
)
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

NPE 위험: uiState.homeData!! 강제 언래핑 제거 필요 (제보동물 리스트)

동일하게 안전 처리 권장합니다.

-                    HomeReportedAnimalList(
+                    HomeReportedAnimalList(
                         nickname = userNickname,
                         homeUserStatusType = uiState.userHomeUserStatusType,
                         navigationToSearch = navigationToReportAnimal,
-                        animalCards = uiState.homeData!!.reportAnimalCards,
+                        animalCards = uiState.homeData?.reportAnimalCards.orEmpty(),
                         navigateToReportDetail = navigateToReportDetail
                     )
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/findu/presentation/ui/home/composeview/HomeScreen.kt
around lines 177 to 183, remove the unsafe force-unwrapping of
uiState.homeData!! when passing reportAnimalCards; instead guard against null by
either returning/omitting the HomeReportedAnimalList when homeData is null or
use a safe call with a default (e.g., uiState.homeData?.reportAnimalCards ?:
emptyList()) so the composable receives a non-null list; update the surrounding
conditional logic to only render HomeReportedAnimalList when homeData is present
or provide a sensible fallback.

Comment on lines 37 to 40
setAccessTokenUseCase(accessToken = loginData.userInfo!!.accessToken)
setNicknameUseCase(nickname = loginData.userInfo.nickname)
Log.d("http", "nickname: ${loginData.userInfo.nickname}")
startMainActivity()
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

NPE 위험: userInfo를 강제 언래핑(!!)하고 있습니다

비(첫 로그인) 경로에서 userInfo가 null이면 크래시 납니다. null‑세이프 가드로 처리해주세요.

적용 제안:

-                    setAccessTokenUseCase(accessToken = loginData.userInfo!!.accessToken)
-                    setNicknameUseCase(nickname = loginData.userInfo.nickname)
-                    Log.d("http", "nickname: ${loginData.userInfo.nickname}")
+                    val userInfo = loginData.userInfo
+                    if (userInfo == null) {
+                        Log.e("http", "loginData.userInfo is null on non-first login")
+                        return@onSuccess
+                    }
+                    setAccessTokenUseCase(accessToken = userInfo.accessToken)
+                    setNicknameUseCase(nickname = userInfo.nickname)
+                    Log.d("http", "nickname: ${userInfo.nickname}")
📝 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
setAccessTokenUseCase(accessToken = loginData.userInfo!!.accessToken)
setNicknameUseCase(nickname = loginData.userInfo.nickname)
Log.d("http", "nickname: ${loginData.userInfo.nickname}")
startMainActivity()
// guard against a null userInfo to avoid NPE
val userInfo = loginData.userInfo
if (userInfo == null) {
Log.e("http", "loginData.userInfo is null on non-first login")
return@onSuccess
}
setAccessTokenUseCase(accessToken = userInfo.accessToken)
setNicknameUseCase(nickname = userInfo.nickname)
Log.d("http", "nickname: ${userInfo.nickname}")
startMainActivity()
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/findu/presentation/ui/login/viewmodel/LoginViewModel.kt
around lines 37 to 40, the code force-unwraps loginData.userInfo (!!) which can
cause an NPE on non-first-login or when userInfo is null; replace the forced
unwrap with a null-safety guard: check if loginData.userInfo is null and if so
handle the error/early-return or show an appropriate user-visible error/log,
otherwise call setAccessTokenUseCase and setNicknameUseCase with the safe values
and then startMainActivity; ensure nickname access uses safe access or a
non-null assertion only after the null check and log diagnostic information when
userInfo is missing.

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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3c2ba0e and 2ed57f4.

📒 Files selected for processing (1)
  • app/src/main/java/com/example/findu/presentation/ui/home/viewmodel/HomeViewModel.kt (3 hunks)

Comment on lines +191 to 194
private fun alarmButtonClicked() {
//TODO: 추후 기능 추가
}

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

전화 신고 이벤트가 동작하지 않습니다

OnAlarmButtonClick 이벤트가 alarmButtonClicked()에서 TODO로 끝나 현재 Dial 이펙트가 발행되지 않습니다. 버튼을 눌러도 아무 반응이 없어 이번 PR 목표(전화 신고 추가)가 달성되지 않습니다. 최소한 Dial 이펙트를 전송하도록 구현이 필요합니다.

다음과 같이 연결하면 해결됩니다.

     private fun alarmButtonClicked() {
-        //TODO: 추후 기능 추가
+        dial()
     }
📝 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 alarmButtonClicked() {
//TODO: 추후 기능 추가
}
private fun alarmButtonClicked() {
dial()
}

# Conflicts:
#	app/src/main/java/com/example/findu/data/dataremote/util/Constraints.kt
#	app/src/main/java/com/example/findu/di/UseCaseModule.kt
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

🧹 Nitpick comments (1)
app/src/main/java/com/example/findu/di/UseCaseModule.kt (1)

202-205: 제공 함수 이름과 실제 용도 일치화 제안
함수 이름이 Email을 가리키지만 반환 타입은 닉네임 체크용이라 헷갈릴 수 있어요. 이번 기회에 이름을 맞춰두면 가독성이 더 좋아질 듯합니다.

-    fun providePostCheckEmailUseCase(
+    fun providePostCheckNicknameUseCase(
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2ed57f4 and 65b3d96.

📒 Files selected for processing (3)
  • app/src/main/java/com/example/findu/data/dataremote/util/Constraints.kt (1 hunks)
  • app/src/main/java/com/example/findu/di/UseCaseModule.kt (3 hunks)
  • app/src/main/res/values/strings.xml (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/src/main/java/com/example/findu/data/dataremote/util/Constraints.kt
  • app/src/main/res/values/strings.xml
🔇 Additional comments (2)
app/src/main/java/com/example/findu/di/UseCaseModule.kt (2)

188-211: UserInfoRepository 주입으로 흐름이 정리되었네요.
로그인·회원가입 계열 use case들이 같은 저장 책임을 공유하게 되어 구성 파악이 한결 수월합니다.


249-259: 로컬 닉네임 UseCase 바인딩 좋습니다
로컬 저장소 전용 use case를 분리해 둬서 ViewModel 쪽 의존성을 깔끔하게 유지하기 훨씬 쉬워질 것 같아요.

@t1nm1ksun t1nm1ksun merged commit f401891 into develop Oct 10, 2025
1 check passed
@t1nm1ksun t1nm1ksun deleted the feat-home-api branch October 10, 2025 06:51
@coderabbitai coderabbitai bot mentioned this pull request Oct 10, 2025
1 task
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] 홈 api 연동

3 participants