Conversation
Walkthrough인증/온보딩 전반 리팩터링. 카카오/게스트 로그인 도입, 닉네임 중복검사·회원가입 API 전환 및 DTO/응답 래퍼 변경, 멀티파트 회원가입 추가. 디바이스 ID 로컬 저장/주입 체계 신설. 온보딩 UI는 Uri 기반 이미지 처리와 닉네임 검증으로 수정. 구(이메일) 가입 흐름 제거. Changes
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
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 이벤트
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: 메인 화면으로 이동
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Possibly related PRs
Suggested reviewers
Poem
✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
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/GuestLoginData에accessToken이 들어가면 캐시·로그 등에서 과도하게 전파될 수 있어 보안 리스크가 커집니다. 토큰은 별도 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 시그니처 재확인 요청
onLoadFailed의target파라미터는 보통 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,LoginInfoimport는 제거하는 게 좋습니다.-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.UserInfoapp/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: Longapp/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 → profileImageUriUI 상태(
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: 불리언 의미 명시 필요
postCheckNickname의Boolean의미(사용 가능/불가)와 에러 케이스 구분 방식을 KDoc 등으로 명확히 해 주세요. 호출부 오해를 줄일 수 있어요.
22-28: 회원가입 파라미터 응집 및 불변식 보장현재 시그니처는
profileImageFile/defaultImageName이 동시에 null 또는 동시에 값이 될 수 있어 불변식이 약합니다. 프로필 입력을 모델로 캡슐화하여 “하나만 선택”을 타입 수준에서 보장하는 것을 권장합니다. 또한defaultImageName은DefaultProfileType등 도메인 타입으로 받으면 매핑 오류를 예방할 수 있습니다.- 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: 메서드명-반환타입 불일치: 혼동 방지를 위해 리네이밍 권장
providePostCheckEmailUseCase가PostCheckNicknameUseCase를 반환합니다. 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: 프로필 이미지 전달 방식의 불변식 보장 필요(동시 설정 방지).
profileImageFile와defaultImageName가 동시에 전달되는 상황을 타입으로 막아두면 서버/클라이언트 해석 혼선을 줄일 수 있어요. 이미지 업로드 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.
📒 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.ktapp/src/main/java/com/example/findu/data/dataremote/model/response/auth/GuestLoginResponseDto.ktapp/src/main/java/com/example/findu/data/mapper/todomain/UserInfoDtoMapper.ktapp/src/main/java/com/example/findu/domain/model/AuthData.ktapp/src/main/java/com/example/findu/data/dataremote/model/request/LoginRequestDto.ktapp/src/main/java/com/example/findu/data/mapper/torequest/GuestLoginRequestDtoMapper.ktapp/src/main/java/com/example/findu/data/dataremote/model/response/auth/LoginResponseDto.ktapp/src/main/java/com/example/findu/data/dataremote/service/AuthService.ktapp/src/main/java/com/example/findu/data/mapper/todomain/LoginResponseDtoMapper.ktapp/src/main/java/com/example/findu/data/mapper/toDomain/GuestLoginResponseDtoMapper.ktapp/src/main/java/com/example/findu/data/dataremote/datasource/AuthRemoteDataSource.ktapp/src/main/java/com/example/findu/data/dataremote/datasourceimpl/AuthRemoteDataSourceImpl.ktapp/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()으로 안전하게 매핑되었고,- 도메인 모델
LoginData도UserInfo?로 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
String→Uri?전환과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()) |
There was a problem hiding this comment.
💡 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.
| 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.
| class DeviceLocalDataSourceImpl @Inject constructor( | ||
| @ApplicationContext context: Context | ||
| ) : DeviceLocalDataSource { | ||
|
|
||
| private val sharedPreferences: SharedPreferences = | ||
| context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) | ||
|
|
There was a problem hiding this comment.
🛠️ 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.
| 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.
There was a problem hiding this comment.
저도 예 동의해서 Shared preference를 주입받아 Context를 몰라도되도록
하는 건어떨까요?
There was a problem hiding this comment.
@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를 전혀 알 필요가 없어지고, 테스트 시에도 훨씬 깔끔해질 거예요!
| suspend fun postCheckNickname( | ||
| nickname: String | ||
| ): Response<Unit> | ||
| ): BaseResponse<CheckNicknameResponseDto> | ||
|
|
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
응답 래퍼(BaseResponse vs NullableBaseResponse) 일관성 확인 요청.
postCheckNickname만 BaseResponse를 사용하고 나머지는 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' -C2Length 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.
| data class GuestLoginResponseDto( | ||
| @SerialName("userId") | ||
| val userId: Long, | ||
| @SerialName("accessToken") | ||
| val accessToken: String | ||
| ) |
There was a problem hiding this comment.
💡 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.
| @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 |
There was a problem hiding this comment.
💡 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.
app/src/main/java/com/example/findu/presentation/ui/login/LoginActivity.kt
Outdated
Show resolved
Hide resolved
| 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") | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| 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.
| 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") | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| 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") | |
| } | |
| } | |
| } |
| kakaoId = 4241046198 | ||
| ).onSuccess { |
There was a problem hiding this comment.
💡 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' -C2Length 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.
| 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 | ||
| } | ||
| } |
There was a problem hiding this comment.
InputStream 닫힘 누락으로 인한 리소스 누수.
inputStream이 use {}로 감싸지지 않아 닫히지 않습니다. 아래처럼 수정해 주세요.
- 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.
| 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.
ikseong00
left a comment
There was a problem hiding this comment.
코드 양이 많은데 고생하셨습니다!
리뷰 사항 확인부탁드려요!
| 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()) |
There was a problem hiding this comment.
확장함수로 만들어서 간단하게 만들면 어떨까요?
재사용성도 좋아질것 같습니다!
| @SerialName("code") val code: Int, | ||
| @SerialName("message") val message: String, | ||
| @SerialName("data") val data: T? = null | ||
| @SerialName("data") val data: T |
| val deviceId = Settings.Secure.getString(this.contentResolver, Settings.Secure.ANDROID_ID) | ||
| ?: "unknown_device_id" | ||
|
|
||
| setDeviceIdUseCase(deviceId=deviceId) |
There was a problem hiding this comment.
굳이 뷰모델 호출없이 액티비티에서
Use case 호출하는것도 좋아보이네요!
There was a problem hiding this comment.
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.
📒 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 — 확장함수로 요청 바디/파트 생성 추출한 방향 좋아요패키지 위치와 네이밍이 명확해 재사용성 높아졌습니다.
nasohee
left a comment
There was a problem hiding this comment.
api 연동할때 참고해야겠습니다.... 좋은거같아요!!
Related issue 🛠
Work Description 📝
Screenshot 📸
Uncompleted Tasks 😅
To Reviewers 📢
현재 서버측에서 멀티파트로 전송한 이미지의 처리가 되지 않아 기본이미지로만 가입이 가능한 상태입니다.
클라측 로직상 서버측 문제가 해결되면 별 문제 없을것으로 예상되지만 추후 문제가 생긴다면 이슈 파서 해결하도록 하겠습니다!
영상이나 스크린샷을 첨부하고 싶은데 한번 회원가입 성공하니까 포스트맨으로 회원탈퇴 api를 쏴서 응답이 200으로 성공해도 왜인지 탈퇴가 되지 않아 찍을 수가 없네요..
Summary by CodeRabbit
신기능
변경
리팩터링