Skip to content

Comments

[Feat] 온보딩 관련 API 연동#81

Merged
t1nm1ksun merged 20 commits intodevelopfrom
feat-onboarding-api
Sep 17, 2025
Merged

[Feat] 온보딩 관련 API 연동#81
t1nm1ksun merged 20 commits intodevelopfrom
feat-onboarding-api

Conversation

@t1nm1ksun
Copy link
Collaborator

@t1nm1ksun t1nm1ksun commented Sep 2, 2025

Related issue 🛠

Work Description 📝

  • 닉네임 중복체크 API 연동
  • 회원가입 API 연동
    • 유저 갤러리에서 프로필 사진 선택
    • 디바이스 ID 로컬에서관리
    • 기존 api 로직 삭제
    • 이전 pr에 있던 nullable 응답관련 내용 수정

Screenshot 📸

Uncompleted Tasks 😅

  • N/A

To Reviewers 📢

  1. 현재 서버측에서 멀티파트로 전송한 이미지의 처리가 되지 않아 기본이미지로만 가입이 가능한 상태입니다.
    클라측 로직상 서버측 문제가 해결되면 별 문제 없을것으로 예상되지만 추후 문제가 생긴다면 이슈 파서 해결하도록 하겠습니다!

  2. 영상이나 스크린샷을 첨부하고 싶은데 한번 회원가입 성공하니까 포스트맨으로 회원탈퇴 api를 쏴서 응답이 200으로 성공해도 왜인지 탈퇴가 되지 않아 찍을 수가 없네요..

Summary by CodeRabbit

  • 신기능

    • 게스트 로그인 지원 추가.
    • 닉네임 중복 확인 기능 도입.
    • 회원가입 시 프로필 이미지 업로드 및 기본 이미지 선택 지원.
  • 변경

    • 로그인 방식을 이메일/비밀번호에서 카카오 ID + 기기 연동으로 전환.
    • 온보딩 프로필 이미지 입력을 Uri 기반으로 변경, 기본 프로필 옵션명 업데이트(DEFAULT, PUPPY 등).
    • 온보딩 Next 버튼 활성화 로직 개선(페이지 단계 및 닉네임 유효성 반영).
  • 리팩터링

    • 기존 이메일 기반 회원가입 화면 및 가입 완료 화면 제거로 흐름 단순화.

@coderabbitai
Copy link

coderabbitai bot commented Sep 2, 2025

Walkthrough

인증/온보딩 전반 리팩터링. 카카오/게스트 로그인 도입, 닉네임 중복검사·회원가입 API 전환 및 DTO/응답 래퍼 변경, 멀티파트 회원가입 추가. 디바이스 ID 로컬 저장/주입 체계 신설. 온보딩 UI는 Uri 기반 이미지 처리와 닉네임 검증으로 수정. 구(이메일) 가입 흐름 제거.

Changes

Cohort / File(s) Summary
Device ID local storage & DI
app/.../datalocal/datasource/DeviceLocalDataSource.kt, app/.../datalocal/datasourceimpl/DeviceLocalDataSourceImpl.kt, app/.../domain/repository/DeviceRepository.kt, app/.../data/repositoryimpl/DeviceRepositoryImpl.kt, app/.../di/DataSourceModule.kt, app/.../di/RepositoryModule.kt, app/.../di/SharedPreferencesModule.kt, app/.../di/qualifier/Qualifier.kt, app/.../domain/usecase/SetDeviceIdUseCase.kt, app/.../presentation/ui/main/MainActivity.kt
디바이스 ID 로컬 데이터소스/레포지토리/유즈케이스 추가 및 Hilt 바인딩. 새로운 SharedPreferences(@DeviceIdPrefs) 제공. MainActivity에서 ANDROID_ID 저장. 기존 TokenPrefs Qualifier 해제, DeviceIdPrefs 신설.
Auth remote API overhaul
app/.../dataremote/datasource/AuthRemoteDataSource.kt, app/.../dataremote/datasourceimpl/AuthRemoteDataSourceImpl.kt, app/.../dataremote/service/AuthService.kt, app/.../dataremote/model/request/CheckEmailRequestDto.kt, app/.../dataremote/model/response/CheckEmailResponseDto.kt, app/.../dataremote/util/ApiResponseHandler.kt, app/.../data/mapper/torequest/TextRequestBodyMapper.kt
이메일 기반 API 제거/대체. 카카오 로그인/게스트 로그인/닉네임 중복검사/멀티파트 회원가입 추가. BaseResponse 코드 범위 3자리 HTTP 스타일로 변경. 텍스트/이미지 RequestBody 매퍼 추가.
Auth repository & mappers
app/.../data/repositoryimpl/AuthRepositoryImpl.kt, app/.../domain/repository/AuthRepository.kt, app/.../data/mapper/todomain/LoginResponseDtoMapper.kt, app/.../data/mapper/todomain/UserInfoDtoMapper.kt, app/.../domain/model/AuthData.kt
레포 인터페이스/구현을 새 DTO/도메인 모델(LoginInfo, LoginData, GuestLoginData, UserInfo)에 맞게 변경. 이메일 체크/가입 제거, 게스트 로그인·닉네임 체크·확장 회원가입 추가. 로그인/회원정보 매퍼 추가.
Use cases wiring
app/.../di/UseCaseModule.kt, app/.../domain/usecase/PostLoginUseCase.kt, app/.../domain/usecase/PostGuestLoginUseCase.kt, app/.../domain/usecase/PostSignupUseCase.kt, app/.../domain/usecase/PostCheckNicknameUseCase.kt
로그인 유즈케이스를 카카오ID+디바이스ID 입력으로 변경. 게스트 로그인·닉네임 중복검사 유즈케이스 추가. 회원가입 유즈케이스 멀티파트/디바이스ID 대응. DI 모듈 갱신.
Login presentation
app/.../presentation/ui/login/LoginActivity.kt, app/.../presentation/ui/login/viewmodel/LoginViewModel.kt
카카오 SDK로 user.id 조회 후 로그인 호출. 게스트 로그인 버튼 흐름 변경. ViewModel은 로그인 결과에 따라 온보딩/메인 분기, 게스트 로그인 처리 추가.
Onboarding UI/VM updates
app/.../presentation/type/DefaultProfileType.kt, app/.../presentation/ui/onboarding/OnboardingActivity.kt, app/.../presentation/ui/onboarding/component/OnboardingProfile.kt, app/.../presentation/ui/onboarding/component/OnboardingProfileBox.kt, app/.../presentation/ui/onboarding/composeview/OnboardingScreen.kt, app/.../presentation/ui/onboarding/viewmodel/OnboardingViewModel.kt
프로필 이미지 입력을 String→Uri?로 변경. 기본 프로필 enum 개명/문자열 값 추가. 다음 버튼 활성 로직을 페이지/닉네임 유효성 기반으로 변경. ViewModel에 닉네임 중복검사/회원가입(파일 변환 포함) 추가.
Legacy signup removal
app/.../presentation/ui/signup/SignupActivity.kt, app/.../presentation/ui/signup/SignupSuccessActivity.kt, app/.../presentation/ui/signup/viewmodel/SignupViewModel.kt
이메일 기반 가입 화면/뷰모델/성공 화면 삭제.
Minor refactors
app/.../datalocal/datasourceimpl/TokenLocalDataSourceImpl.kt, app/.../data/mapper/toDomain/CheckEmailResponseDtoMapper.kt
SharedPreferences edit 람다 사용으로 정리. 이메일중복 응답 매퍼 제거.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant LoginActivity
  participant KakaoSDK as Kakao UserApiClient
  participant LoginVM as LoginViewModel
  participant PostLoginUC as PostLoginUseCase
  participant AuthRepo as AuthRepository
  participant Remote as AuthService
  participant DeviceRepo as DeviceRepository

  User->>LoginActivity: 카카오 로그인 버튼
  LoginActivity->>KakaoSDK: me()
  KakaoSDK-->>LoginActivity: kakaoId
  LoginActivity->>LoginVM: postLogin(kakaoId)
  LoginVM->>PostLoginUC: postLogin(kakaoId)
  PostLoginUC->>DeviceRepo: getDeviceId()
  PostLoginUC->>AuthRepo: postLogin(LoginInfo(kakaoId, deviceId))
  AuthRepo->>Remote: POST /auth/login (LoginRequestDto)
  Remote-->>AuthRepo: NullableBaseResponse<LoginResponseDto>
  AuthRepo-->>PostLoginUC: Result<LoginData>
  PostLoginUC-->>LoginVM: Result<LoginData>
  alt isFirstLogin
    LoginVM-->>User: 온보딩 화면으로 이동
  else
    LoginVM-->>User: 메인 화면으로 이동
  end
Loading
sequenceDiagram
  autonumber
  actor User
  participant OnboardingUI as OnboardingScreen
  participant OnboardingVM as OnboardingViewModel
  participant CheckNickUC as PostCheckNicknameUseCase
  participant SignupUC as PostSignupUseCase
  participant AuthRepo as AuthRepository
  participant Remote as AuthService
  participant DeviceRepo as DeviceRepository

  User->>OnboardingUI: 닉네임 입력
  OnboardingUI->>OnboardingVM: nicknameDuplicateCheck()
  OnboardingVM->>CheckNickUC: invoke(nickname)
  CheckNickUC->>AuthRepo: postCheckNickname(nickname)
  AuthRepo->>Remote: POST /users/check/duplicate-nickname
  Remote-->>AuthRepo: BaseResponse<CheckNicknameResponseDto>
  AuthRepo-->>CheckNickUC: Result<Boolean>
  CheckNickUC-->>OnboardingVM: Result<Boolean>
  OnboardingVM-->>OnboardingUI: 유효성 상태 갱신

  User->>OnboardingUI: 회원가입 완료
  OnboardingUI->>OnboardingVM: signUp()
  OnboardingVM->>SignupUC: postSignup(file?, defaultImageName?, nickname, kakaoId)
  SignupUC->>DeviceRepo: getDeviceId()
  SignupUC->>AuthRepo: postSignup(multipart..., deviceId)
  AuthRepo->>Remote: POST /users (multipart)
  Remote-->>AuthRepo: NullableBaseResponse<UserInfoDto>
  AuthRepo-->>SignupUC: Result<UserInfo>
  SignupUC-->>OnboardingVM: Result<UserInfo>
  OnboardingVM-->>OnboardingUI: startMainActivity 이벤트
Loading
sequenceDiagram
  autonumber
  actor User
  participant LoginActivity
  participant LoginVM as LoginViewModel
  participant GuestUC as PostGuestLoginUseCase
  participant AuthRepo as AuthRepository
  participant DeviceRepo as DeviceRepository
  participant Remote as AuthService

  User->>LoginActivity: 비회원으로 시작
  LoginActivity->>LoginVM: postGuestLogin()
  LoginVM->>GuestUC: postGuestLogin()
  GuestUC->>DeviceRepo: getDeviceId()
  GuestUC->>AuthRepo: postGuestLogin(deviceId)
  AuthRepo->>Remote: POST /auth/login/guest
  Remote-->>AuthRepo: NullableBaseResponse<GuestLoginResponseDto>
  AuthRepo-->>GuestUC: Result<GuestLoginData>
  GuestUC-->>LoginVM: Result<GuestLoginData>
  LoginVM-->>User: 메인 화면으로 이동
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Assessment against linked issues

