Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
5445bb0
Merge remote-tracking branch 'origin/feat-login' into feat-onboarding…
t1nm1ksun Aug 17, 2025
c33914c
[feat] #78: 닉네임 중복체크 서비스 구현
t1nm1ksun Aug 17, 2025
799564c
[feat] #78: 닉네임 중복체크 DTO 구현
t1nm1ksun Aug 17, 2025
ba66a15
[feat] #78: 닉네임 중복체크 DataSource 구현
t1nm1ksun Aug 17, 2025
1739860
[feat] #78: 닉네임 중복체크 Repository 구현
t1nm1ksun Aug 17, 2025
c3ca3bd
[feat] #78: 닉네임 중복체크 유스케이스 구현
t1nm1ksun Aug 17, 2025
a856963
[delete] #78: 기존 이메일 중복체크 로직 삭제
t1nm1ksun Aug 17, 2025
e5698ea
[feat] #78: SignUp 서비스 구현
t1nm1ksun Aug 17, 2025
cbbeb3b
[feat] #78: SignUp 데이터소스구현
t1nm1ksun Aug 17, 2025
8d1f133
[feat] #78: SignUp 유저정보 매퍼 구현
t1nm1ksun Aug 17, 2025
d5f74ab
[feat] #78: SignUp Repository 구현
t1nm1ksun Aug 17, 2025
e5cd752
[feat] #78: SignUp 유스케이스 구현
t1nm1ksun Aug 17, 2025
9f106d4
[feat] #78: SignUp 다음버튼상태 derivedState로 변경
t1nm1ksun Aug 17, 2025
e8892d1
[chore] #78: 변수명 URL 에서 URI로 수정
t1nm1ksun Aug 17, 2025
a225691
[feat] #78: DeviceId 로컬에서 관리
t1nm1ksun Aug 18, 2025
91ccfa1
[feat] #78: 회원가입 body deviceId 추가
t1nm1ksun Aug 18, 2025
f6461b7
[feat] #78: 로그인 및 회원가입 API 응답 nullable 응답으로 수정
t1nm1ksun Sep 2, 2025
3bad5b1
[chore] #78: 토큰 로그 제거
t1nm1ksun Sep 2, 2025
966ff22
Merge remote-tracking branch 'origin/develop' into feat-onboarding-api
t1nm1ksun Sep 2, 2025
8d428ef
[feat] #78: 멀티파트 매퍼 구현
t1nm1ksun Sep 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.example.findu.data.datalocal.datasource

