Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TNT-114] 회원가입 데이터 취합 및 API 연동 #54

Merged
merged 14 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
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
12 changes: 10 additions & 2 deletions core/navigation/src/main/java/co/kr/tnt/navigation/RouteModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,18 @@ sealed interface Route {
data object Login : Route

@Serializable
data object TrainerSignUp : Route
data class TrainerSignUp(
val authId: String,
val authType: String,
val email: String,
) : Route

@Serializable
data object TraineeSignUp : Route
data class TraineeSignUp(
val authId: String,
val authType: String,
val email: String,
) : Route

@Serializable
data class TrainerConnect(val isFromMyPage: Boolean) : Route
Expand Down
34 changes: 34 additions & 0 deletions core/ui/src/main/java/co/kr/tnt/ui/util/FileUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package co.kr.tnt.ui.util

import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.provider.MediaStore
import android.util.Log
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import java.io.File

fun Uri.toMultiPart(context: Context, keyName: String = "image"): MultipartBody.Part? {
return getRealPathFromUri(this, context)?.let { filePath ->
val file = File(filePath)
val requestFile = file.asRequestBody("image/*".toMediaTypeOrNull())
MultipartBody.Part.createFormData(keyName, file.name, requestFile)
} ?: run {
Log.e("toMultipartImagePart", "Error creating multipart image for URI: $this")
null
}
}

fun getRealPathFromUri(uri: Uri, context: Context): String? {
val projection = arrayOf(MediaStore.Images.Media.DATA)
val cursor: Cursor? = context.contentResolver.query(uri, projection, null, null, null)
cursor?.use {
if (it.moveToFirst()) {
val columnIndex = it.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
return it.getString(columnIndex)
}
}
return null
}
2 changes: 2 additions & 0 deletions core/ui/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
<string name="close">닫기</string>
<string name="ok">확인</string>

<string name="error_server_request_failed">서버 요청에 실패했어요</string>

<string name="trainee">트레이니</string>
<string name="trainer">트레이너</string>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package co.kr.data.network.model

import co.kr.tnt.domain.model.UserType
import kotlinx.serialization.Serializable

@Serializable
data class SignUpRequest(
val socialType: String,
val socialId: String,
val fcmToken: String,
val serviceAgreement: Boolean,
val collectionAgreement: Boolean,
val advertisementAgreement: Boolean,
val socialEmail: String,
val memberType: String,
val name: String,
val birthday: String? = null,
val height: Double? = null,
val weight: Double? = null,
val goalContents: List<String>? = null,
val cautionNote: String? = "",
)

object SignUpRequestMapper {
Copy link
Member

Choose a reason for hiding this comment

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

LoginResponse, SignupResponse 에서 매핑할 때와 같이 통일성 맞춰주는게 좋을 것 같습니다..!

fun fromUserType(
userType: UserType,
socialId: String,
socialType: String,
email: String,
fcmToken: String,
): SignUpRequest {
return when (userType) {
is UserType.Trainer -> SignUpRequest(
memberType = "trainer",
name = userType.name,
birthday = null,
height = null,
weight = null,
goalContents = null,
cautionNote = null,
socialType = socialType,
socialId = socialId,
socialEmail = email,
fcmToken = fcmToken,
serviceAgreement = true,
collectionAgreement = true,
advertisementAgreement = true,
)

is UserType.Trainee -> SignUpRequest(
memberType = "trainee",
name = userType.name,
birthday = userType.birthday?.toString(),
height = userType.height.toDouble(),
weight = userType.weight,
goalContents = userType.ptPurpose,
cautionNote = userType.caution?.ifBlank { null },
socialType = socialType,
socialId = socialId,
socialEmail = email,
fcmToken = fcmToken,
serviceAgreement = true,
collectionAgreement = true,
advertisementAgreement = true,
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package co.kr.data.network.model

import co.kr.tnt.domain.model.SignUpResult
import kotlinx.serialization.Serializable

@Serializable
data class SignUpResponse(
val memberType: String,
val sessionId: String,
val name: String,
val profileImageUrl: String,
)

fun SignUpResponse.toDomain(): SignUpResult {
return SignUpResult(
memberType = memberType,
sessionId = sessionId,
name = name,
profileImageUrl = profileImageUrl,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,24 @@ package co.kr.data.network.service

import co.kr.data.network.model.LoginRequest
import co.kr.data.network.model.LoginResponse
import co.kr.data.network.model.SignUpResponse
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.http.Body
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part

interface ApiService {
@POST("/login")
suspend fun postLogin(
@Body request: LoginRequest,
): LoginResponse

@Multipart
@POST("/members/sign-up")
suspend fun postSignUp(
@Part profileImage: MultipartBody.Part?,
@Part("request") request: RequestBody,
Copy link
Member

Choose a reason for hiding this comment

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

이거 @Body 로는 불가능한가요?

Copy link
Contributor Author

@SeonJeongk SeonJeongk Jan 29, 2025

Choose a reason for hiding this comment

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

multipart/form-data요청에서는 @Body 대신 @Part 또는@PartMap을 사용해야 한다고 해서 @Part로 구현했습니다!

@Body로 수정해서 호출해보면 아래와 같은 오류가 뜹니다

java.lang.IllegalArgumentException: @Body parameters cannot be used with form or multi-part encoding. (parameter #2)

): SignUpResponse
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package co.kr.data.network.source

import co.kr.data.network.model.SignUpResponse
import co.kr.data.network.service.ApiService
import co.kr.data.network.util.networkHandler
import okhttp3.MultipartBody
import okhttp3.RequestBody
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class SignUpRemoteDataSource @Inject constructor(
private val apiService: ApiService,
) {
suspend fun postSignUp(
profileImage: MultipartBody.Part?,
request: RequestBody,
): SignUpResponse = networkHandler {
apiService.postSignUp(profileImage, request)
}
}
2 changes: 2 additions & 0 deletions data/repository/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ dependencies {
implementation(projects.domain)
implementation(projects.data.network)
implementation(projects.data.storage)
implementation(libs.okhttp.logging)
implementation(libs.kotlinx.serialization.json)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package co.kr.data.repository

import co.kr.data.network.model.SignUpRequest
import co.kr.data.network.model.SignUpRequestMapper
import co.kr.data.network.model.toDomain
import co.kr.data.network.source.SignUpRemoteDataSource
import co.kr.data.storage.source.SessionLocalDataSource
import co.kr.tnt.domain.model.SignUpResult
import co.kr.tnt.domain.model.UserType
import co.kr.tnt.domain.repository.SignUpRepository
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import javax.inject.Inject

class SignUpRepositoryImpl @Inject constructor(
private val signupRemoteDataSource: SignUpRemoteDataSource,
private val sessionLocalDataSource: SessionLocalDataSource,
) : SignUpRepository {
override suspend fun signUp(
profileImage: MultipartBody.Part?,
userType: UserType,
socialId: String,
socialType: String,
email: String,
): SignUpResult {
// TODO FCM token
val signUpRequest = SignUpRequestMapper.fromUserType(
userType = userType,
socialId = socialId,
socialType = socialType,
email = email,
fcmToken = "EMPTY",
)
val requestBody = prepareJsonRequestBody(signUpRequest)

val response = signupRemoteDataSource.postSignUp(
profileImage = profileImage,
request = requestBody,
)

response.sessionId.let { sessionId ->
sessionLocalDataSource.updateSessionId(sessionId)
}
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
response.sessionId.let { sessionId ->
sessionLocalDataSource.updateSessionId(sessionId)
}
sessionLocalDataSource.updateSessionId(response.sessionId)


return response.toDomain()
}

private fun prepareJsonRequestBody(signUpRequest: SignUpRequest): RequestBody {
val jsonString = Json.encodeToString(signUpRequest)
Copy link
Member

Choose a reason for hiding this comment

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

DI Container 에 Json 인스턴스가 등록되어 있어서, 새롭게 초기화하지 않아도 됩니다!

return jsonString.toRequestBody("application/json".toMediaTypeOrNull())
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package co.kr.data.repository.di

import co.kr.data.repository.LoginRepositoryImpl
import co.kr.data.repository.SignUpRepositoryImpl
import co.kr.tnt.domain.repository.LoginRepository
import co.kr.tnt.domain.repository.SignUpRepository
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
Expand All @@ -14,4 +16,9 @@ internal abstract class RepositoryModule {
abstract fun bindsLoginRepository(
repository: LoginRepositoryImpl,
): LoginRepository

@Binds
abstract fun bindsSignUpRepository(
repository: SignUpRepositoryImpl,
): SignUpRepository
}
1 change: 1 addition & 0 deletions domain/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ plugins {

dependencies {
implementation(libs.inject)
implementation(libs.okhttp.logging)
Copy link
Member

@hoyahozz hoyahozz Jan 28, 2025

Choose a reason for hiding this comment

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

도메인 영역에서 네트워크 관련 라이브러리를 알고 있어선 안됩니다!!

왜일까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

클린 아키텍처 원칙에 따라 도메인 계층은 네트워크와 무관해야 한다는건 알고 있지만 Multipart 때문에 넣었습니다..😿

지금 생각해보니 아래 리뷰처럼 File로 넘기고 SignUpRepositoryImpl에서 MultipartBody.Part로 변환해도 될 것 같네요!

심약 이슈였습니다.. 고쳐올게요!

implementation(libs.coroutines.core)
}
8 changes: 8 additions & 0 deletions domain/src/main/java/co/kr/tnt/domain/model/SignUpResult.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package co.kr.tnt.domain.model

data class SignUpResult(
val memberType: String,
val sessionId: String,
val name: String,
val profileImageUrl: String,
)
9 changes: 6 additions & 3 deletions domain/src/main/java/co/kr/tnt/domain/model/UserType.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package co.kr.tnt.domain.model

import java.time.LocalDate

sealed class UserType {
abstract val id: String
abstract val name: String
Expand All @@ -15,10 +17,11 @@ sealed class UserType {
override val id: String = "",
override val name: String = "",
override val image: String? = null,
val age: Int = 0,
val weight: Float = 0f,
val birthday: LocalDate? = null,
val age: Int? = 0,
val weight: Double = 0.0,
val height: Int = 0,
val ptPurpose: List<String> = emptyList(),
val caution: String? = null,
val caution: String? = "",
) : UserType()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package co.kr.tnt.domain.repository

import co.kr.tnt.domain.model.SignUpResult
import co.kr.tnt.domain.model.UserType
import okhttp3.MultipartBody

interface SignUpRepository {
suspend fun signUp(
profileImage: MultipartBody.Part?,
Copy link
Member

Choose a reason for hiding this comment

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

File 형태로 알고 있어도 되지 않을까요?

userType: UserType,
socialId: String,
socialType: String,
email: String,
): SignUpResult
}
19 changes: 18 additions & 1 deletion feature/main/src/main/java/co/kr/tnt/main/ui/TnTNavHost.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ import co.kr.tnt.roleselect.navigateToRoleSelection
import co.kr.tnt.roleselect.roleSelectionScreen
import co.kr.tnt.trainee.connect.navigation.navigateToTraineeConnect
import co.kr.tnt.trainee.connect.navigation.traineeConnectScreen
import co.kr.tnt.trainee.signup.navigation.navigateToTraineeSignUp
import co.kr.tnt.trainee.signup.navigation.traineeSignUpScreen
import co.kr.tnt.trainer.connect.navigation.navigateToTrainerConnect
import co.kr.tnt.trainer.connect.navigation.trainerConnectScreen
import co.kr.tnt.trainer.signup.navigation.navigateToTrainerSignUp
import co.kr.tnt.trainer.signup.navigation.trainerSignUpScreen

@Composable
Expand Down Expand Up @@ -50,6 +52,22 @@ fun TnTNavHost(
)
},
)
roleSelectionScreen(
navigateToTraineeSignUp = { authId, authType, email ->
navController.navigateToTraineeSignUp(
authId = authId,
authType = authType,
email = email,
)
},
navigateToTrainerSignUp = { authId, authType, email ->
navController.navigateToTrainerSignUp(
authId = authId,
authType = authType,
email = email,
)
},
)
trainerSignUpScreen(
navigateToPrevious = { navController.popBackStack() },
navigateToConnect = { navController.navigateToTrainerConnect(isFromMyPage = false) },
Expand All @@ -70,7 +88,6 @@ fun TnTNavHost(
navController.navigateToHome(isTrainer = false, clearBackStack = true)
},
)
roleSelectionScreen()
homeNavGraph()
}
}
Expand Down
Loading
Loading