Objective Addressed Explanation
닉네임 중복체크 API 연동 (#78)
회원가입 API 연동 (#78)

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
카카오/게스트 로그인 플로우 도입 및 ViewModel/Activity 변경 (.../presentation/ui/login/*.kt) 온보딩 API 연동 범위를 넘어서는 로그인 기능 전반 변경입니다.
디바이스 ID 로컬 저장소/레포/DI 추가 (.../datalocal/...DeviceLocalDataSource*.kt, .../domain/repository/DeviceRepository.kt, .../data/repositoryimpl/DeviceRepositoryImpl.kt, .../domain/usecase/SetDeviceIdUseCase.kt, .../presentation/ui/main/MainActivity.kt) 온보딩 API와 직접 관련되지 않은 인프라 신설입니다.
TokenPrefs Qualifier 제거 및 SharedPreferences 제공자 추가 (.../di/qualifier/Qualifier.kt, .../di/SharedPreferencesModule.kt) 온보딩 API 연동과 무관한 DI 구조 변경입니다.
API 응답 핸들러 코드 범위 변경 (.../dataremote/util/ApiResponseHandler.kt) 온보딩 연동 요구사항에 명시되지 않은 범용 에러코드 정책 변경입니다.

Possibly related PRs

  • FindYou-Android#77: 인증 API 전반(카카오/게스트 로그인, DTO, Service/DS 구현, 응답 핸들러) 개편과 매우 유사한 변경을 포함.

Suggested reviewers

  • nasohee
  • ikseong00

Poem

새 디바이스 ID, 살포시 저장해
카카오는 “me”, 게스트는 “hi”로 답해 👋
닉네임 확인, 파란 불이 반짝—OK!
멀티파트로 사진도 첨부해 찰칵 📸
온보딩 끝, 메인으로 슝—오늘도 배포 성공! 🚀

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat-onboarding-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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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

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/splash/SplashActivity.kt (1)

19-19: import jakarta.inject.Inject를 javax.inject.Inject로 변경 Hilt에서는 javax.inject 패키지를 사용하므로 SplashActivity에서 jakarta.inject.Inject를 javax.inject.Inject로 교체해야 합니다.

🧹 Nitpick comments (54)
app/build.gradle.kts (1)

45-51: debug 빌드타입 정리: BASE_URL 따옴표 추가, isDebuggable 중복 설정 제거 제안

  • BASE_URL도 release와 동일하게 문자열 리터럴을 명시적으로 감싸주세요.
  • isDebuggable = true는 debug 기본값이라 생략해도 됩니다(취향 차이면 유지 OK).
         debug {
-            isMinifyEnabled = false
-            isDebuggable = true
+            isMinifyEnabled = false
             versionNameSuffix = "-DEBUG"
-            buildConfigField("String", "BASE_URL", properties["base.url.dev"].toString())
+            buildConfigField("String", "BASE_URL", "\"${properties["base.url.dev"]}\"")
 
         }

참고: dev BASE_URL도 Retrofit 요구사항에 맞게 반드시 "/"로 끝나야 합니다. 값 확인 부탁드려요.

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

21-21: edit { clear() } 전환은 OK. 즉시 반영이 필요한 로그아웃 경로라면 commit 옵션 고려
androidx.core.content.edit 기본값은 apply(비동기)라서 기존 동작과 동일합니다. 다만 토큰 무효화 직후 네비게이션/인터셉터에서 즉시 비워진 상태를 보장해야 한다면 commit = true를 검토해 주세요.

적용 예:

-        sharedPreferences.edit { clear() }
+        sharedPreferences.edit(commit = true) { clear() }
app/src/main/java/com/example/findu/data/dataremote/model/request/CheckEmailRequestDto.kt (1)

7-9: 클래스/파일명 불일치: 파일명을 CheckNicknameRequestDto.kt로 정렬 권장
Kotlin 컴파일에는 문제 없지만 컨벤션/탐색성 측면에서 혼선을 줄 수 있습니다. 또한 서버 스키마가 정확히 "nickname" 키를 기대하는지 한 번만 더 확인 부탁드립니다.

참고:

  • 파일명 제안: app/src/main/java/com/example/findu/data/dataremote/model/request/CheckNicknameRequestDto.kt
app/src/main/java/com/example/findu/data/datalocal/datasource/DeviceLocalDataSource.kt (1)

3-6: deviceId 계약은 심플해서 좋습니다. 구현체 상수명 오타(DEVICE_ID = "token") 의심
구현체(DeviceLocalDataSourceImpl)의 키 값이 "token"으로 설정돼 있다는 노트가 있어 추후 혼선을 부를 수 있습니다. 의미에 맞게 키 문자열을 "DEVICE_ID"로 교정하는 것을 권장합니다. 또한 clear()가 해당 prefs 전부를 지우는지(키만 삭제 vs 전체 초기화) KDoc로 명시하면 좋습니다.

구현체 제안 diff(참고):

-    private const val DEVICE_ID = "token"
+    private const val DEVICE_ID = "DEVICE_ID"
app/src/main/java/com/example/findu/data/dataremote/util/Constraints.kt (1)

5-10: API 버전/세그먼트 하드코딩 대신 BuildConfig로 이동 권장
환경별(v1/v2, 스테이징/프로덕션) 전환을 쉽게 하려면 버전 문자열을 빌드타임 주입으로 관리하는 편이 안전합니다.

Gradle 예:

buildTypes {
  debug {
    buildConfigField "String", "API_VERSION", "\"v2\""
  }
  release {
    buildConfigField "String", "API_VERSION", "\"v2\""
  }
}

적용 예:

+import com.example.findu.BuildConfig
 object ApiConstraints {
     // Common
     const val API = "api"
-    const val VERSION = "v2"
+    val VERSION: String = BuildConfig.API_VERSION
     // Auth
     const val AUTH = "auth"
 }
app/src/main/java/com/example/findu/data/dataremote/model/response/CheckEmailResponseDto.kt (1)

7-9: 클래스/파일명 불일치: 파일명을 CheckNicknameResponseDto.kt로 정렬 + 필드명 최종 확인
응답 DTO 이름과 파일명이 달라 탐색성이 떨어집니다. 또한 서버 응답 키가 "isDuplicate"로 확정인지(예: "duplicate" 또는 "is_duplicate" 아님) 네트워크 로그로 한 번 검증 부탁드립니다. 기존 도메인 매퍼/사용처도 새 필드명으로 모두 갱신됐는지 점검해 주세요.

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

3-7: 인터페이스 계약 명확화(KDoc 추가 제안)

getDeviceId가 미설정 시 어떤 값을 보장하는지(빈 문자열 vs null 불가)와 clear가 “deviceId만” 초기화하는지 등을 문서화하면 사용처 혼선을 줄일 수 있습니다.

 interface DeviceRepository {
+    /**
+     * Device ID의 로컬 저장/조회 계약.
+     * - getDeviceId(): 미설정 시 빈 문자열을 반환(명시)합니다.
+     * - setDeviceId(deviceId): 공백/빈 문자열은 (가능하면) 허용하지 않는 것을 권장합니다.
+     * - clear(): 저장된 deviceId만 초기화합니다.
+     */
     fun getDeviceId(): String
     fun setDeviceId(deviceId: String)
     fun clear()
 }

6-6: 메서드 의도 명확화 네이밍

clear()보다 clearDeviceId()가 의도를 더 분명히 전달합니다. 추후 메서드가 늘어나도 혼동이 줄어듭니다. (파급이 커서 이번 PR에 강제하진 않습니다.)

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

7-12: deviceId 공백 검증 init 블록 추가 권장

  • LoginRequestDto 및 GuestLoginRequestDto 생성 시 init { require(deviceId.isNotBlank()) } 추가 검토
  • rg 검색 결과 빈 문자열 리터럴 할당(deviceId = "")은 발견되지 않음 (매퍼에서만 DTO 인스턴스 생성됨)
app/src/main/java/com/example/findu/data/repositoryimpl/DeviceRepositoryImpl.kt (1)

12-14: 사소하지만 안전한 처리: 공백/양끝 공백 트리밍 후 저장

의도치 않은 공백 저장을 방지합니다. 상위 계층에서 이미 정제하더라도 여기서 한 번 더 방어하면 좋습니다.

-    override fun setDeviceId(deviceId: String) {
-        deviceLocalDataSource.deviceId = deviceId
-    }
+    override fun setDeviceId(deviceId: String) {
+        deviceLocalDataSource.deviceId = deviceId.trim()
+    }
app/src/main/java/com/example/findu/domain/model/AuthData.kt (2)

8-11: 로그인 상태 표현을 더 명확하게: nullable 대신 분기형으로 모델링 제안

isFirstLogin/userInfo? 조합은 불변식이 깨지기 쉽습니다(예: isFirstLogin=false인데 userInfo=null). 분기 상태를 sealed 타입으로 분리하면 사용처에서 안전성이 높아집니다.

-data class LoginData(
-    val isFirstLogin: Boolean,
-    val userInfo: UserInfo?
-)
+sealed interface LoginResult
+data class FirstLogin(val kakaoId: Long) : LoginResult
+data class LoggedIn(val userInfo: UserInfo) : LoginResult

원한다면 매퍼/유즈케이스 영향 범위까지 함께 리팩토링해드릴게요.


13-16: 토큰을 사용자 프로필과 분리 추천

UserInfo/GuestLoginDataaccessToken이 들어가면 캐시·로그 등에서 과도하게 전파될 수 있어 보안 리스크가 커집니다. 토큰은 별도 VO로 분리하는 것을 권장합니다.

// 제안: 토큰 분리
data class AuthTokens(val accessToken: String /*, val refreshToken: String? */)

// 예: LoggedIn(userInfo, tokens)처럼 전달
data class LoggedIn(val userInfo: UserInfo, val tokens: AuthTokens) : LoginResult
// 또는 GuestLoginData -> (userId, tokens)

추가로 UI/DB 계층으로 accessToken이 흘러들지 않도록 경계도 같이 점검하면 좋아요.

Also applies to: 18-22

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

3-8: 프로퍼티 명확화: string → serverKey(또는 code)로 이름 변경 권장

string은 의미가 모호합니다. 서버/로컬 매핑 의도라면 더 구체적으로 명명해 주세요. 세미콜론도 Kotlin에선 불필요합니다.

-enum class DefaultProfileType(val string: String) {
-    DEFAULT("default"),
-    PUPPY("puppy"),
-    CHICK("chick"),
-    PANDA("panda");
+enum class DefaultProfileType(val serverKey: String) {
+    DEFAULT("default"),
+    PUPPY("puppy"),
+    CHICK("chick"),
+    PANDA("panda")
 }

또한 역매핑 유틸을 추가해두면 사용성이 좋아집니다:

companion object {
    fun fromServerKey(key: String?): DefaultProfileType =
        entries.firstOrNull { it.serverKey.equals(key, ignoreCase = true) } ?: DEFAULT
}
app/src/main/java/com/example/findu/presentation/ui/splash/SplashActivity.kt (2)

44-44: 토큰 존재만으로 Main 진입은 위험 — 유효성 검증/사일런트 리프레시 고려

빈 문자열 체크만으로는 만료/폐기 토큰을 거르지 못합니다. 스플래시에서 가벼운 유효성 검사(만료 확인) 또는 사일런트 리프레시를 수행하고 실패 시 로그인으로 보내는 흐름을 추천합니다.

-            if (accessToken.isEmpty()) {
-                navigateToLogin()
-            } else {
-                navigateToMain()
-            }
+            val isValid = /* e.g. */ validateAccessTokenUseCase(accessToken)
+            if (isValid) navigateToMain() else navigateToLogin()

이미 리프레시 토큰을 운용 중이라면 refreshIfNeededUseCase() 형태로 대체해도 됩니다.


53-61: Glide RequestListener 시그니처 재확인 요청

onLoadFailedtarget 파라미터는 보통 nullable입니다(Target<R>?). 현재 비-null로 선언되어 있어 시그니처 미일치가 발생할 수 있습니다. 아래처럼 정렬해 주세요.

-override fun onLoadFailed(
-    e: GlideException?,
-    model: Any?,
-    target: Target<GifDrawable>,
-    isFirstResource: Boolean
-): Boolean {
+override fun onLoadFailed(
+    e: GlideException?,
+    model: Any?,
+    target: Target<GifDrawable>?,
+    isFirstResource: Boolean
+): Boolean {
     return false
 }

부가로 setLoopCount(1) 직후 stop() 호출은 첫 루프가 재생되기 전에 멈출 수 있습니다. 원하는 동작이 “1회 재생 후 정지”라면 stop()은 제거하는 편이 안전합니다.

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

12-18: 에러 메시지에 상태코드 포함 및 에러타입 정교화 제안

디버깅 편의와 로깅 품질을 위해 코드값을 메시지에 포함하고, 가능하면 도메인 수준의 에러 타입으로 랩핑을 권장합니다.

-Result.failure(Exception("Client error : ${this.message}"))
+Result.failure(Exception("Client error (${this.code}): ${this.message}"))
...
-Result.failure(Exception("Server error : ${this.message}"))
+Result.failure(Exception("Server error (${this.code}): ${this.message}"))

또는

sealed interface ApiError : Throwable {
    val code: Int
    data class Client(override val code: Int, val msg: String) : RuntimeException(msg), ApiError
    data class Server(override val code: Int, val msg: String) : RuntimeException(msg), ApiError
    data class Unknown(override val code: Int, val msg: String) : RuntimeException(msg), ApiError
}

그리고 Result.failure(ApiError.Client(code, message)) 형태로 감싸면 상위 계층에서 분기 처리가 더 쉬워집니다.

Also applies to: 31-37

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

26-30: ANDROID_ID null 대비: UUID 생성·저장으로 고유 ID 보장

@@
-import android.provider.Settings
+import android.provider.Settings
+import java.util.UUID
@@
-        val deviceId = Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID)
-            ?: "unknown_device_id"
+        val deviceId = Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID)
+            ?: UUID.randomUUID().toString()

SetDeviceIdUseCase.invoke는 suspend가 아니므로 메인 스레드 호출 유지해도 OK.

app/src/main/java/com/example/findu/data/mapper/torequest/GuestLoginRequestDtoMapper.kt (2)

4-5: 불필요한 import 정리

사용되지 않는 LoginRequestDto, LoginInfo import는 제거하는 게 좋습니다.

-import com.example.findu.data.dataremote.model.request.LoginRequestDto
-import com.example.findu.domain.model.LoginInfo

8-9: deviceId 전처리(트림) 제안

공백 문자열 전송을 방지하려면 간단히 trim만 적용해두는 걸 추천합니다. (네이밍 충돌 위험은 낮지만, 동일한 String.toRequestDto() 확장이 늘어날 경우를 대비해 파일별로만 import하도록 주의해주세요.)

-fun String.toRequestDto() =
-    GuestLoginRequestDto(deviceId = this)
+fun String.toRequestDto() =
+    GuestLoginRequestDto(deviceId = trim())
app/src/main/java/com/example/findu/data/mapper/todomain/LoginResponseDtoMapper.kt (1)

5-5: 사용되지 않는 import 제거

UserInfo 타입을 직접 참조하지 않으므로 import 제거 추천.

-import com.example.findu.domain.model.UserInfo
app/src/main/java/com/example/findu/di/SharedPreferencesModule.kt (1)

20-21: 함수명 명확화 및 일관성 정리

토큰/디바이스ID 제공자 함수명을 구체화하고 표현식 바디로 정리하면 가독성이 좋아집니다.

-    fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences =
+    fun provideTokenSharedPreferences(@ApplicationContext context: Context): SharedPreferences =
         context.getSharedPreferences("token_prefs", Context.MODE_PRIVATE)
@@
-    fun provideDeviceIdSharedPreference(@ApplicationContext context: Context): SharedPreferences {
-        return context.getSharedPreferences("device_id_prefs", Context.MODE_PRIVATE)
-    }
+    fun provideDeviceIdSharedPreferences(@ApplicationContext context: Context): SharedPreferences =
+        context.getSharedPreferences("device_id_prefs", Context.MODE_PRIVATE)

Also applies to: 27-29

app/src/main/java/com/example/findu/domain/usecase/SetDeviceIdUseCase.kt (2)

11-11: 빈 deviceId 저장 방지 + 불필요한 공백 트리밍 제안

빈 문자열이 저장되면 이후 게스트 로그인 등에서 서버 오류로 이어질 수 있어요. 간단히 validate 해두는 게 안전합니다.

-    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.trim())
+    }