interface DeviceLocalDataSource {
var deviceId: String
fun clear()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.example.findu.data.datalocal.datasourceimpl

import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
import com.example.findu.data.datalocal.datasource.DeviceLocalDataSource
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject

class DeviceLocalDataSourceImpl @Inject constructor(
@ApplicationContext context: Context
) : DeviceLocalDataSource {

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

Comment on lines +10 to +16
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를 전혀 알 필요가 없어지고, 테스트 시에도 훨씬 깔끔해질 거예요!

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

override fun clear() = sharedPreferences.edit { clear() }

companion object {
const val PREFERENCES_NAME = "device_preferences"
const val DEVICE_ID = "token"
const val INITIAL_VALUE = ""
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class TokenLocalDataSourceImpl @Inject constructor(
set(value) = sharedPreferences.edit { putString(REFRESH_TOKEN, value) }

override fun clearToken() {
sharedPreferences.edit().clear().apply()
sharedPreferences.edit { clear() }
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,34 @@
package com.example.findu.data.dataremote.datasource

import com.example.findu.data.dataremote.model.base.BaseResponse
import com.example.findu.data.dataremote.model.base.NullableBaseResponse
import com.example.findu.data.dataremote.model.request.GuestLoginRequestDto
import com.example.findu.data.dataremote.model.request.LoginRequestDto
import com.example.findu.data.dataremote.model.response.CheckEmailResponseDto
import com.example.findu.data.dataremote.model.response.CheckNicknameResponseDto
import com.example.findu.data.dataremote.model.response.auth.GuestLoginResponseDto
import com.example.findu.data.dataremote.model.response.auth.LoginResponseDto
import com.example.findu.data.dataremote.model.response.auth.UserInfoDto
import retrofit2.Response
import java.io.File

interface AuthRemoteDataSource {
suspend fun postLogin(
loginRequestDto: LoginRequestDto
): BaseResponse<LoginResponseDto>
): NullableBaseResponse<LoginResponseDto>

suspend fun postGuestLogin(
guestLoginRequestDto: GuestLoginRequestDto
): BaseResponse<GuestLoginResponseDto>
): NullableBaseResponse<GuestLoginResponseDto>

suspend fun postCheckEmail(
email: String
): BaseResponse<CheckEmailResponseDto>
suspend fun postCheckNickname(
nickname: String
): BaseResponse<CheckNicknameResponseDto>

suspend fun postSignup(
email: String,
password: String,
nickname: String
): Response<Unit>
profileImageFile: File?,
defaultImageName: String?,
nickname: String,
kakaoId: Long,
deviceId: String
): NullableBaseResponse<UserInfoDto>
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,42 @@ package com.example.findu.data.dataremote.datasourceimpl
import com.example.findu.data.dataremote.datasource.AuthRemoteDataSource
import com.example.findu.data.dataremote.model.base.BaseResponse
import com.example.findu.data.dataremote.model.base.NullableBaseResponse
import com.example.findu.data.dataremote.model.request.CheckEmailRequestDto
import com.example.findu.data.dataremote.model.request.CheckNicknameRequestDto
import com.example.findu.data.dataremote.model.request.GuestLoginRequestDto
import com.example.findu.data.dataremote.model.request.LoginRequestDto
import com.example.findu.data.dataremote.model.request.SignupRequestDto
import com.example.findu.data.dataremote.model.response.CheckEmailResponseDto
import com.example.findu.data.dataremote.model.response.CheckNicknameResponseDto
import com.example.findu.data.dataremote.model.response.auth.GuestLoginResponseDto
import com.example.findu.data.dataremote.model.response.auth.LoginResponseDto
import com.example.findu.data.dataremote.model.response.auth.UserInfoDto
import com.example.findu.data.dataremote.service.AuthService
import retrofit2.Response
import com.example.findu.data.mapper.torequest.toImageMultipart
import com.example.findu.data.mapper.torequest.toPlainTextRequestBody
import java.io.File
import javax.inject.Inject

class AuthRemoteDataSourceImpl @Inject constructor(
private val authService: AuthService
) : AuthRemoteDataSource {
override suspend fun postLogin(loginRequestDto: LoginRequestDto): BaseResponse<LoginResponseDto> =
override suspend fun postLogin(loginRequestDto: LoginRequestDto): NullableBaseResponse<LoginResponseDto> =
authService.postLogin(loginRequestDto=loginRequestDto)

override suspend fun postGuestLogin(guestLoginRequestDto: GuestLoginRequestDto): BaseResponse<GuestLoginResponseDto> =
override suspend fun postGuestLogin(guestLoginRequestDto: GuestLoginRequestDto): NullableBaseResponse<GuestLoginResponseDto> =
authService.postGuestLogin(guestLoginRequestDto=guestLoginRequestDto)

override suspend fun postCheckEmail(email: String): BaseResponse<CheckEmailResponseDto> =
authService.postCheckEmail(CheckEmailRequestDto(email))
override suspend fun postCheckNickname(nickname: String): BaseResponse<CheckNicknameResponseDto> =
authService.postCheckNickname(CheckNicknameRequestDto(nickname))

override suspend fun postSignup(
email: String,
password: String,
nickname: String
): Response<Unit> = authService.postSignup(SignupRequestDto(email, password, nickname))
profileImageFile: File?,
defaultImageName: String?,
nickname: String,
kakaoId: Long,
deviceId: String
): NullableBaseResponse<UserInfoDto> = authService.postSignup(
profileImage = profileImageFile?.toImageMultipart("profileImage"),
defaultImageName = defaultImageName?.toPlainTextRequestBody(),
nickname = nickname.toPlainTextRequestBody(),
kakaoId = kakaoId.toString().toPlainTextRequestBody(),
deviceId = deviceId.toPlainTextRequestBody()
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ data class BaseResponse<T>(
@SerialName("success") val success: Boolean,
@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.

👍

)
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class CheckEmailRequestDto(
@SerialName("email")
val email: String
data class CheckNicknameRequestDto(
@SerialName("nickname")
val nickname: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class CheckEmailResponseDto(
@SerialName("isDuplicateEmail")
val isDuplicateEmail: Boolean
data class CheckNicknameResponseDto(
@SerialName("isDuplicate")
val isDuplicate: Boolean
)
Original file line number Diff line number Diff line change
@@ -1,38 +1,49 @@
package com.example.findu.data.dataremote.service

import com.example.findu.data.dataremote.model.base.BaseResponse
import com.example.findu.data.dataremote.model.request.CheckEmailRequestDto
import com.example.findu.data.dataremote.model.base.NullableBaseResponse
import com.example.findu.data.dataremote.model.request.CheckNicknameRequestDto
import com.example.findu.data.dataremote.model.request.GuestLoginRequestDto
import com.example.findu.data.dataremote.model.request.LoginRequestDto
import com.example.findu.data.dataremote.model.request.SignupRequestDto
import com.example.findu.data.dataremote.model.response.CheckEmailResponseDto
import com.example.findu.data.dataremote.model.response.CheckNicknameResponseDto
import com.example.findu.data.dataremote.model.response.auth.GuestLoginResponseDto
import com.example.findu.data.dataremote.model.response.auth.LoginResponseDto
import com.example.findu.data.dataremote.model.response.auth.UserInfoDto
import com.example.findu.data.dataremote.util.ApiConstraints.API
import com.example.findu.data.dataremote.util.ApiConstraints.AUTH
import com.example.findu.data.dataremote.util.ApiConstraints.VERSION
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part

interface AuthService {
@POST("/$API/$VERSION/$AUTH/login/kakao")
suspend fun postLogin(
@Body loginRequestDto: LoginRequestDto
): BaseResponse<LoginResponseDto>
): NullableBaseResponse<LoginResponseDto>

@POST("/$API/$VERSION/$AUTH/login/guest")
suspend fun postGuestLogin(
@Body guestLoginRequestDto: GuestLoginRequestDto
): BaseResponse<GuestLoginResponseDto>
): NullableBaseResponse<GuestLoginResponseDto>

@POST("/$API/$VERSION/$AUTH/check/duplicate-email")
suspend fun postCheckEmail(
@Body checkEmailRequestDto: CheckEmailRequestDto
): BaseResponse<CheckEmailResponseDto>
@POST("/$API/$VERSION/users/check/duplicate-nickname")
suspend fun postCheckNickname(
@Body checkNicknameRequestDto: CheckNicknameRequestDto
): BaseResponse<CheckNicknameResponseDto>

@POST("/$API/$VERSION/$AUTH/signup")
@Multipart
@POST("/$API/$VERSION/users")
suspend fun postSignup(
@Body signupRequestBody: SignupRequestDto
): Response<Unit>
@Part profileImage: MultipartBody.Part?,
@Part("defaultProfileImageName") defaultImageName: RequestBody?,
@Part("nickname") nickname: RequestBody,
@Part("kakaoId") kakaoId: RequestBody,
@Part("deviceId") deviceId: RequestBody
): NullableBaseResponse<UserInfoDto>
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import com.example.findu.data.dataremote.model.base.NullableBaseResponse
fun <T> BaseResponse<T>.handleBaseResponse(): Result<T> =
when (this.code) {
in 200..299 -> { // 성공
data?.let { Result.success(it) }
?: Result.failure(IllegalStateException("data is null"))
Result.success(this.data)
}

in 400..499 -> { // 클라이언트 에러
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,5 @@ import com.example.findu.domain.model.UserInfo
fun LoginResponseDto.toDomain(): LoginData =
LoginData(
isFirstLogin = isFirstLogin,
userInfo = userInfo?.let {
UserInfo(
userId = it.userId,
nickname = it.nickname,
accessToken = it.accessToken
)
}
userInfo = userInfo?.toDomain()
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.findu.data.mapper.todomain

import com.example.findu.data.dataremote.model.response.auth.UserInfoDto
import com.example.findu.domain.model.UserInfo

fun UserInfoDto.toDomain(): UserInfo =
UserInfo(
userId = this.userId,
nickname = this.nickname,
accessToken = this.accessToken
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.example.findu.data.mapper.torequest

import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File

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

fun File.toImageMultipart(name: String): MultipartBody.Part =
MultipartBody.Part.createFormData(
name,
this.name,
asRequestBody("image/*".toMediaTypeOrNull())
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,50 +4,48 @@ import com.example.findu.data.dataremote.datasource.AuthRemoteDataSource
import com.example.findu.data.dataremote.util.handleBaseResponse
import com.example.findu.data.mapper.todomain.toDomain
import com.example.findu.data.mapper.torequest.toRequestDto
import com.example.findu.domain.model.CheckEmailData
import com.example.findu.domain.model.GuestLoginData
import com.example.findu.domain.model.LoginData
import com.example.findu.domain.model.LoginInfo
import com.example.findu.domain.model.UserInfo
import com.example.findu.domain.repository.AuthRepository
import retrofit2.Response
import java.io.File
import javax.inject.Inject

class AuthRepositoryImpl @Inject constructor(
private val authRemoteDataSource: AuthRemoteDataSource
) : AuthRepository {
override suspend fun postLogin(loginInfo: LoginInfo): Result<LoginData> =
runCatching {
authRemoteDataSource.postLogin(loginRequestDto = loginInfo.toRequestDto()).handleBaseResponse().getOrThrow().toDomain()
authRemoteDataSource.postLogin(loginRequestDto = loginInfo.toRequestDto()).handleBaseResponse().getOrThrow()
?.toDomain() ?: error("Login data is null")
}

override suspend fun postGuestLogin(deviceId:String): Result<GuestLoginData> =
override suspend fun postGuestLogin(deviceId: String): Result<GuestLoginData> =
runCatching {
authRemoteDataSource.postGuestLogin(guestLoginRequestDto = deviceId.toRequestDto()).handleBaseResponse().getOrThrow().toDomain()
authRemoteDataSource.postGuestLogin(guestLoginRequestDto = deviceId.toRequestDto()).handleBaseResponse()
.getOrThrow()
?.toDomain() ?: error("Login data is null")
}

override suspend fun postCheckEmail(email: String): Result<CheckEmailData> =
override suspend fun postCheckNickname(nickname: String): Result<Boolean> =
runCatching {
authRemoteDataSource.postCheckEmail(email).handleBaseResponse().getOrThrow().toDomain()
authRemoteDataSource.postCheckNickname(nickname = nickname).handleBaseResponse().getOrThrow().isDuplicate
}

override suspend fun postSignup(
email: String,
password: String,
nickname: String
): Result<String> =
runCatching {
val response: Response<Unit> =
authRemoteDataSource.postSignup(email, password, nickname)

if (response.isSuccessful) {
val accessToken = response.headers()["Authorization"]?.removePrefix("Bearer ")
if (!accessToken.isNullOrEmpty()) {
return@runCatching accessToken
} else {
throw Exception("Access Token이 응답 헤더에 없음")
}
} else {
throw Exception("회원가입 실패: ${response.code()}")
}
}
profileImageFile: File?,
defaultImageName: String?,
nickname: String,
kakaoId: Long,
deviceId: String
): Result<UserInfo> = runCatching {
authRemoteDataSource.postSignup(
profileImageFile = profileImageFile,
defaultImageName = defaultImageName,
nickname = nickname,
kakaoId = kakaoId,
deviceId = deviceId
).handleBaseResponse().getOrThrow()?.toDomain() ?: error("Signup data is null")
}
}
Loading