7-8: UseCase에 @singleton은 과해요

상태 없는 얇은 유즈케이스는 굳이 싱글톤일 필요 없습니다. DI 스코프 단순화 차원에서 제거 고려해 주세요.

-@Singleton
-class SetDeviceIdUseCase @Inject constructor(
+class SetDeviceIdUseCase @Inject constructor(
app/src/main/java/com/example/findu/presentation/ui/onboarding/OnboardingActivity.kt (2)

33-33: Compose 수명주기 대응 수집으로 교체 권장

Flow 수집은 collectAsStateWithLifecycle()가 안전합니다.

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

추가: import androidx.lifecycle.compose.collectAsStateWithLifecycle


34-38: 프로필 Uri를 장기 보관해야 한다면 OpenDocument + 권한 영속화 고려

GetContent는 권한 영속화가 안 됩니다. 앱 재시작 이후에도 사용할 이미지라면 OpenDocument로 전환하고 takePersistableUriPermission을 호출하세요.

-            val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
+            val launcher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
                 uri?.let {
-                    onboardingViewModel.setProfileImage(it)
+                    // 권한 영속화
+                    contentResolver.takePersistableUriPermission(
+                        it,
+                        Intent.FLAG_GRANT_READ_URI_PERMISSION
+                    )
+                    onboardingViewModel.setProfileImage(it)
                 }
             }
-                cameraIconClicked = {launcher.launch(MIME_TYPE_IMAGE)},
+                cameraIconClicked = { launcher.launch(arrayOf(MIME_TYPE_IMAGE)) },
                 clearProfileImage = {onboardingViewModel.setProfileImage(null)}

주의: OpenDocument 전환 시 최초 한 번 Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION이 가능한 스토리지 제공자에서만 동작합니다.

Also applies to: 51-53

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

8-10: 닉네임 normalize(트리밍) 및 빈 값 가드 제안

공백 포함/전후 공백으로 중복 체크 결과가 달라지는 걸 방지합시다.

-    suspend operator fun invoke(nickname: String): Result<Boolean> =
-        authRepository.postCheckNickname(nickname = nickname)
+    suspend operator fun invoke(nickname: String): Result<Boolean> {
+        val normalized = nickname.trim()
+        if (normalized.isEmpty()) return Result.failure(IllegalArgumentException("Empty nickname"))
+        return authRepository.postCheckNickname(nickname = normalized)
+    }
app/src/main/java/com/example/findu/presentation/ui/login/LoginActivity.kt (2)

58-61: Activity 컨텍스트는 명시적으로 사용하면 안전합니다

this@LoginActivity로 명시하면 가독성과 안전성이 좋아요.

-                    KakaoLoginHelper.login(
-                        context = this,
+                    KakaoLoginHelper.login(
+                        context = this@LoginActivity,
                         callback = callback
                     )
-                    loginViewModel.postGuestLogin{
-                        this.showToast(message = getString(R.string.login_without_signup_toast_message))
+                    loginViewModel.postGuestLogin {
+                        this@LoginActivity.showToast(message = getString(R.string.login_without_signup_toast_message))
                     }

Also applies to: 65-66


41-53: 뷰모델로 Kakao 사용자 조회 이동 고려

UI(Activity)에서 외부 SDK 콜백과 도메인 호출이 섞여 있습니다. 테스트 용이성과 분리도를 위해 LoginViewModel에서 UserApiClient.instance.me를 래핑하고, Activity는 결과만 구독하는 구조가 더 깔끔합니다.

원하시면 ViewModel 중심으로 콜백 래핑한 예시 패치 드릴게요.

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

13-13: 코틀린 코딩 컨벤션 공백 닛픽

매개변수 타입 콜론 뒤 공백을 맞춰 주세요.

-        kakaoId:Long
+        kakaoId: Long
app/src/main/java/com/example/findu/presentation/ui/onboarding/component/OnboardingProfileBox.kt (4)

31-40: 플레이스홀더 이미지도 원형 클리핑으로 일관성 유지

실제 이미지일 때만 .clip(CircleShape)이 적용되어 빈 상태/기본 이미지가 사각형으로 보여요. 둘 다 원형 클립을 적용하면 UI 일관성이 좋아집니다.

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

41-50: contentDescription 현지화/의도 명확화

"Profile Image" 하드코딩 보다는 장식적 이미지면 null, 접근성 라벨이 필요하면 stringResource 사용을 권장합니다.

-                contentDescription = "Profile Image",
+                contentDescription = null, // 또는 stringResource(R.string.profile_image_content_description)

추가로 placeholder/error도 장식적 리소스라면 별도 설명은 불필요합니다.


53-58: 카메라 아이콘 접근성 라벨 및 포커스 타겟 강화

클릭 가능한 컨트롤이므로 접근성 라벨을 부여해 주세요. Semantics를 부착하면 BaseVectorIcon 내부 구현에 의존하지 않아도 됩니다.

         BaseVectorIcon(
             vectorResource = R.drawable.ic_profile_camera_44,
             modifier = Modifier
-                .noRippleClickable { cameraClicked() }
+                .noRippleClickable { cameraClicked() }
+                .semantics { contentDescription = "프로필 사진 변경" } // stringResource 추천
                 .align(Alignment.BottomEnd)
         )

필요시 아래 import 추가:

import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics

32-32: 코딩 스타일 닛픽: 연산자 양옆 공백

가독성을 위해 공백을 맞춰 주세요.

-        if (profileImgUri==null) {
+        if (profileImgUri == null) {
app/src/main/java/com/example/findu/domain/usecase/PostSignupUseCase.kt (1)

13-17: 도메인 모델로 의도를 캡슐화(선택사항)

파일 업로드 vs 기본 이미지 선택을 매개변수 2개 대신 Sealed class로 표현하면 호출부와 계약이 더 명확해집니다.

예시:

sealed interface ProfileImageSelection {
    data class Upload(val file: File): ProfileImageSelection
    data class Default(val name: String): ProfileImageSelection
}
// fun postSignup(image: ProfileImageSelection, nickname: String, kakaoId: Long): Result<UserInfo>
app/src/main/java/com/example/findu/presentation/ui/onboarding/component/OnboardingProfile.kt (2)

32-33: 이름 통일 제안: profileImgUri → profileImageUri

UI 상태(profileImageUri)와 파라미터 명을 통일하면 검색/리팩토링 시 혼선을 줄일 수 있어요.

-fun OnboardingProfile(
-    cameraIconClicked: () -> Unit,
-    profileImgUri: Uri?,
+fun OnboardingProfile(
+    cameraIconClicked: () -> Unit,
+    profileImageUri: Uri?,
@@
-        OnboardingProfileBox(
-            profileImgUri = profileImgUri,
+        OnboardingProfileBox(
+            profileImgUri = profileImageUri,

72-81: 접근성: contentDescription 추가

미리보기/기본 이미지에 contentDescription이 null입니다. 접근성 개선을 위해 간단한 설명을 넣는 것을 권장합니다.

-Image(
-    painter = painterResource(R.drawable.img_onboarding_default_profile_dog_68),
-    contentDescription = null,
+Image(
+    painter = painterResource(R.drawable.img_onboarding_default_profile_dog_68),
+    contentDescription = stringResource(R.string.cd_onboarding_profile_default_dog),

Also applies to: 82-91, 92-101, 102-111

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

17-20: null 안전 처리 단순화

toString()은 기본값이 있어도 오해의 여지가 있어요. orEmpty()로 명시하면 더 안전합니다.

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

23-27: 키 네이밍/가시성 정리 + 마이그레이션 고려

  • "token" 키는 의미가 넓습니다. "device_id" 등으로 명확히 하는 것을 권장합니다.
  • 상수는 외부 노출 필요 없으면 private const val로 축소해 주세요.
  • 이미 배포된 값이 있다면 키 변경 시 마이그레이션 로직이 필요합니다.
-    companion object {
-        const val PREFERENCES_NAME = "device_preferences"
-        const val DEVICE_ID = "token"
-        const val INITIAL_VALUE = ""
-    }
+    companion object {
+        private const val PREFERENCES_NAME = "device_preferences"
+        private const val DEVICE_ID = "device_id"
+        private const val INITIAL_VALUE = ""
+    }

마이그레이션 예: 기존 "token"이 존재하면 새 키로 복사 후 구키 제거.

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

18-21: 불리언 의미 명시 필요

postCheckNicknameBoolean 의미(사용 가능/불가)와 에러 케이스 구분 방식을 KDoc 등으로 명확히 해 주세요. 호출부 오해를 줄일 수 있어요.


22-28: 회원가입 파라미터 응집 및 불변식 보장

현재 시그니처는 profileImageFile/defaultImageName이 동시에 null 또는 동시에 값이 될 수 있어 불변식이 약합니다. 프로필 입력을 모델로 캡슐화하여 “하나만 선택”을 타입 수준에서 보장하는 것을 권장합니다. 또한 defaultImageNameDefaultProfileType 등 도메인 타입으로 받으면 매핑 오류를 예방할 수 있습니다.

-    suspend fun postSignup(
-        profileImageFile: File?,
-        defaultImageName: String?,
-        nickname: String,
-        kakaoId: Long,
-        deviceId: String
-    ): Result<UserInfo>
+    suspend fun postSignup(
+        profile: SignupProfile,
+        nickname: String,
+        kakaoId: Long,
+        deviceId: String
+    ): Result<UserInfo>

추가 타입 예시(별도 파일에 배치 권장):

sealed interface SignupProfile {
    data class FileImage(val file: File) : SignupProfile
    data class DefaultImage(val type: DefaultProfileType) : SignupProfile
}

변경 시 구현체/유즈케이스/매퍼 전파 필요합니다.

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

10-13: derivedStateOf 단순화 제안

연산이 가벼워 remember + derivedStateOf 없이 바로 불리언 계산이 더 읽기 쉽습니다.

-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
+import androidx.compose.runtime.getValue
@@
-    val isNextButtonEnabled by remember(uiState.pageState, uiState.nickNameValidState) {
-        derivedStateOf {
-            uiState.pageState == 1 ||
-                    (uiState.pageState == 2 && uiState.nickNameValidState == NicknameValidType.VALID)
-        }
-    }
+    val isNextButtonEnabled =
+        uiState.pageState == 1 ||
+            (uiState.pageState == 2 && uiState.nickNameValidState == NicknameValidType.VALID)

Also applies to: 44-49


66-76: 매직 넘버(1, 2) 제거 검토

페이지 상태를 sealed class/enum으로 다루면 가독성과 안전성이 올라갑니다.

sealed class OnboardingPage { data object Profile: OnboardingPage(); data object Nickname: OnboardingPage() }
app/src/main/java/com/example/findu/di/UseCaseModule.kt (1)

192-195: 메서드명-반환타입 불일치: 혼동 방지를 위해 리네이밍 권장

providePostCheckEmailUseCasePostCheckNicknameUseCase를 반환합니다. Hilt에선 반환 타입이 중요하긴 하지만, 유지보수 관점에서 오해의 소지가 큽니다.

아래처럼 함수명을 닉네임 기준으로 맞추는 것을 제안합니다.

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

33-51: 프로필 이미지/기본이미지 동시 전송 방지 및 RequestBody 생성 중복 축소 제안

  • 서버 요구사항상 둘 중 하나만 보내야 한다면, 동시 전송을 방지하는 가드가 필요합니다.
  • "text/plain" 반복 호출은 확장함수로 정리하면 가독성과 오타 방지에 유리합니다.

아래처럼 가드를 추가하고, 반환식을 블록으로 전환하면 안전합니다.

-    ): NullableBaseResponse<UserInfoDto> = authService.postSignup(
+    ): NullableBaseResponse<UserInfoDto> {
+        require(profileImageFile == null || defaultImageName == null) {
+            "profileImageFile and defaultImageName should not be sent together"
+        }
+        return authService.postSignup(
         profileImage = profileImageFile?.let {
             MultipartBody.Part.createFormData(
                 "profileImage",
                 it.name,
                 it.asRequestBody("image/*".toMediaTypeOrNull())
             )
         },
-        defaultImageName = defaultImageName?.toRequestBody("text/plain".toMediaTypeOrNull()),
-        nickname = nickname.toRequestBody("text/plain".toMediaTypeOrNull()),
-        kakaoId = kakaoId.toString().toRequestBody("text/plain".toMediaTypeOrNull()),
-        deviceId = deviceId.toRequestBody("text/plain".toMediaTypeOrNull())
-    )
+        defaultImageName = defaultImageName?.toPlainRequestBody(),
+        nickname = nickname.toPlainRequestBody(),
+        kakaoId = kakaoId.toString().toPlainRequestBody(),
+        deviceId = deviceId.toPlainRequestBody()
+        )
+    }

확장함수(파일 내 또는 공용 유틸)에 추가:

private fun String.toPlainRequestBody() =
    this.toRequestBody("text/plain".toMediaTypeOrNull())

서버 스펙과 필드명/파트명이 정확히 일치하는지(예: "profileImage", "defaultImageName") 다시 한 번 확인 부탁드립니다. 둘을 동시에 보낼 때 서버 동작(우선순위/에러)은 어떻게 정의되어 있는지도 체크해 주세요.

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

43-55: ViewModel에서 콜백 인자 대신 이벤트로 노출 권장

postGuestLogin(onSuccess: () -> Unit)의 UI 콜백 주입은 VM-UI 결합을 키웁니다. 이미 startMainActivity 이벤트 플로우가 있으니, 성공 토스트/네비게이션도 플로우로 통일하는 편이 깔끔합니다.


51-53: 로그 메시지 콜론 중복 제거

메시지 가독성 개선 제안.

-                    Log.d("http", "Error Message: : $e")
+                    Log.d("http", "Error Message: $e")
app/src/main/java/com/example/findu/data/dataremote/datasource/AuthRemoteDataSource.kt (1)

27-33: 프로필 이미지 전달 방식의 불변식 보장 필요(동시 설정 방지).

profileImageFiledefaultImageName가 동시에 전달되는 상황을 타입으로 막아두면 서버/클라이언트 해석 혼선을 줄일 수 있어요. 이미지 업로드 vs 기본이미지 선택을 표현하는 전용 파라미터(예: sealed class)로 캡슐화 고려 부탁드립니다.

예시:

sealed interface ProfileImageParam {
  data class Upload(val file: File): ProfileImageParam
  data class Default(val name: String): ProfileImageParam
  data object None: ProfileImageParam
}
app/src/main/java/com/example/findu/data/repositoryimpl/AuthRepositoryImpl.kt (2)

24-29: 게스트 로그인 null 메시지 오타 수정.

게스트 로그인에서 null 에러 메시지가 "Login data is null"로 남아 있어요. 문맥에 맞게 수정하는 게 좋습니다.

다음 패치 제안:

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

18-22: 중복 패턴 공통화로 보일러플레이트 감소.

runCatching { remote.call().handleBaseResponse().getOrThrow()?.toDomain() ?: error(...) } 패턴이 3곳에서 반복됩니다. 공통 헬퍼로 추출하면 가독성과 유지보수성이 좋아져요.

예시(클래스 내부 private 함수로 추가):

private inline fun <T, R> mapNullableOrError(
  crossinline call: suspend () -> T?,
  nullMessage: String,
  crossinline mapper: (T) -> R
): Result<R> = runCatching { call() ?: error(nullMessage) }.mapCatching(mapper)

사용:

override suspend fun postLogin(info: LoginInfo) =
  mapNullableOrError(
    call = { authRemoteDataSource.postLogin(info.toRequestDto()).handleBaseResponse().getOrThrow() },
    nullMessage = "Login data is null",
    mapper = { it.toDomain() }
)

Also applies to: 24-29, 36-50

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

20-21: IO 디스패처/withContext import 추가 필요.

위 수정 반영 시 필요한 import를 추가해 주세요.

 import kotlinx.coroutines.launch
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
 import java.io.File

89-105: 닉네임 포맷이 INVALID일 때는 API 호출을 스킵하세요.

빈 값만 체크하고 있어 포맷이 잘못돼도 중복체크를 호출합니다.

-            if (uiState.value.nickname.isEmpty()) {
-                _uiState.update { it.copy(nickNameValidState = NicknameValidType.EMPTY_INVALID) }
-            } else {
-                postCheckNicknameUseCase(uiState.value.nickname).onSuccess { isDuplicate ->
+            if (_uiState.value.nickname.isEmpty()) {
+                _uiState.update { it.copy(nickNameValidState = NicknameValidType.EMPTY_INVALID) }
+            } else if (_uiState.value.nickNameValidState == NicknameValidType.FORMAT_INVALID) {
+                // 포맷이 유효하지 않으면 호출하지 않음
+                return@launch
+            } else {
+                postCheckNicknameUseCase(_uiState.value.nickname).onSuccess { isDuplicate ->
                     if (isDuplicate) {
                         _uiState.update { it.copy(nickNameValidState = NicknameValidType.DUPLICATE_INVALID) }
                     } else {
                         _uiState.update { it.copy(nickNameValidState = NicknameValidType.VALID) }
                     }
                 }.onFailure { e ->
-                    Log.d("http", "Error Message: : $e")
+                    Log.d("http", "Nickname check failed: $e")
                 }
                 focusChanged(false)
             }
app/src/main/java/com/example/findu/data/dataremote/service/AuthService.kt (3)

25-29: suspend 반환형에서 Response<> 미사용 시 4xx/5xx 처리 전략 확인

현재 T(예: NullableBaseResponse)를 직접 반환하고 있어, 비-2xx 응답은 HttpException으로 throw 됩니다. RemoteDataSource 레이어에서 try/catch 및 에러 바디 파싱을 일관되게 처리하고 있는지 확인해 주세요. 그렇지 않다면 Response로 감싸 2xx/4xx/5xx를 상태코드 기반으로 다루는 접근이 단순합니다.

예시(옵션) — Response 래핑:

-    suspend fun postLogin(
-        @Body loginRequestDto: LoginRequestDto
-    ): NullableBaseResponse<LoginResponseDto>
+    suspend fun postLogin(
+        @Body loginRequestDto: LoginRequestDto
+    ): Response<NullableBaseResponse<LoginResponseDto>>

-    suspend fun postGuestLogin(
-        @Body guestLoginRequestDto: GuestLoginRequestDto
-    ): NullableBaseResponse<GuestLoginResponseDto>
+    suspend fun postGuestLogin(
+        @Body guestLoginRequestDto: GuestLoginRequestDto
+    ): Response<NullableBaseResponse<GuestLoginResponseDto>>

-    suspend fun postSignup(
+    suspend fun postSignup(
         @Part profileImage: MultipartBody.Part?,
         @Part("defaultProfileImageName") defaultImageName: RequestBody?,
         @Part("nickname") nickname: RequestBody,
         @Part("kakaoId") kakaoId: RequestBody,
         @Part("deviceId") deviceId: RequestBody
-    ): NullableBaseResponse<UserInfoDto>
+    ): Response<NullableBaseResponse<UserInfoDto>>

참고: 이미 HttpException 기반 흐름이 정착되어 있다면 그대로 가도 됩니다. 다만 호출부의 예외 처리 일관성은 꼭 점검 부탁드립니다.

Also applies to: 30-34, 40-48


40-48: 멀티파트 텍스트 파라미터 @PartMap으로 간소화 + 와일드카드 억제 권장

텍스트 파라미터가 늘어날수록 시그니처가 길어집니다. @PartMap을 사용하면 확장·유지보수가 쉬워집니다. Kotlin → Java 제네릭 브릿지 이슈 방지를 위해 @JvmSuppressWildcards도 함께 권장합니다.

-    suspend fun postSignup(
-        @Part profileImage: MultipartBody.Part?,
-        @Part("defaultProfileImageName") defaultImageName: RequestBody?,
-        @Part("nickname") nickname: RequestBody,
-        @Part("kakaoId") kakaoId: RequestBody,
-        @Part("deviceId") deviceId: RequestBody
-    ): NullableBaseResponse<UserInfoDto>
+    suspend fun postSignup(
+        @Part profileImage: MultipartBody.Part?,
+        @PartMap parts: Map<String, @JvmSuppressWildcards RequestBody>
+    ): NullableBaseResponse<UserInfoDto>

추가로 아래 import가 필요합니다:

  • import retrofit2.http.PartMap

서버 계약 상의 파라미터 키("defaultProfileImageName", "nickname", "kakaoId", "deviceId")는 @PartMap에 동일 키로 넣어 주시면 됩니다.


25-26: 컴파일타임 상수 조건 충족
ApiConstraints.API, VERSION, AUTH가 모두 const val로 선언되어 Retrofit 애노테이션 인자로 바로 사용 가능합니다.
(선택) 인터페이스 내 companion object에 경로 상수를 정의해 모아두는 리팩터링을 고려할 수 있습니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4f6779e and f6461b7.

📒 Files selected for processing (50)
  • app/build.gradle.kts (1 hunks)
  • 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/datasourceimpl/AuthRemoteDataSourceImpl.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/request/GuestLoginRequestDto.kt (1 hunks)
  • app/src/main/java/com/example/findu/data/dataremote/model/request/LoginRequestDto.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/auth/GuestLoginResponseDto.kt (1 hunks)
  • app/src/main/java/com/example/findu/data/dataremote/model/response/auth/LoginResponseDto.kt (1 hunks)
  • app/src/main/java/com/example/findu/data/dataremote/service/AuthService.kt (1 hunks)
  • app/src/main/java/com/example/findu/data/dataremote/util/ApiResponseHandler.kt (2 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/GuestLoginResponseDtoMapper.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/GuestLoginRequestDtoMapper.kt (1 hunks)
  • app/src/main/java/com/example/findu/data/mapper/torequest/LoginRequestDtoMapper.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/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/repository/AuthRepository.kt (1 hunks)
  • app/src/main/java/com/example/findu/domain/repository/DeviceRepository.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/login/LoginActivity.kt (3 hunks)
  • app/src/main/java/com/example/findu/presentation/ui/login/viewmodel/LoginViewModel.kt (1 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/java/com/example/findu/presentation/ui/splash/SplashActivity.kt (1 hunks)
💤 Files with no reviewable changes (5)
  • app/src/main/java/com/example/findu/presentation/ui/signup/SignupActivity.kt
  • app/src/main/java/com/example/findu/data/mapper/toDomain/CheckEmailResponseDtoMapper.kt
  • app/src/main/java/com/example/findu/domain/usecase/PostCheckEmailUseCase.kt
  • app/src/main/java/com/example/findu/presentation/ui/signup/SignupSuccessActivity.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/torequest/LoginRequestDtoMapper.kt
  • app/src/main/java/com/example/findu/data/dataremote/model/response/auth/GuestLoginResponseDto.kt
  • app/src/main/java/com/example/findu/data/mapper/todomain/UserInfoDtoMapper.kt
  • app/src/main/java/com/example/findu/domain/model/AuthData.kt
  • app/src/main/java/com/example/findu/data/dataremote/model/request/LoginRequestDto.kt
  • app/src/main/java/com/example/findu/data/mapper/torequest/GuestLoginRequestDtoMapper.kt
  • app/src/main/java/com/example/findu/data/dataremote/model/response/auth/LoginResponseDto.kt
  • app/src/main/java/com/example/findu/data/dataremote/service/AuthService.kt
  • app/src/main/java/com/example/findu/data/mapper/todomain/LoginResponseDtoMapper.kt
  • app/src/main/java/com/example/findu/data/mapper/toDomain/GuestLoginResponseDtoMapper.kt
  • app/src/main/java/com/example/findu/data/dataremote/datasource/AuthRemoteDataSource.kt
  • app/src/main/java/com/example/findu/data/dataremote/datasourceimpl/AuthRemoteDataSourceImpl.kt
  • app/src/main/java/com/example/findu/data/repositoryimpl/AuthRepositoryImpl.kt
🧬 Code graph analysis (4)
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/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/login/viewmodel/LoginViewModel.kt (1)
app/src/main/java/com/example/findu/presentation/ui/onboarding/viewmodel/OnboardingViewModel.kt (1)
  • startMainActivity (129-133)
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)
🔇 Additional comments (34)
app/src/main/java/com/example/findu/data/repositoryimpl/DeviceRepositoryImpl.kt (1)

7-17: DeviceRepositoryImpl에 @singleton 불필요 RepositoryModule의 bindDeviceRepository 메서드에 이미 @singleton이 적용되어 단일 인스턴스가 보장되므로, 클래스에 @singleton 어노테이션을 추가할 필요 없습니다.

Likely an incorrect or invalid review comment.

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

8-11: userInfo null-safe 매핑 확인, 사용처 검증 필요

  • LoginResponseDtoMapper에서 userInfo = userInfo?.toDomain()으로 안전하게 매핑되었고,
  • 도메인 모델 LoginDataUserInfo?로 nullable 정의되어 있습니다.

UI/ViewModel 등 실제 사용처에서 null 분기 혹은 기본값 처리가 이전 PR 대응처럼 유지되고 있는지 한 번 더 점검해 주세요.

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

3-6: 도메인 → 요청/응답 매핑 전제 깔끔합니다

LoginInfo(kakaoId, deviceId) 구성은 요청 DTO로의 변환에 필요한 최소 정보만 담고 있어 좋습니다. 유지하세요.

app/src/main/java/com/example/findu/data/mapper/torequest/LoginRequestDtoMapper.kt (2)

7-8: 도메인→요청 매핑 간결하고 정확합니다

필드 매핑이 1:1로 맞고, 불필요한 변환이 없습니다. 그대로 가면 됩니다.


1-8: 패키지/디렉터리 네이밍 일관화 (todomain 디렉터리 소문자화)

  • com.example.findu.data.mapper 하위에 toDomain/(대문자 D)와 todomain/(소문자) 디렉터리가 혼재되어 있음. 패키지 선언은 모두 com.example.findu.data.mapper.todomain 소문자 패키지를 참조하므로, 디렉터리명도 전부 소문자 todomain으로 통일하세요.
  • torequest 디렉터리는 이미 소문자이므로 별도 조치 불필요.
⛔ Skipped due to learnings
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으로 통일되어 있다. 초기 분석 시 더 신중하게 확인해야 한다.
app/src/main/java/com/example/findu/data/dataremote/util/ApiResponseHandler.kt (2)

8-18: HTTP 표준 코드 범위로의 전환 좋습니다

2xx/4xx/5xx 범위 해석으로 단순·명확해졌어요. 유지 추천합니다.


25-42: 204/빈 본문 응답 대비 정책 확인

일부 2xx(예: 204 No Content)는 본문이 비어 있을 수 있습니다. 해당 엔드포인트들이 모두 NullableBaseResponse<T>를 사용한다면 OK, 아니라면 NPE/역직렬화 예외가 생길 수 있어 계약을 한 번 더 확인 부탁드립니다.

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

6-6: DI 설정 점검 완료, 문제 없음

app/src/main/java/com/example/findu/data/mapper/toDomain/GuestLoginResponseDtoMapper.kt (1)

6-10: 단순 매핑 잘 됐습니다.

  • DTO -> Domain 필드 매핑(userId, accessToken) 정확합니다.
app/src/main/java/com/example/findu/di/DataSourceModule.kt (1)

3-3: Qualifier 및 SharedPreferences 제공자와 DeviceLocalDataSourceImpl 구현체 모두 확인되었습니다. 승인합니다.

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

6-10: DTO 정의 깔끔합니다.

  • @Serializable + @SerialName("deviceId") 조합 적절. 서버 스키마와 필드명이 동일하다면 @SerialName은 선택 사항이지만 유지해도 문제 없습니다.
app/src/main/java/com/example/findu/di/qualifier/Qualifier.kt (1)

5-11: LGTM – Qualifier 구성 적절

TokenPrefs, DeviceIdPrefs로 DI 구분 명확합니다.

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

6-11: DTO ↔ Domain 널러빌리티 정합성 확인 필요

userId, nickname, accessToken의 null 가능성이 DTO/Domain 사이에서 일치하는지 한번만 점검 부탁드립니다. 불일치 시 컴파일/런타임 이슈가 생길 수 있습니다.

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

7-11: LGTM – 매핑 및 null-safe 처리 적절

userInfo?.toDomain() 처리로 안정적으로 매핑됩니다.

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

24-29: LGTM – DeviceId 전용 SharedPreferences 주입 추가 적절

별도 prefs로 분리해 충돌/오염 가능성을 낮춘 점 좋습니다.

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

36-37: Uri 직접 전달로의 전환, 깔끔합니다

String → Uri? 전환이 API 계약에 잘 맞습니다. null로 클리어하는 흐름과 함께 일관성 좋아요.

app/src/main/java/com/example/findu/domain/usecase/PostLoginUseCase.kt (2)

12-19: 굿 리팩터: 카카오ID+디바이스ID 기반으로 도메인 모델(LoginInfo) 전달이 깔끔합니다

단방향 의존성(UseCase -> Repository)도 유지되고 반환 타입을 Result로 명확히 한 점 좋아요.


12-19: getDeviceId가 non-null String을 반환하므로 null 체크는 불필요합니다.
서버가 빈 문자열을 허용하지 않는다면 isBlank() 검사를 추가하여 빈 값일 경우 조기 실패 처리하는 것은 여전히 유용합니다.

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

24-30: Uri 기반 API 전환 👍

profileImgUri: Uri?로의 전환과 DEFAULT 기본값 채택이 사용성 측면에서 합리적입니다.

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

8-11: 의존성 전환 👍

TokenRepository → DeviceRepository로의 전환이 현재 로그인/가입 플로우 방향과 일치합니다.

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

32-33: Uri 기반 전환 LGTM

StringUri? 전환과 OnboardingProfileBox 전달부 일관성 좋아요. 컴파일 타임에서 많은 오류를 줄여줄 겁니다.

Also applies to: 60-61


36-36: DEFAULT 기본값 전환 영향 확인 필요

기존 NONE 기반 로직/리소스가 남아있다면 초기 상태와 서버에 넘기는 기본 이미지 이름 매핑이 달라질 수 있어요. 전역적으로 DEFAULT 사용으로 정리됐는지 확인 부탁드립니다.


79-81: 기본 아이콘 매핑 변경 검증

NONE→DEFAULT, DOG→PUPPY 매핑 변경이 서버/도메인(DefaultProfileType ↔ 서버 이미지 이름)과 정확히 일치하는지 체크해 주세요. 중복 클릭 시 clearProfileImage() 호출 순서도 의도대로인지 한번 더 점검 부탁!

Also applies to: 89-91

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

10-13: 로그인 도메인 모델 도입 LGTM

LoginInfo/LoginData 도입으로 시그니처가 명확해졌어요.


14-17: 게스트 로그인 분리 LGTM

디바이스 ID 기반 플로우 분리 적절합니다.

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

68-74: 프로필 Uri 전환 반영 LGTM

profileImageUri로의 전환과 버튼 enabled 연동이 명확합니다.

Also applies to: 92-93

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

177-181: 로그인/게스트 로그인에 DeviceRepository 주입 전환, 구성 타당

네임드 인자 사용도 가독성 좋습니다. 별다른 이슈 없어 보입니다.


182-189: PostGuestLoginUseCase 프로바이더 추가 LGTM

의존성 및 스코프 일관성 OK.


198-202: 회원가입 UseCase에 DeviceRepository 주입 전환 LGTM

온보딩/디바이스 연동 목적과 부합합니다.


6-6: DeviceRepository 바인딩 및 중복 제공 문제 없음
RepositoryModule에서 DeviceRepository가 @BINDS로 올바르게 바인딩되어 있으며, PostCheckNicknameUseCase에 중복된 @provides도 없습니다.

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

24-29: 로그인/게스트 로그인 DTO 기반 전환 LGTM

NullableBaseResponse로 정렬되어 있고, 서비스 호출 인자 네임드 적용도 좋습니다.


30-32: 닉네임 중복 체크 래핑 적절

간결하고 일관적입니다. 클라이언트 단 트림/포맷 검증은 상위 레이어에서 처리하면 좋겠습니다.

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

59-63: startMainActivity 가시성 private 전환 LGTM

이벤트 플로우 기반 내비게이션과 일관적입니다.

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

35-39: 응답 래퍼 일관성 확인(BaseResponse vs NullableBaseResponse)

닉네임 중복 확인만 BaseResponse, 나머지는 NullableBaseResponse를 사용합니다. 의도(데이터 null 가능성)의 차별화라면 OK입니다. 다만 도메인/에러 핸들러에서 두 래퍼를 혼용하는 로직이 누락되지 않았는지 한 번만 점검 부탁드립니다.

buildTypes {
release {
isMinifyEnabled = false
buildConfigField("String", "BASE_URL", properties["base.url.release"].toString())
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

❓ Verification inconclusive

String 리터럴 따옴표 누락 가능성: 컴파일 실패/오동작 예방을 위해 명시적으로 감싸주세요

buildConfigField의 3번째 인자는 소스 코드 리터럴이어야 해서 문자열은 반드시 따옴표로 감싸져야 합니다. local.properties에 값이 "https://..."처럼 이미 따옴표가 포함돼 있지 않다면 현재 코드는 컴파일 에러 또는 잘못된 값 주입을 야기할 수 있습니다. 안전하게 Gradle에서 직접 감싸는 방식으로 고쳐주세요.

-            buildConfigField("String", "BASE_URL", properties["base.url.release"].toString())
+            buildConfigField("String", "BASE_URL", "\"${properties["base.url.release"]}\"")

추가 체크: Retrofit은 baseUrl이 슬래시(/)로 끝나야 합니다. release 값이 반드시 "/"로 끝나는지 확인 부탁드립니다.


buildConfigField에 문자열 리터럴을 반드시 따옴표로 감싸세요

  • buildConfigField 세 번째 인자는 코드 리터럴이므로 문자열은 \"value\" 형태로 전달해야 합니다. 현재 properties["base.url.release"].toString()은 따옴표 없이 주입되어 컴파일 오류를 일으킬 수 있습니다.
  • 아래처럼 수정하고, BASE_URL 값이 반드시 /로 끝나는지 확인하세요.
-            buildConfigField("String", "BASE_URL", properties["base.url.release"].toString())
+            buildConfigField("String", "BASE_URL", "\"${properties["base.url.release"]}\"")
📝 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
buildConfigField("String", "BASE_URL", properties["base.url.release"].toString())
buildConfigField("String", "BASE_URL", "\"${properties["base.url.release"]}\"")
🤖 Prompt for AI Agents
In app/build.gradle.kts around line 39, the buildConfigField call injects
BASE_URL without surrounding quotes which produces invalid Java/Kotlin code;
change the third argument to a quoted string literal by wrapping the property
value in escaped double quotes (i.e. pass
"\"${properties["base.url.release"].toString()}\"" as the value) and ensure the
resolved BASE_URL string ends with a trailing "/" (append one if missing) before
quoting so the generated BuildConfig contains a proper String literal with a
trailing slash.

Comment on lines +10 to +16
class DeviceLocalDataSourceImpl @Inject constructor(
@ApplicationContext context: Context
) : DeviceLocalDataSource {

private val sharedPreferences: SharedPreferences =
context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)

Copy link

@coderabbitai coderabbitai bot Sep 2, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

SharedPreferences DI로 주입 권장 (일관성/테스트 용이성)

직접 생성 대신 DI 모듈(예: @DeviceIdPrefs 또는 @Named)로 주입하면 테스트/모킹이 쉬워지고, 동일 SP를 여러 곳에서 일관되게 사용할 수 있어요.

-class DeviceLocalDataSourceImpl @Inject constructor(
-    @ApplicationContext context: Context
-) : DeviceLocalDataSource {
-
-    private val sharedPreferences: SharedPreferences =
-        context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
+class DeviceLocalDataSourceImpl @Inject constructor(
+    private val sharedPreferences: SharedPreferences
+) : DeviceLocalDataSource {

추가로 DI 모듈에서 동일 PREFERENCES_NAME로 바인딩해 주세요.

📝 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
class DeviceLocalDataSourceImpl @Inject constructor(
@ApplicationContext context: Context
) : DeviceLocalDataSource {
private val sharedPreferences: SharedPreferences =
context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
class DeviceLocalDataSourceImpl @Inject constructor(
private val sharedPreferences: SharedPreferences
) : DeviceLocalDataSource {
// …rest of implementation…
}
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/findu/data/datalocal/datasourceimpl/DeviceLocalDataSourceImpl.kt
around lines 10-16, the class currently creates SharedPreferences directly which
hampers consistency and testability; change the constructor to receive
SharedPreferences via DI (e.g., a @DeviceIdPrefs or @Named injection) and stop
calling context.getSharedPreferences inside the class, then add/update your DI
module to provide a SharedPreferences bound to PREFERENCES_NAME (a @Provides or
@Binds method that calls context.getSharedPreferences(PREFERENCES_NAME,
MODE_PRIVATE)) so the same instance can be injected and easily mocked in tests.

Copy link
Collaborator

Choose a reason for hiding this comment

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

저도 예 동의해서 Shared preference를 주입받아 Context를 몰라도되도록
하는 건어떨까요?

Copy link

Choose a reason for hiding this comment

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

@ikseong00 맞습니다! SharedPreferences를 주입받는 방식이 훨씬 좋은 접근법이에요.

Context 의존성을 제거하면:

  • 테스트할 때 SharedPreferences를 쉽게 모킹할 수 있고
  • 단일 책임 원칙을 더 잘 지킬 수 있어요
  • DI 컨테이너가 SharedPreferences 생명주기를 관리하게 되죠

PR을 보니 SharedPreferencesModule에 이미 @DeviceIdPrefs 한정자로 device용 SharedPreferences 제공자가 추가되어 있네요. 그러면 다음과 같이 리팩토링하면 될 것 같아요:

class DeviceLocalDataSourceImpl @Inject constructor(
    @DeviceIdPrefs private val sharedPreferences: SharedPreferences
) : DeviceLocalDataSource {
    // context.getSharedPreferences 호출 제거
    // 바로 sharedPreferences 사용
}

이렇게 하면 Context를 전혀 알 필요가 없어지고, 테스트 시에도 훨씬 깔끔해질 거예요!

Comment on lines 23 to 26
suspend fun postCheckNickname(
nickname: String
): Response<Unit>
): BaseResponse<CheckNicknameResponseDto>

Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

응답 래퍼(BaseResponse vs NullableBaseResponse) 일관성 확인 요청.

postCheckNicknameBaseResponse를 사용하고 나머지는 NullableBaseResponse를 사용합니다. handleBaseResponse가 두 케이스를 모두 지원하는지 확인 부탁드려요(제네릭 바운드/오버로드 유무).

다음 스크립트로 핸들러 정의를 찾아 타입 시그니처를 확인해 주세요:


🏁 Script executed:

#!/bin/bash
# handleBaseResponse 정의/오버로드 확인
rg -nP --glob '!**/build/**' 'fun\s+<[^>]*>\s*handleBaseResponse\s*\(' -C2
rg -nP --glob '!**/build/**' '(data\s+class|class)\s+NullableBaseResponse' -C2
rg -nP --glob '!**/build/**' '(data\s+class|class)\s+BaseResponse' -C2

Length of output: 1345


handleBaseResponse 정의 및 지원 범위 확인 필요
코드베이스에서 handleBaseResponse 함수 정의가 발견되지 않습니다. BaseResponse<T>NullableBaseResponse<T>를 모두 처리할 수 있도록 제네릭 바운드 또는 오버로드 형태로 올바르게 구현돼 있는지 검토하고, 누락된 경우 함수 정의를 추가해주세요.

🤖 Prompt for AI Agents
In
app/src/main/java/com/example/findu/data/dataremote/datasource/AuthRemoteDataSource.kt
around lines 23 to 26, the call-site expects a helper named handleBaseResponse
but that function is missing or doesn't handle both BaseResponse<T> and
NullableBaseResponse<T>; add a single generic utility (or two overloads) that
accepts the network response type and returns the mapped domain/DTO result while
unwrapping nullable payloads and normalizing error cases. Implement it as a
generic inline function (or two overloads) in the shared remote/util layer that
constrains the input to BaseResponse<T> and NullableBaseResponse<T>
respectively, extracts the payload or returns an appropriate error/exception
when null, and preserves status/meta fields; update imports and use this helper
in postCheckNickname so the function compiles and both response shapes are
supported.

Comment on lines 7 to 12
data class GuestLoginResponseDto(
@SerialName("userId")
val userId: Long,
@SerialName("accessToken")
val accessToken: String
)
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

액세스 토큰 로그 노출 방지: toString 마스킹

data class 기본 toString()이 accessToken을 그대로 노출할 수 있어 보안/로그 유출 리스크가 있습니다. 간단히 toString을 오버라이드해 마스킹해 주세요.

 @Serializable
 data class GuestLoginResponseDto(
     @SerialName("userId")
     val userId: Long,
     @SerialName("accessToken")
     val accessToken: String
-) 
+) {
+    override fun toString(): String =
+        "GuestLoginResponseDto(userId=$userId, accessToken=**redacted**)"
+}

로그에서 토큰이 찍히는지 점검:


🏁 Script executed:

#!/bin/bash
rg -nP -C2 '(Log\.(d|i|w|e)|Timber\.(d|i|w|e)|println).*accessToken|GuestLoginResponseDto\(' -g '!**/build/**'

Length of output: 1363


GuestLoginResponseDto.toString에서 accessToken 마스킹 구현 및 OAuthToken 로그 검토

  • app/src/main/java/com/example/findu/data/dataremote/model/response/auth/GuestLoginResponseDto.kt: 기본 data class toString()에 accessToken이 노출되므로, toString()을 오버라이드해 accessToken을 “redacted” 등으로 마스킹하세요.
  • app/src/main/java/com/example/findu/presentation/ui/login/LoginActivity.kt:39 — Log.d(TAG, "oAuth_AccessToken: ${oAuthToken.accessToken}")가 토큰을 그대로 출력하니, 해당 로그를 제거하거나 마스킹 처리하세요.
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/findu/data/dataremote/model/response/auth/GuestLoginResponseDto.kt
around lines 7–12, the data class exposes accessToken via the default
toString(); override toString() to return userId and a masked accessToken (e.g.,
"**redacted**" or show only last 4 chars) to prevent logging the full token. In
app/src/main/java/com/example/findu/presentation/ui/login/LoginActivity.kt at
line 39, remove the Log.d that prints oAuthToken.accessToken or change it to log
a masked token (do not log the full accessToken); ensure no plain token strings
remain in logs.

Comment on lines 14 to 22
@Serializable
data class UserInfoDto(
@SerialName("userId")
val userId: Long,
@SerialName("nickname")
val nickname: String,
@SerialName("accessToken")
val accessToken: String
) 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.

💡 Verification agent

🧩 Analysis chain

액세스 토큰 로그 노출 방지: UserInfoDto.toString 마스킹

UserInfoDto 또한 data class 기본 toString으로 accessToken이 노출됩니다. 보안 사고 예방을 위해 마스킹 권장합니다.

 @Serializable
 data class UserInfoDto(
     @SerialName("userId")
     val userId: Long,
     @SerialName("nickname")
     val nickname: String,
     @SerialName("accessToken")
     val accessToken: String
-) 
+) {
+    override fun toString(): String =
+        "UserInfoDto(userId=$userId, nickname=$nickname, accessToken=**redacted**)"
+}

토큰이 로그에 직접 출력되는지 스캔:


🏁 Script executed:

#!/bin/bash
rg -nP -C2 '(Log\.(d|i|w|e)|Timber\.(d|i|w|e)|println).*(accessToken|UserInfoDto\()' -g '!**/build/**'

Length of output: 760


로그에 액세스 토큰 노출 제거

  • app/src/main/java/com/example/findu/presentation/ui/login/LoginActivity.kt:39에서 Log.d(TAG, "oAuth_AccessToken: ${oAuthToken.accessToken}")로 직접 토큰이 출력되고 있습니다. 해당 로그를 제거하거나 마스킹 처리하세요.
  • UserInfoDto의 기본 toString()도 accessToken을 노출하므로, toString()을 오버라이드해 토큰을 마스킹 처리해야 합니다.
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/findu/data/dataremote/model/response/auth/LoginResponseDto.kt
around lines 14 to 22, and
app/src/main/java/com/example/findu/presentation/ui/login/LoginActivity.kt at
line 39: remove or mask the direct accessToken log in LoginActivity (replace the
Log.d that prints the raw token with either no logging or a masked token
representation), and in UserInfoDto override toString() to return a safe
representation that includes userId and nickname but masks accessToken (e.g.,
show only last 4 chars or replace with "***") so the token is never printed in
logs or stack traces.

Comment on lines 27 to 39
fun postLogin(kakaoId: Long) {
viewModelScope.launch {
loginUseCase.postLogin(email, password).fold(
onSuccess = {
_loginResult.value = true
},
onFailure = { _loginResult.value = false }
)
loginUseCase.postLogin(kakaoId = kakaoId).onSuccess { loginData ->
if (loginData.isFirstLogin) {
startOnboardingActivity()
} else {
setAccessTokenUseCase(accessToken = loginData.userInfo!!.accessToken)
startMainActivity()
}
}.onFailure { e ->
Log.d("http", "Error Message: : $e")
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Null 가능성 안전 처리 및 로그 메시지 포맷 보완

loginData.userInfo!!는 NPE 리스크가 있습니다. 널 세이프 처리와 로그 포맷(콜론 중복 제거)을 제안합니다.

아래 수정으로 안전하게 처리할 수 있습니다.

-            loginUseCase.postLogin(kakaoId = kakaoId).onSuccess { loginData ->
-                if (loginData.isFirstLogin) {
-                    startOnboardingActivity()
-                } else {
-                    setAccessTokenUseCase(accessToken = loginData.userInfo!!.accessToken)
-                    startMainActivity()
-                }
-            }.onFailure { e ->
-                Log.d("http", "Error Message: : $e")
-            }
+            loginUseCase.postLogin(kakaoId = kakaoId)
+                .onSuccess { loginData ->
+                    if (loginData.isFirstLogin) {
+                        startOnboardingActivity()
+                    } else {
+                        val token = loginData.userInfo?.accessToken
+                        if (token != null) {
+                            setAccessTokenUseCase(accessToken = token)
+                            startMainActivity()
+                        } else {
+                            Log.w("auth", "Missing accessToken on non-first login")
+                        }
+                    }
+                }
+                .onFailure { e ->
+                    Log.d("http", "Error Message: $e")
+                }
📝 Committable suggestion

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

Suggested change
fun postLogin(kakaoId: Long) {
viewModelScope.launch {
loginUseCase.postLogin(email, password).fold(
onSuccess = {
_loginResult.value = true
},
onFailure = { _loginResult.value = false }
)
loginUseCase.postLogin(kakaoId = kakaoId).onSuccess { loginData ->
if (loginData.isFirstLogin) {
startOnboardingActivity()
} else {
setAccessTokenUseCase(accessToken = loginData.userInfo!!.accessToken)
startMainActivity()
}
}.onFailure { e ->
Log.d("http", "Error Message: : $e")
}
}
fun postLogin(kakaoId: Long) {
viewModelScope.launch {
loginUseCase.postLogin(kakaoId = kakaoId)
.onSuccess { loginData ->
if (loginData.isFirstLogin) {
startOnboardingActivity()
} else {
val token = loginData.userInfo?.accessToken
if (token != null) {
setAccessTokenUseCase(accessToken = token)
startMainActivity()
} else {
Log.w("auth", "Missing accessToken on non-first login")
}
}
}
.onFailure { e ->
Log.d("http", "Error Message: $e")
}
}
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/findu/presentation/ui/login/viewmodel/LoginViewModel.kt
around lines 27 to 39, replace the unsafe forced unwrap of loginData.userInfo!!
with a null-safe handling: check if loginData.userInfo is null and if so log an
appropriate message and avoid calling setAccessTokenUseCase/startMainActivity;
only call setAccessTokenUseCase and startMainActivity when userInfo is non-null,
and change the failure log format to remove the duplicated colon and print the
exception message or stack (e.g. "Error Message: ${e.message}" or include
e.toString()) so logs are clear.

Comment on lines +114 to 127
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 = 4241046198
).onSuccess {
startMainActivity()
}.onFailure {e->
Log.d("http", "Error Message: : $e")
}
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

메인 스레드에서 파일 I/O 수행 + 가입 전 최종 검증 누락 + 기본이미지/업로드 동시 전달 가능성.

  • URI→File 변환이 메인 스레드에서 실행됩니다(ANR 리스크).
  • 닉네임이 VALID가 아닌데도 가입을 시도할 수 있어요.
  • 업로드 파일이 있을 때도 defaultImageName을 항상 보냅니다(해석 충돌 가능).

아래처럼 수정 제안드립니다.

-    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 = 4241046198
-            ).onSuccess {
-                startMainActivity()
-            }.onFailure {e->
-                Log.d("http", "Error Message: : $e")
-            }
-        }
-    }
+    private fun signUp() {
+        viewModelScope.launch {
+            // 닉네임 최종 유효성 검증
+            if (_uiState.value.nickNameValidState != NicknameValidType.VALID) {
+                _uiState.update { it.copy(nickNameValidState = NicknameValidType.FORMAT_INVALID) }
+                return@launch
+            }
+
+            // 파일 I/O는 백그라운드에서
+            val imageFile = withContext(Dispatchers.IO) {
+                _uiState.value.profileImageUri?.let { uriToFile(uri = it) }
+            }
+
+            postSignupUseCase.postSignup(
+                profileImageFile = imageFile,
+                // 파일이 있으면 기본이미지 이름은 보내지 않음
+                defaultImageName = imageFile?.let { null } ?: _uiState.value.defaultProfileType.string,
+                nickname = _uiState.value.nickname,
+                kakaoId = 4241046198 // TODO: 실제 인증 세션/저장소에서 kakaoId 주입
+            ).onSuccess {
+                startMainActivity()
+            }.onFailure { e ->
+                Log.d("http", "Signup failed: $e")
+            }
+        }
+    }
📝 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 signUp() {
viewModelScope.launch {
postSignupUseCase.postSignup(
profileImageFile = uiState.value.profileImageUri?.let { uriToFile(uri = it) },
defaultImageName = uiState.value.defaultProfileType.string,
nickname = uiState.value.nickname,
kakaoId = 4241046198
).onSuccess {
startMainActivity()
}.onFailure {e->
Log.d("http", "Error Message: : $e")
}
}
}
private fun signUp() {
viewModelScope.launch {
// 닉네임 최종 유효성 검증
if (_uiState.value.nickNameValidState != NicknameValidType.VALID) {
_uiState.update { it.copy(nickNameValidState = NicknameValidType.FORMAT_INVALID) }
return@launch
}
// 파일 I/O는 백그라운드에서
val imageFile = withContext(Dispatchers.IO) {
_uiState.value.profileImageUri?.let { uriToFile(uri = it) }
}
postSignupUseCase.postSignup(
profileImageFile = imageFile,
// 파일이 있으면 기본이미지 이름은 보내지 않음
defaultImageName = imageFile?.let { null } ?: _uiState.value.defaultProfileType.string,
nickname = _uiState.value.nickname,
kakaoId = 4241046198 // TODO: 실제 인증 세션/저장소에서 kakaoId 주입
).onSuccess {
startMainActivity()
}.onFailure { e ->
Log.d("http", "Signup failed: $e")
}
}
}

Comment on lines +120 to +121
kakaoId = 4241046198
).onSuccess {
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

kakaoId 하드코딩 제거 필요.

고정값(4241046198)을 그대로 전송하면 다른 계정으로 가입되거나 서버 일관성이 깨질 수 있어요. 로그인/세션에서 확보한 실제 kakaoId를 주입해 주세요(예: 로컬 저장소/세션 매니저/UseCase 내부에서 조회).

다음 스크립트로 kakaoId를 어디서 관리하는지 확인해 보세요:

원하시면 kakaoId 공급자 주입(예: AuthSession/KakaoAccountStore) 설계를 함께 제안드릴게요.


🏁 Script executed:

#!/bin/bash
rg -nP --glob '!**/build/**' 'class\s+PostSignupUseCase\b' -C5
rg -nP --glob '!**/build/**' '\bkakaoId\b' -C2

Length of output: 11604


하드코딩된 kakaoId 제거 및 실제 id 주입
OnboardingViewModel.kt 120행의 kakaoId(4241046198)를 로그인 시 획득된 값(예: LoginActivity → LoginViewModel을 통해 전달된 kakaoId)을 보관하는 세션 매니저(또는 UseCase 파라미터)로부터 가져오도록 수정하세요.

🤖 Prompt for AI Agents
In
app/src/main/java/com/example/findu/presentation/ui/onboarding/viewmodel/OnboardingViewModel.kt
around lines 120-121 the kakaoId is hardcoded as 4241046198; remove the
hardcoded value and retrieve the real kakaoId from the session source (e.g.,
SessionManager, SavedStateHandle, or a constructor/use-case parameter populated
by LoginViewModel). Change the call site to use the injected/session-provided
kakaoId (e.g., sessionManager.getKakaoId() or a viewModel parameter) and add
null/absent handling (throw, log, or early return) if the id isn’t available;
ensure dependency injection or function signature is updated so the real id
flows from LoginActivity/LoginViewModel into OnboardingViewModel.

Comment on lines +140 to +152
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
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

InputStream 닫힘 누락으로 인한 리소스 누수.

inputStreamuse {}로 감싸지지 않아 닫히지 않습니다. 아래처럼 수정해 주세요.

-    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 {
+            context.contentResolver.openInputStream(uri)?.use { inputStream ->
+                val file = File.createTempFile("profile_", ".jpg", context.cacheDir)
+                file.outputStream().use { outputStream ->
+                    inputStream.copyTo(outputStream)
+                }
+                file
+            }
+        } catch (e: Exception) {
+            e.printStackTrace()
+            return 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
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 {
context.contentResolver.openInputStream(uri)?.use { inputStream ->
val file = File.createTempFile("profile_", ".jpg", context.cacheDir)
file.outputStream().use { outputStream ->
inputStream.copyTo(outputStream)
}
file
}
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/findu/presentation/ui/onboarding/viewmodel/OnboardingViewModel.kt
around lines 140 to 152, the InputStream opened from
context.contentResolver.openInputStream(uri) is not closed which can leak
resources; update the method to use Kotlin's use { } on the inputStream (or both
inputStream and file.outputStream()) so the stream is automatically closed even
on exceptions, and remove the explicit e.printStackTrace() in favor of logging
the exception if needed while returning null on failure.

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 47 to 50
defaultImageName = defaultImageName?.toRequestBody("text/plain".toMediaTypeOrNull()),
nickname = nickname.toRequestBody("text/plain".toMediaTypeOrNull()),
kakaoId = kakaoId.toString().toRequestBody("text/plain".toMediaTypeOrNull()),
deviceId = deviceId.toRequestBody("text/plain".toMediaTypeOrNull())
Copy link
Collaborator

Choose a reason for hiding this comment

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

확장함수로 만들어서 간단하게 만들면 어떨까요?
재사용성도 좋아질것 같습니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

인정합니다!

@SerialName("code") val code: Int,
@SerialName("message") val message: String,
@SerialName("data") val data: T? = null
@SerialName("data") val data: T
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍

val deviceId = Settings.Secure.getString(this.contentResolver, Settings.Secure.ANDROID_ID)
?: "unknown_device_id"

setDeviceIdUseCase(deviceId=deviceId)
Copy link
Collaborator

Choose a reason for hiding this comment

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

굳이 뷰모델 호출없이 액티비티에서
Use case 호출하는것도 좋아보이네요!

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/mapper/torequest/TextRequestBodyMapper.kt (2)

10-12: text/plain은 고정값이므로 toMediaType() 사용 + charset 명시 제안

현재도 동작하지만 실패를 조기 발견하고 서버와의 인코딩 오해를 줄이기 위해 UTF-8을 명시하는 걸 권장합니다.

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

추가 import:

import okhttp3.MediaType.Companion.toMediaType

13-18: 멀티파트 이미지 Content-Type을 구체 타입으로 지정해주세요
app/src/main/java/com/example/findu/data/mapper/torequest/TextRequestBodyMapper.kt (13–18행)

toImageMultipart 함수에서 "image/*".toMediaTypeOrNull() 대신 파일 확장자에 따른 구체 MediaType을 반환하는 inferImageMediaType()를 사용해보세요. 예:

 fun File.toImageMultipart(name: String): MultipartBody.Part =
     MultipartBody.Part.createFormData(
         name,
         this.name,
-        asRequestBody("image/*".toMediaTypeOrNull())
+        asRequestBody(inferImageMediaType())
     )
private fun File.inferImageMediaType(): MediaType = when (extension.lowercase()) {
    "jpg", "jpeg" -> "image/jpeg".toMediaType()
    "png"         -> "image/png".toMediaType()
    "webp"        -> "image/webp".toMediaType()
    "gif"         -> "image/gif".toMediaType()
    "heic"        -> "image/heic".toMediaType()
    "heif"        -> "image/heif".toMediaType()
    else          -> "application/octet-stream".toMediaType()
}

호출부의 필드명(profileImage)도 일관된 것을 확인했습니다. null 반환 방지를 통해 서버 처리 안정성을 높여보세요.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3bad5b1 and 8d428ef.

📒 Files selected for processing (2)
  • app/src/main/java/com/example/findu/data/dataremote/datasourceimpl/AuthRemoteDataSourceImpl.kt (1 hunks)
  • app/src/main/java/com/example/findu/data/mapper/torequest/TextRequestBodyMapper.kt (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/main/java/com/example/findu/data/dataremote/datasourceimpl/AuthRemoteDataSourceImpl.kt
🔇 Additional comments (1)
app/src/main/java/com/example/findu/data/mapper/torequest/TextRequestBodyMapper.kt (1)

1-9: LGTM — 확장함수로 요청 바디/파트 생성 추출한 방향 좋아요

패키지 위치와 네이밍이 명확해 재사용성 높아졌습니다.

@ikseong00 ikseong00 requested a review from Copilot September 5, 2025 14:02
@ikseong00 ikseong00 review requested due to automatic review settings September 5, 2025 14:03
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.

api 연동할때 참고해야겠습니다.... 좋은거같아요!!

@t1nm1ksun t1nm1ksun merged commit b160e22 into develop Sep 17, 2025
1 check passed
@coderabbitai coderabbitai bot mentioned this pull request Sep 17, 2025
@coderabbitai coderabbitai bot mentioned this pull request Oct 6, 2025
1 task
@coderabbitai coderabbitai bot mentioned this pull request Oct 21, 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