diff --git a/core/navigation/src/main/java/co/kr/tnt/navigation/RouteModel.kt b/core/navigation/src/main/java/co/kr/tnt/navigation/RouteModel.kt
index 2ca83df..4869e61 100644
--- a/core/navigation/src/main/java/co/kr/tnt/navigation/RouteModel.kt
+++ b/core/navigation/src/main/java/co/kr/tnt/navigation/RouteModel.kt
@@ -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
diff --git a/core/ui/src/main/java/co/kr/tnt/ui/util/FileUtils.kt b/core/ui/src/main/java/co/kr/tnt/ui/util/FileUtils.kt
new file mode 100644
index 0000000..3fdab8b
--- /dev/null
+++ b/core/ui/src/main/java/co/kr/tnt/ui/util/FileUtils.kt
@@ -0,0 +1,29 @@
+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 java.io.File
+
+fun Uri.toFile(context: Context): File? {
+ return getRealPathFromUri(this, context)?.let { filePath ->
+ File(filePath)
+ } ?: run {
+ Log.e("toFile", "Error creating file 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
+}
diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml
index 2211686..318e8d3 100644
--- a/core/ui/src/main/res/values/strings.xml
+++ b/core/ui/src/main/res/values/strings.xml
@@ -16,6 +16,8 @@
닫기
확인
+ 서버 요청에 실패했어요
+
트레이니
트레이너
diff --git a/data/network/src/main/java/co/kr/data/network/model/SignUpRequest.kt b/data/network/src/main/java/co/kr/data/network/model/SignUpRequest.kt
new file mode 100644
index 0000000..030a9f1
--- /dev/null
+++ b/data/network/src/main/java/co/kr/data/network/model/SignUpRequest.kt
@@ -0,0 +1,65 @@
+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? = null,
+ val cautionNote: String? = "",
+)
+
+fun UserType.toSignUpRequest(
+ socialId: String,
+ socialType: String,
+ email: String,
+ fcmToken: String,
+): SignUpRequest {
+ return when (this) {
+ is UserType.Trainer -> SignUpRequest(
+ memberType = "trainer",
+ name = 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 = name,
+ birthday = birthday?.toString(),
+ height = height.toDouble(),
+ weight = weight,
+ goalContents = ptPurpose,
+ cautionNote = caution?.ifBlank { null },
+ socialType = socialType,
+ socialId = socialId,
+ socialEmail = email,
+ fcmToken = fcmToken,
+ serviceAgreement = true,
+ collectionAgreement = true,
+ advertisementAgreement = true,
+ )
+ }
+}
diff --git a/data/network/src/main/java/co/kr/data/network/model/SignUpResponse.kt b/data/network/src/main/java/co/kr/data/network/model/SignUpResponse.kt
new file mode 100644
index 0000000..77de8c8
--- /dev/null
+++ b/data/network/src/main/java/co/kr/data/network/model/SignUpResponse.kt
@@ -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,
+ )
+}
diff --git a/data/network/src/main/java/co/kr/data/network/service/ApiService.kt b/data/network/src/main/java/co/kr/data/network/service/ApiService.kt
index a3be07a..bb5412c 100644
--- a/data/network/src/main/java/co/kr/data/network/service/ApiService.kt
+++ b/data/network/src/main/java/co/kr/data/network/service/ApiService.kt
@@ -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,
+ ): SignUpResponse
}
diff --git a/data/network/src/main/java/co/kr/data/network/source/SignUpRemoteDataSource.kt b/data/network/src/main/java/co/kr/data/network/source/SignUpRemoteDataSource.kt
new file mode 100644
index 0000000..cb59d42
--- /dev/null
+++ b/data/network/src/main/java/co/kr/data/network/source/SignUpRemoteDataSource.kt
@@ -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)
+ }
+}
diff --git a/data/repository/build.gradle.kts b/data/repository/build.gradle.kts
index de82399..af8f0eb 100644
--- a/data/repository/build.gradle.kts
+++ b/data/repository/build.gradle.kts
@@ -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)
}
diff --git a/data/repository/src/main/java/co/kr/data/repository/SignUpRepositoryImpl.kt b/data/repository/src/main/java/co/kr/data/repository/SignUpRepositoryImpl.kt
new file mode 100644
index 0000000..f026958
--- /dev/null
+++ b/data/repository/src/main/java/co/kr/data/repository/SignUpRepositoryImpl.kt
@@ -0,0 +1,61 @@
+package co.kr.data.repository
+
+import co.kr.data.network.model.SignUpRequest
+import co.kr.data.network.model.toDomain
+import co.kr.data.network.model.toSignUpRequest
+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.asRequestBody
+import okhttp3.RequestBody.Companion.toRequestBody
+import java.io.File
+import javax.inject.Inject
+
+class SignUpRepositoryImpl @Inject constructor(
+ private val signupRemoteDataSource: SignUpRemoteDataSource,
+ private val sessionLocalDataSource: SessionLocalDataSource,
+ private val json: Json,
+) : SignUpRepository {
+ override suspend fun signUp(
+ profileImage: File?,
+ userType: UserType,
+ socialId: String,
+ socialType: String,
+ email: String,
+ ): SignUpResult {
+ val profileImagePart = profileImage?.let {
+ val requestFile = it.asRequestBody("image/*".toMediaTypeOrNull())
+ MultipartBody.Part.createFormData("profileImage", it.name, requestFile)
+ }
+
+ // TODO FCM token
+ val signUpRequest = userType.toSignUpRequest(
+ socialId = socialId,
+ socialType = socialType,
+ email = email,
+ fcmToken = "EMPTY",
+ )
+ val requestBody = signUpRequest.toRequestBody(Json)
+
+ val response = signupRemoteDataSource.postSignUp(
+ profileImage = profileImagePart,
+ request = requestBody,
+ )
+
+ sessionLocalDataSource.updateSessionId(response.sessionId)
+
+ return response.toDomain()
+ }
+
+ private fun SignUpRequest.toRequestBody(json: Json): RequestBody {
+ val jsonString = json.encodeToString(this)
+ return jsonString.toRequestBody("application/json".toMediaTypeOrNull())
+ }
+}
diff --git a/data/repository/src/main/java/co/kr/data/repository/di/RepositoryModule.kt b/data/repository/src/main/java/co/kr/data/repository/di/RepositoryModule.kt
index a41cb99..d510ece 100644
--- a/data/repository/src/main/java/co/kr/data/repository/di/RepositoryModule.kt
+++ b/data/repository/src/main/java/co/kr/data/repository/di/RepositoryModule.kt
@@ -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
@@ -14,4 +16,9 @@ internal abstract class RepositoryModule {
abstract fun bindsLoginRepository(
repository: LoginRepositoryImpl,
): LoginRepository
+
+ @Binds
+ abstract fun bindsSignUpRepository(
+ repository: SignUpRepositoryImpl,
+ ): SignUpRepository
}
diff --git a/domain/src/main/java/co/kr/tnt/domain/model/SignUpResult.kt b/domain/src/main/java/co/kr/tnt/domain/model/SignUpResult.kt
new file mode 100644
index 0000000..4019e29
--- /dev/null
+++ b/domain/src/main/java/co/kr/tnt/domain/model/SignUpResult.kt
@@ -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,
+)
diff --git a/domain/src/main/java/co/kr/tnt/domain/model/UserType.kt b/domain/src/main/java/co/kr/tnt/domain/model/UserType.kt
index c4a2731..989a556 100644
--- a/domain/src/main/java/co/kr/tnt/domain/model/UserType.kt
+++ b/domain/src/main/java/co/kr/tnt/domain/model/UserType.kt
@@ -1,24 +1,27 @@
package co.kr.tnt.domain.model
+import java.time.LocalDate
+
sealed class UserType {
abstract val id: String
abstract val name: String
abstract val image: String?
data class Trainer(
- override val id: String = "",
- override val name: String = "",
- override val image: String? = null,
+ override val id: String,
+ override val name: String,
+ override val image: String?,
) : UserType()
data class Trainee(
- override val id: String = "",
- override val name: String = "",
- override val image: String? = null,
- val age: Int = 0,
- val weight: Float = 0f,
- val height: Int = 0,
- val ptPurpose: List = emptyList(),
- val caution: String? = null,
+ override val id: String,
+ override val name: String,
+ override val image: String?,
+ val birthday: LocalDate?,
+ val age: Int?,
+ val weight: Double,
+ val height: Int,
+ val ptPurpose: List,
+ val caution: String?,
) : UserType()
}
diff --git a/domain/src/main/java/co/kr/tnt/domain/repository/SignUpRepository.kt b/domain/src/main/java/co/kr/tnt/domain/repository/SignUpRepository.kt
new file mode 100644
index 0000000..07494d9
--- /dev/null
+++ b/domain/src/main/java/co/kr/tnt/domain/repository/SignUpRepository.kt
@@ -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 java.io.File
+
+interface SignUpRepository {
+ suspend fun signUp(
+ profileImage: File?,
+ userType: UserType,
+ socialId: String,
+ socialType: String,
+ email: String,
+ ): SignUpResult
+}
diff --git a/feature/main/src/main/java/co/kr/tnt/main/ui/TnTNavHost.kt b/feature/main/src/main/java/co/kr/tnt/main/ui/TnTNavHost.kt
index 198f551..9dc688a 100644
--- a/feature/main/src/main/java/co/kr/tnt/main/ui/TnTNavHost.kt
+++ b/feature/main/src/main/java/co/kr/tnt/main/ui/TnTNavHost.kt
@@ -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
@@ -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) },
@@ -70,7 +88,6 @@ fun TnTNavHost(
navController.navigateToHome(isTrainer = false, clearBackStack = true)
},
)
- roleSelectionScreen()
homeNavGraph()
}
}
diff --git a/feature/roleselect/src/main/java/co/kr/tnt/roleselect/RoleSelectionContract.kt b/feature/roleselect/src/main/java/co/kr/tnt/roleselect/RoleSelectionContract.kt
new file mode 100644
index 0000000..329935c
--- /dev/null
+++ b/feature/roleselect/src/main/java/co/kr/tnt/roleselect/RoleSelectionContract.kt
@@ -0,0 +1,21 @@
+package co.kr.tnt.roleselect
+
+import co.kr.tnt.roleselect.model.RoleState
+import co.kr.tnt.ui.base.UiEvent
+import co.kr.tnt.ui.base.UiSideEffect
+import co.kr.tnt.ui.base.UiState
+
+internal class RoleSelectionContract {
+ data class RoleSelectionUiState(
+ val userType: RoleState = RoleState.Trainer,
+ ) : UiState
+
+ sealed interface RoleSelectionUiEvent : UiEvent {
+ data class OnNextClick(val role: RoleState) : RoleSelectionUiEvent
+ }
+
+ sealed interface RoleSelectionEffect : UiSideEffect {
+ data object NavigateToTrainerSignUp : RoleSelectionEffect
+ data object NavigateToTraineeSignUp : RoleSelectionEffect
+ }
+}
diff --git a/feature/roleselect/src/main/java/co/kr/tnt/roleselect/RoleSelectionNavigation.kt b/feature/roleselect/src/main/java/co/kr/tnt/roleselect/RoleSelectionNavigation.kt
index 15bfeb5..192c586 100644
--- a/feature/roleselect/src/main/java/co/kr/tnt/roleselect/RoleSelectionNavigation.kt
+++ b/feature/roleselect/src/main/java/co/kr/tnt/roleselect/RoleSelectionNavigation.kt
@@ -21,13 +21,15 @@ fun NavController.navigateToRoleSelection(
builder = navOptions,
)
-fun NavGraphBuilder.roleSelectionScreen() {
+fun NavGraphBuilder.roleSelectionScreen(
+ navigateToTraineeSignUp: (authId: String, authType: String, email: String) -> Unit,
+ navigateToTrainerSignUp: (authId: String, authType: String, email: String) -> Unit,
+) {
composable { navBackstackEntry ->
navBackstackEntry.toRoute().apply {
- RoleSelectionScreen(
- authId = authId,
- authType = authType,
- email = email,
+ RoleSelectionRoute(
+ navigateToTraineeSignUp = { navigateToTraineeSignUp(authId, authType, email) },
+ navigateToTrainerSignUp = { navigateToTrainerSignUp(authId, authType, email) },
)
}
}
diff --git a/feature/roleselect/src/main/java/co/kr/tnt/roleselect/RoleSelectionScreen.kt b/feature/roleselect/src/main/java/co/kr/tnt/roleselect/RoleSelectionScreen.kt
index 1b09876..1225319 100644
--- a/feature/roleselect/src/main/java/co/kr/tnt/roleselect/RoleSelectionScreen.kt
+++ b/feature/roleselect/src/main/java/co/kr/tnt/roleselect/RoleSelectionScreen.kt
@@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -21,29 +22,54 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
import co.kr.tnt.designsystem.component.button.TnTBottomButton
import co.kr.tnt.designsystem.component.button.TnTTextButton
import co.kr.tnt.designsystem.component.button.model.ButtonSize
import co.kr.tnt.designsystem.component.button.model.ButtonType
import co.kr.tnt.designsystem.theme.TnTTheme
-import co.kr.tnt.domain.model.AuthType
import co.kr.tnt.domain.model.UserType
import co.kr.tnt.feature.roleselect.R
+import co.kr.tnt.roleselect.RoleSelectionContract.RoleSelectionEffect
+import co.kr.tnt.roleselect.RoleSelectionContract.RoleSelectionUiEvent
import co.kr.tnt.roleselect.model.RoleState
import co.kr.tnt.core.ui.R as uiResource
@Composable
-@Suppress("UnusedParameter")
+internal fun RoleSelectionRoute(
+ viewModel: RoleSelectionViewModel = hiltViewModel(),
+ navigateToTraineeSignUp: () -> Unit,
+ navigateToTrainerSignUp: () -> Unit,
+) {
+ RoleSelectionScreen(
+ onNextClick = { viewModel.setEvent(RoleSelectionUiEvent.OnNextClick(it)) },
+ )
+
+ LaunchedEffect(viewModel.effect) {
+ viewModel.effect.collect { effect ->
+ when (effect) {
+ RoleSelectionEffect.NavigateToTraineeSignUp -> navigateToTraineeSignUp()
+ RoleSelectionEffect.NavigateToTrainerSignUp -> navigateToTrainerSignUp()
+ }
+ }
+ }
+}
+
+@Composable
fun RoleSelectionScreen(
- onRoleSelected: (UserType) -> Unit = {},
- onNextClick: () -> Unit = {},
- authId: String,
- authType: String,
- email: String,
- modifier: Modifier = Modifier,
+ onNextClick: (RoleState) -> Unit = {},
) {
- var selectedRole by remember { mutableStateOf(RoleState.fromDomain(UserType.Trainer())) }
- val authType = AuthType.from(authType)
+ var selectedRole by remember {
+ mutableStateOf(
+ RoleState.fromDomain(
+ UserType.Trainer(
+ id = "",
+ name = "",
+ image = "TODO()",
+ ),
+ ),
+ )
+ }
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Column(
@@ -71,7 +97,6 @@ fun RoleSelectionScreen(
contentDescription = null,
modifier = Modifier.align(Alignment.CenterHorizontally),
)
- // TODO 선택한 버튼 정보 저장
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
@@ -85,7 +110,6 @@ fun RoleSelectionScreen(
type = if (selectedRole == RoleState.Trainer) ButtonType.RedOutline else ButtonType.GrayOutline,
onClick = {
selectedRole = RoleState.Trainer
- onRoleSelected(UserType.Trainer("", ""))
},
)
TnTTextButton(
@@ -95,14 +119,13 @@ fun RoleSelectionScreen(
type = if (selectedRole == RoleState.Trainee) ButtonType.RedOutline else ButtonType.GrayOutline,
onClick = {
selectedRole = RoleState.Trainee
- onRoleSelected(UserType.Trainer("", ""))
},
)
}
TnTBottomButton(
text = stringResource(uiResource.string.next),
enabled = true,
- onClick = { onNextClick() },
+ onClick = { onNextClick(selectedRole) },
)
}
}
@@ -113,12 +136,7 @@ fun RoleSelectionScreen(
private fun RoleScreenPreview() {
TnTTheme {
RoleSelectionScreen(
- onRoleSelected = {},
onNextClick = {},
- authId = "",
- authType = "",
- email = "",
- modifier = Modifier.fillMaxSize(),
)
}
}
diff --git a/feature/roleselect/src/main/java/co/kr/tnt/roleselect/RoleSelectionViewModel.kt b/feature/roleselect/src/main/java/co/kr/tnt/roleselect/RoleSelectionViewModel.kt
new file mode 100644
index 0000000..a105033
--- /dev/null
+++ b/feature/roleselect/src/main/java/co/kr/tnt/roleselect/RoleSelectionViewModel.kt
@@ -0,0 +1,33 @@
+package co.kr.tnt.roleselect
+
+import co.kr.tnt.roleselect.RoleSelectionContract.RoleSelectionEffect
+import co.kr.tnt.roleselect.RoleSelectionContract.RoleSelectionUiEvent
+import co.kr.tnt.roleselect.RoleSelectionContract.RoleSelectionUiState
+import co.kr.tnt.roleselect.model.RoleState
+import co.kr.tnt.ui.base.BaseViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+
+@HiltViewModel
+internal class RoleSelectionViewModel @Inject constructor() :
+ BaseViewModel(
+ RoleSelectionUiState(),
+ ) {
+ override suspend fun handleEvent(event: RoleSelectionUiEvent) {
+ when (event) {
+ is RoleSelectionUiEvent.OnNextClick -> navigateToSignUp(event.role)
+ }
+ }
+
+ private fun navigateToSignUp(role: RoleState) {
+ when (role) {
+ is RoleState.Trainer -> {
+ sendEffect(RoleSelectionEffect.NavigateToTrainerSignUp)
+ }
+
+ is RoleState.Trainee -> {
+ sendEffect(RoleSelectionEffect.NavigateToTraineeSignUp)
+ }
+ }
+ }
+ }
diff --git a/feature/roleselect/src/main/java/co/kr/tnt/roleselect/model/RoleState.kt b/feature/roleselect/src/main/java/co/kr/tnt/roleselect/model/RoleState.kt
index 3f652a3..6b8ce9b 100644
--- a/feature/roleselect/src/main/java/co/kr/tnt/roleselect/model/RoleState.kt
+++ b/feature/roleselect/src/main/java/co/kr/tnt/roleselect/model/RoleState.kt
@@ -23,8 +23,8 @@ sealed class RoleState(
companion object {
fun fromDomain(role: UserType): RoleState {
return when (role) {
- is UserType.Trainee -> Trainer
- is UserType.Trainer -> Trainee
+ is UserType.Trainer -> Trainer
+ is UserType.Trainee -> Trainee
}
}
}
diff --git a/feature/trainee/connect/src/main/java/co/kr/tnt/trainee/connect/CodeEntryPage.kt b/feature/trainee/connect/src/main/java/co/kr/tnt/trainee/connect/CodeEntryPage.kt
index d0d0067..d2b3277 100644
--- a/feature/trainee/connect/src/main/java/co/kr/tnt/trainee/connect/CodeEntryPage.kt
+++ b/feature/trainee/connect/src/main/java/co/kr/tnt/trainee/connect/CodeEntryPage.kt
@@ -113,9 +113,9 @@ private fun CodeEntryPagePreview() {
isFromMyPage = false,
onSkipClick = {},
onNextClick = {},
- state = TODO(),
+ state = TraineeConnectUiState(),
onValidateClick = {},
- onCodeChanged = TODO(),
+ onCodeChanged = {},
onBackClick = {},
)
}
diff --git a/feature/trainee/connect/src/main/java/co/kr/tnt/trainee/connect/PTSessionFormPage.kt b/feature/trainee/connect/src/main/java/co/kr/tnt/trainee/connect/PTSessionFormPage.kt
index c34426c..70efa05 100644
--- a/feature/trainee/connect/src/main/java/co/kr/tnt/trainee/connect/PTSessionFormPage.kt
+++ b/feature/trainee/connect/src/main/java/co/kr/tnt/trainee/connect/PTSessionFormPage.kt
@@ -267,7 +267,7 @@ private fun PTSessionFormPagePreview() {
PTSessionFormPage(
onNextClick = {},
onBackClick = {},
- state = TODO(),
+ state = TraineeConnectUiState(),
)
}
}
diff --git a/feature/trainee/connect/src/main/java/co/kr/tnt/trainee/connect/TraineeConnectCompletePage.kt b/feature/trainee/connect/src/main/java/co/kr/tnt/trainee/connect/TraineeConnectCompletePage.kt
index 636a54d..a196055 100644
--- a/feature/trainee/connect/src/main/java/co/kr/tnt/trainee/connect/TraineeConnectCompletePage.kt
+++ b/feature/trainee/connect/src/main/java/co/kr/tnt/trainee/connect/TraineeConnectCompletePage.kt
@@ -132,7 +132,7 @@ private fun ProfileSection(
private fun TraineeConnectCompletePagePreview() {
TnTTheme {
TraineeConnectCompletePage(
- state = TODO(),
+ state = TraineeConnectUiState(),
onNextClick = {},
onBackClick = {},
)
diff --git a/feature/trainee/connect/src/main/java/co/kr/tnt/trainee/connect/TraineeConnectContract.kt b/feature/trainee/connect/src/main/java/co/kr/tnt/trainee/connect/TraineeConnectContract.kt
index f22ea23..6e0e724 100644
--- a/feature/trainee/connect/src/main/java/co/kr/tnt/trainee/connect/TraineeConnectContract.kt
+++ b/feature/trainee/connect/src/main/java/co/kr/tnt/trainee/connect/TraineeConnectContract.kt
@@ -16,8 +16,22 @@ internal class TraineeConnectContract {
val completedSession: Int = 0,
val totalSession: Int = 0,
val selectedStartDate: LocalDate = LocalDate.now(),
- val trainerState: UserType.Trainer = UserType.Trainer(),
- val traineeState: UserType.Trainee = UserType.Trainee(),
+ val trainerState: UserType.Trainer = UserType.Trainer(
+ id = "",
+ name = "",
+ image = null,
+ ),
+ val traineeState: UserType.Trainee = UserType.Trainee(
+ id = "",
+ name = "",
+ image = null,
+ birthday = null,
+ age = 0,
+ weight = 0.0,
+ height = 0,
+ ptPurpose = emptyList(),
+ caution = null,
+ ),
) : UiState
sealed interface TraineeConnectUiEvent : UiEvent {
diff --git a/feature/trainee/connect/src/main/java/co/kr/tnt/trainee/connect/TraineeConnectViewModel.kt b/feature/trainee/connect/src/main/java/co/kr/tnt/trainee/connect/TraineeConnectViewModel.kt
index 9ac73af..5cfba12 100644
--- a/feature/trainee/connect/src/main/java/co/kr/tnt/trainee/connect/TraineeConnectViewModel.kt
+++ b/feature/trainee/connect/src/main/java/co/kr/tnt/trainee/connect/TraineeConnectViewModel.kt
@@ -47,8 +47,9 @@ internal class TraineeConnectViewModel @Inject constructor() :
id = "trainee",
name = "김회원",
image = "https://buly.kr/3j7VVqN",
+ birthday = null,
age = 25,
- weight = 100F,
+ weight = 100.0,
height = 165,
ptPurpose = listOf("체중 감량", "자세 교정"),
caution = "발목이 안좋아서 발목에 무리가는 행동을 하면 안돼요. 잘 부탁드려요!",
diff --git a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeBasicInfoPage.kt b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeBasicInfoPage.kt
index 0144da7..c536da0 100644
--- a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeBasicInfoPage.kt
+++ b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeBasicInfoPage.kt
@@ -18,11 +18,6 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@@ -36,32 +31,25 @@ import co.kr.tnt.designsystem.component.TnTTopBarWithBackButton
import co.kr.tnt.designsystem.component.button.TnTBottomButton
import co.kr.tnt.designsystem.theme.TnTTheme
import co.kr.tnt.feature.trainee.signup.R
+import co.kr.tnt.trainee.signup.TraineeSignUpContract.TraineeSignUpUiState
import co.kr.tnt.trainee.signup.component.ProgressSteps
import java.time.LocalDate
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import co.kr.tnt.core.ui.R as uiResource
-private const val MAX_HEIGHT_LENGTH = 3
-private const val MAX_WEIGHT_LENGTH = 5
-
@Composable
-fun TraineeBasicInfoPage(
+internal fun TraineeBasicInfoPage(
+ state: TraineeSignUpUiState,
+ onHeightChange: (String) -> Unit,
+ onWeightChange: (String) -> Unit,
+ onBirthdayChange: (LocalDate) -> Unit,
onBackClick: () -> Unit,
onNextClick: () -> Unit,
) {
BackHandler { onBackClick() }
- // TODO 상태 관리 따로 빼기
val today = LocalDate.now()
- var height by remember { mutableStateOf("") }
- var weight by remember { mutableStateOf("") }
- var birthday by remember { mutableStateOf(null) }
-
- val isHeightValid by remember { derivedStateOf { height.isNotEmpty() && validateHeight(height) } }
- val isWeightValid by remember { derivedStateOf { weight.isNotEmpty() && validateWeight(weight) } }
-
- val isFormValid by remember { derivedStateOf { isHeightValid && isWeightValid } }
Scaffold(
topBar = { TnTTopBarWithBackButton(onBackClick = onBackClick) },
@@ -90,8 +78,8 @@ fun TraineeBasicInfoPage(
BirthdayPicker(
modifier = Modifier.padding(horizontal = 20.dp),
today = today,
- selectedDate = birthday,
- onDateSelected = { birthday = it },
+ selectedDate = state.birthday,
+ onDateSelected = onBirthdayChange,
)
HorizontalDivider(
thickness = 1.dp,
@@ -107,32 +95,32 @@ fun TraineeBasicInfoPage(
) {
TnTLabeledTextField(
title = stringResource(uiResource.string.height_label),
- value = height,
+ value = state.height,
placeholder = "0",
isSingleLine = true,
- showWarning = !validateHeight(height),
+ showWarning = state.height.isNotEmpty() && !state.isHeightValid,
warningMessage = stringResource(R.string.entered_wrong_number),
isRequired = true,
keyboardType = KeyboardType.Number,
trailingComponent = {
UnitLabel(uiResource.string.height_unit)
},
- onValueChange = { height = it },
+ onValueChange = onHeightChange,
modifier = Modifier.weight(1f),
)
TnTLabeledTextField(
title = stringResource(uiResource.string.weight_label),
- value = weight,
+ value = state.weight,
placeholder = "00.0",
isSingleLine = true,
- showWarning = !validateWeight(weight),
+ showWarning = state.weight.isNotEmpty() && !state.isWeightValid,
warningMessage = stringResource(R.string.entered_wrong_number),
isRequired = true,
keyboardType = KeyboardType.Number,
trailingComponent = {
UnitLabel(uiResource.string.weight_unit)
},
- onValueChange = { weight = it },
+ onValueChange = onWeightChange,
modifier = Modifier.weight(1f),
)
}
@@ -140,7 +128,7 @@ fun TraineeBasicInfoPage(
TnTBottomButton(
text = stringResource(uiResource.string.next),
modifier = Modifier.align(Alignment.BottomCenter),
- enabled = isFormValid,
+ enabled = state.isBasicInfoValid,
onClick = onNextClick,
)
}
@@ -207,30 +195,17 @@ private fun UnitLabel(stringResId: Int) {
)
}
-/**
- * 키가 유효한 입력값인지 검사
- * 형식: 정수 3자
- */
-private fun validateHeight(input: String): Boolean {
- return input.isEmpty() || input.toIntOrNull() != null && !input.startsWith("0") && input.length <= MAX_HEIGHT_LENGTH
-}
-
-/**
- * 몸무게가 유효한 입력값인지 검사
- * 형식: 5자 이하의 실수 (000, 00, 00.0, 000.0)
- */
-private fun validateWeight(input: String): Boolean {
- val weightRegex = Regex("^(\\d{1,3}(\\.\\d)?)?\$")
- return input.isEmpty() || input.matches(weightRegex) && !input.startsWith("0") && input.length <= MAX_WEIGHT_LENGTH
-}
-
@Preview(showBackground = true)
@Composable
private fun TraineeBasicInfoPagePreview() {
TnTTheme {
TraineeBasicInfoPage(
+ state = TraineeSignUpUiState(),
onBackClick = {},
onNextClick = {},
+ onHeightChange = {},
+ onWeightChange = {},
+ onBirthdayChange = {},
)
}
}
diff --git a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeNoteForTrainerPage.kt b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeNoteForTrainerPage.kt
index 9ae56d2..72cfa3e 100644
--- a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeNoteForTrainerPage.kt
+++ b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeNoteForTrainerPage.kt
@@ -11,10 +11,6 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
@@ -31,15 +27,14 @@ import co.kr.tnt.core.ui.R as uiResource
private const val MAX_LENGTH = 100
@Composable
-fun TraineeNoteForTrainerPage(
+internal fun TraineeNoteForTrainerPage(
+ caution: String?,
+ onCautionChange: (String) -> Unit,
onBackClick: () -> Unit,
onNextClick: () -> Unit,
) {
BackHandler { onBackClick() }
- // TODO 상태 관리 따로 빼기
- var text by remember { mutableStateOf("") }
-
Scaffold(
topBar = { TnTTopBarWithBackButton(onBackClick = onBackClick) },
containerColor = TnTTheme.colors.commonColors.Common0,
@@ -59,13 +54,15 @@ fun TraineeNoteForTrainerPage(
)
Spacer(Modifier.padding(top = 48.dp))
TnTOutlinedTextField(
- value = text,
+ value = caution ?: "",
onValueChange = { newValue ->
if (newValue.length <= MAX_LENGTH) {
- text = newValue
+ onCautionChange(newValue)
}
},
modifier = Modifier.padding(horizontal = 20.dp),
+ isError = (caution?.length ?: 0) == MAX_LENGTH,
+ warningMessage = stringResource(R.string.text_length_overflow),
maxLength = 100,
)
}
@@ -83,8 +80,10 @@ fun TraineeNoteForTrainerPage(
private fun TraineeNoteForTrainerPagePreview() {
TnTTheme {
TraineeNoteForTrainerPage(
+ caution = "",
onBackClick = {},
onNextClick = {},
+ onCautionChange = {},
)
}
}
diff --git a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineePTPurposePage.kt b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineePTPurposePage.kt
index 2c4be6c..e9612cc 100644
--- a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineePTPurposePage.kt
+++ b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineePTPurposePage.kt
@@ -12,10 +12,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
@@ -28,6 +24,7 @@ import co.kr.tnt.designsystem.component.button.model.ButtonSize
import co.kr.tnt.designsystem.component.button.model.ButtonType
import co.kr.tnt.designsystem.theme.TnTTheme
import co.kr.tnt.feature.trainee.signup.R
+import co.kr.tnt.trainee.signup.TraineeSignUpContract.TraineeSignUpUiState
import co.kr.tnt.trainee.signup.component.ProgressSteps
import co.kr.tnt.trainee.signup.model.PTPurpose
import co.kr.tnt.core.ui.R as uiResource
@@ -37,15 +34,14 @@ private const val COLUMNS_NUM = 2
@OptIn(ExperimentalLayoutApi::class)
@Composable
-fun TraineePTPurposePage(
+internal fun TraineePTPurposePage(
+ state: TraineeSignUpUiState,
+ onPurposeSelected: (String) -> Unit,
onBackClick: () -> Unit,
onNextClick: () -> Unit,
) {
BackHandler { onBackClick() }
- // TODO 리소스 id값 텍스트로 전환해 넘겨주기
- var selectedPurposes by remember { mutableStateOf(setOf()) }
-
Scaffold(
topBar = { TnTTopBarWithBackButton(onBackClick = onBackClick) },
containerColor = TnTTheme.colors.commonColors.Common0,
@@ -68,12 +64,11 @@ fun TraineePTPurposePage(
modifier = Modifier.fillMaxWidth(),
) {
PTPurpose.entries.forEach { purpose ->
+ val purposeText = stringResource(purpose.textResId)
PurposeButton(
- text = stringResource(purpose.textResId),
- isSelected = purpose in selectedPurposes,
- onClick = {
- selectedPurposes = toggleSelection(selectedPurposes, purpose)
- },
+ text = purposeText,
+ isSelected = purposeText in state.ptPurpose,
+ onClick = { onPurposeSelected(purposeText) },
modifier = Modifier.weight(1f),
)
}
@@ -83,7 +78,7 @@ fun TraineePTPurposePage(
TnTBottomButton(
text = stringResource(uiResource.string.next),
onClick = onNextClick,
- enabled = selectedPurposes.isNotEmpty(),
+ enabled = state.ptPurpose.isNotEmpty(),
modifier = Modifier.align(Alignment.BottomCenter),
)
}
@@ -106,27 +101,15 @@ fun PurposeButton(
)
}
-// 선택된 값 업데이트
-private fun toggleSelection(
- selectedPurposes: Set,
- purpose: PTPurpose,
-): Set {
- return selectedPurposes.toMutableSet().apply {
- if (contains(purpose)) {
- remove(purpose)
- } else {
- add(purpose)
- }
- }.toSet()
-}
-
@Preview(showBackground = true)
@Composable
private fun TraineePTPurposePagePreview() {
TnTTheme {
TraineePTPurposePage(
+ state = TraineeSignUpUiState(),
onBackClick = {},
onNextClick = {},
+ onPurposeSelected = {},
)
}
}
diff --git a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeProfileSetupPage.kt b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeProfileSetupPage.kt
index 5e79bf5..4d4c209 100644
--- a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeProfileSetupPage.kt
+++ b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeProfileSetupPage.kt
@@ -16,11 +16,6 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@@ -35,6 +30,7 @@ import co.kr.tnt.designsystem.component.image.TnTProfileImage
import co.kr.tnt.designsystem.theme.TnTTheme
import co.kr.tnt.domain.IMAGE_MAX_SIZE
import co.kr.tnt.feature.trainee.signup.R
+import co.kr.tnt.trainee.signup.TraineeSignUpContract.TraineeSignUpUiState
import co.kr.tnt.trainee.signup.component.ProgressSteps
import co.kr.tnt.ui.coil.ResizeTransformation
import co.kr.tnt.ui.model.DefaultUserProfile
@@ -42,29 +38,27 @@ import coil.compose.rememberAsyncImagePainter
import coil.request.ImageRequest
import co.kr.tnt.core.ui.R as uiResource
+private const val MAX_LENGTH = 15
+
@Composable
-fun TraineeProfileSetupPage(
+internal fun TraineeProfileSetupPage(
+ state: TraineeSignUpUiState,
+ onProfileImageSelect: (Uri) -> Unit,
+ onNameChange: (String) -> Unit,
onBackClick: () -> Unit,
onNextClick: () -> Unit,
) {
BackHandler { onBackClick() }
val context = LocalContext.current
-
- // TODO 상태 관리 따로 빼기
- val maxLength = 15
- var text by remember { mutableStateOf("") }
- val isWarning by remember { derivedStateOf { text.length > maxLength } }
- var profileImage by remember { mutableStateOf(null) }
-
val pickMediaLauncher = rememberLauncherForActivityResult(PickVisualMedia()) { uri ->
if (uri != null) {
- profileImage = uri
+ onProfileImageSelect(uri)
}
}
val painter = rememberAsyncImagePainter(
model = ImageRequest.Builder(context)
- .data(profileImage)
+ .data(state.image)
.transformations(ResizeTransformation(IMAGE_MAX_SIZE))
.build(),
)
@@ -91,7 +85,7 @@ fun TraineeProfileSetupPage(
.fillMaxWidth()
.padding(vertical = 12.dp),
defaultImage = painterResource(DefaultUserProfile.Trainee.image),
- image = profileImage?.let { painter },
+ image = state.image?.let { painter },
onEditClick = {
pickMediaLauncher.launch(
PickVisualMediaRequest(
@@ -103,23 +97,23 @@ fun TraineeProfileSetupPage(
Spacer(Modifier.padding(top = 60.dp))
TnTLabeledTextFieldWithCounter(
title = stringResource(uiResource.string.name),
- value = text,
+ value = state.name,
onValueChange = { newValue ->
val filteredText = validateInput(newValue)
- text = filteredText
+ onNameChange(filteredText)
},
modifier = Modifier.padding(horizontal = 20.dp),
placeholder = stringResource(R.string.enter_your_name),
- maxLength = maxLength,
+ maxLength = MAX_LENGTH,
isSingleLine = true,
- showWarning = isWarning,
+ showWarning = !state.isNameValid,
isRequired = true,
- warningMessage = stringResource(R.string.text_length_warning, maxLength),
+ warningMessage = stringResource(R.string.text_length_warning, MAX_LENGTH),
)
}
TnTBottomButton(
text = stringResource(uiResource.string.next),
- enabled = text.isNotBlank() && !isWarning,
+ enabled = state.name.isNotBlank() && state.isNameValid,
onClick = onNextClick,
modifier = Modifier.align(Alignment.BottomCenter),
)
@@ -139,8 +133,11 @@ private fun validateInput(input: String): String {
private fun TraineeProfileSetupPagePreview() {
TnTTheme {
TraineeProfileSetupPage(
+ state = TraineeSignUpUiState(),
onBackClick = {},
onNextClick = {},
+ onProfileImageSelect = {},
+ onNameChange = {},
)
}
}
diff --git a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpCompletePage.kt b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpCompletePage.kt
index dd60f9c..a753361 100644
--- a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpCompletePage.kt
+++ b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpCompletePage.kt
@@ -1,5 +1,6 @@
package co.kr.tnt.trainee.signup
+import android.net.Uri
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
@@ -22,21 +23,19 @@ import co.kr.tnt.designsystem.component.button.TnTBottomButton
import co.kr.tnt.designsystem.component.image.TnTProfileImage
import co.kr.tnt.designsystem.theme.TnTTheme
import co.kr.tnt.feature.trainee.signup.R
+import co.kr.tnt.trainee.signup.TraineeSignUpContract.TraineeSignUpUiState
import co.kr.tnt.ui.model.DefaultUserProfile
import coil.compose.rememberAsyncImagePainter
import co.kr.tnt.core.ui.R as uiResource
@Composable
-fun TraineeSignUpCompletePage(
+internal fun TraineeSignUpCompletePage(
+ state: TraineeSignUpUiState,
onBackClick: () -> Unit,
- onNextClick: () -> Unit,
+ onNextClick: (Uri?) -> Unit,
) {
BackHandler { onBackClick() }
- // TODO 이름, 프로필 사진 불러오기
- val name = "김헬짱"
- val profileImage = "https://buly.kr/7FQeS5M"
-
Scaffold(
containerColor = TnTTheme.colors.commonColors.Common0,
) { innerPadding ->
@@ -54,7 +53,7 @@ fun TraineeSignUpCompletePage(
.padding(bottom = 66.dp),
) {
Text(
- text = stringResource(R.string.nice_to_meet_you_trainee, name),
+ text = stringResource(R.string.nice_to_meet_you_trainee, state.name),
color = TnTTheme.colors.neutralColors.Neutral950,
style = TnTTheme.typography.h1,
textAlign = Center,
@@ -70,14 +69,14 @@ fun TraineeSignUpCompletePage(
Spacer(Modifier.padding(top = 28.dp))
TnTProfileImage(
defaultImage = painterResource(DefaultUserProfile.Trainee.image),
- image = rememberAsyncImagePainter(profileImage),
+ image = rememberAsyncImagePainter(state.image),
imageSize = 200.dp,
showEditButton = false,
)
}
TnTBottomButton(
text = stringResource(uiResource.string.start),
- onClick = onNextClick,
+ onClick = { onNextClick(state.image) },
modifier = Modifier.align(Alignment.BottomCenter),
)
}
@@ -91,6 +90,7 @@ private fun TraineeSignUpCompletePagePreview() {
TraineeSignUpCompletePage(
onBackClick = {},
onNextClick = {},
+ state = TraineeSignUpUiState(name = "김회원"),
)
}
}
diff --git a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpContract.kt b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpContract.kt
index d1b3925..0f15a58 100644
--- a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpContract.kt
+++ b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpContract.kt
@@ -1,21 +1,71 @@
package co.kr.tnt.trainee.signup
+import android.content.Context
+import android.net.Uri
import co.kr.tnt.ui.base.UiEvent
import co.kr.tnt.ui.base.UiSideEffect
import co.kr.tnt.ui.base.UiState
+import java.time.LocalDate
+
+private const val MAX_NAME_LENGTH = 15
+private const val MAX_HEIGHT_LENGTH = 3
+private const val MAX_WEIGHT_LENGTH = 5
internal class TraineeSignUpContract {
data class TraineeSignUpUiState(
val page: TraineeSignUpPage = TraineeSignUpPage.ProfileSetUp,
val name: String = "",
- ) : UiState
+ val image: Uri? = null,
+ val birthday: LocalDate? = null,
+ val height: String = "",
+ val weight: String = "",
+ val ptPurpose: List = emptyList(),
+ val caution: String? = "",
+ ) : UiState {
+ val isNameValid get() = name.length <= MAX_NAME_LENGTH
+
+ /**
+ * 키가 유효한 입력값인지 검사
+ * 형식: 정수 3자
+ */
+ val isHeightValid
+ get() = height.isNotEmpty() && height.toIntOrNull() != null &&
+ !height.startsWith("0") && height.length <= MAX_HEIGHT_LENGTH
+
+ /**
+ * 몸무게가 유효한 입력값인지 검사
+ * 형식: 5자 이하의 실수 (000, 00, 00.0, 000.0)
+ */
+ private val weightRegex = Regex("^(\\d{1,3}(\\.\\d)?)?\$")
+ val isWeightValid
+ get() = weight.isNotEmpty() && weight.matches(weightRegex) &&
+ !weight.startsWith("0") && weight.length <= MAX_WEIGHT_LENGTH
+
+ val isBasicInfoValid
+ get() = isWeightValid && isHeightValid
+ }
sealed interface TraineeSignUpUiEvent : UiEvent {
+ data class OnImageChange(val imageUri: Uri) : TraineeSignUpUiEvent
+ data class OnNameChange(val name: String) : TraineeSignUpUiEvent
+ data class OnHeightChange(val height: String) : TraineeSignUpUiEvent
+ data class OnWeightChange(val weight: String) : TraineeSignUpUiEvent
+ data class OnBirthdayChange(val birthday: LocalDate) : TraineeSignUpUiEvent
+ data class OnPurposeSelected(val purpose: String) : TraineeSignUpUiEvent
+ data class OnCautionChange(val text: String) : TraineeSignUpUiEvent
data object OnNextClick : TraineeSignUpUiEvent
data object OnBackClick : TraineeSignUpUiEvent
+ data class RequestSignUp(
+ val context: Context,
+ val imageUri: Uri?,
+ val id: String,
+ val email: String,
+ val authType: String,
+ ) : TraineeSignUpUiEvent
}
sealed interface TraineeSignUpEffect : UiSideEffect {
+ data class ShowToast(val message: String) : TraineeSignUpEffect
data object NavigateToBack : TraineeSignUpEffect
data object NavigateToConnect : TraineeSignUpEffect
}
diff --git a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpScreen.kt b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpScreen.kt
index c1a584b..5c6ed56 100644
--- a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpScreen.kt
+++ b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpScreen.kt
@@ -1,27 +1,53 @@
package co.kr.tnt.trainee.signup
+import android.net.Uri
+import android.widget.Toast
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
+import androidx.compose.ui.platform.LocalContext
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.kr.tnt.trainee.signup.TraineeSignUpContract.TraineeSignUpEffect
import co.kr.tnt.trainee.signup.TraineeSignUpContract.TraineeSignUpPage
import co.kr.tnt.trainee.signup.TraineeSignUpContract.TraineeSignUpUiEvent
import co.kr.tnt.trainee.signup.TraineeSignUpContract.TraineeSignUpUiState
+import java.time.LocalDate
@Composable
internal fun TraineeSignUpRoute(
+ authId: String,
+ authType: String,
+ email: String,
navigateToPrevious: () -> Unit,
navigateToConnect: () -> Unit,
viewModel: TraineeSignUpViewModel = hiltViewModel(),
) {
+ val context = LocalContext.current
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
TraineeSignUpScreen(
state = uiState,
+ onNameChange = { viewModel.setEvent(TraineeSignUpUiEvent.OnNameChange(it)) },
+ onProfileImageSelect = { viewModel.setEvent(TraineeSignUpUiEvent.OnImageChange(it)) },
+ onHeightChange = { viewModel.setEvent(TraineeSignUpUiEvent.OnHeightChange(it)) },
+ onWeightChange = { viewModel.setEvent(TraineeSignUpUiEvent.OnWeightChange(it)) },
+ onBirthdayChange = { viewModel.setEvent(TraineeSignUpUiEvent.OnBirthdayChange(it)) },
+ onPurposeSelected = { viewModel.setEvent(TraineeSignUpUiEvent.OnPurposeSelected(it)) },
+ onCautionChange = { viewModel.setEvent(TraineeSignUpUiEvent.OnCautionChange(it)) },
onBackClick = { viewModel.setEvent(TraineeSignUpUiEvent.OnBackClick) },
onNextClick = { viewModel.setEvent(TraineeSignUpUiEvent.OnNextClick) },
+ onSubmitSignUp = { uri ->
+ viewModel.setEvent(
+ TraineeSignUpUiEvent.RequestSignUp(
+ context = context,
+ imageUri = uri,
+ id = authId,
+ email = email,
+ authType = authType,
+ ),
+ )
+ },
)
LaunchedEffect(viewModel.effect) {
@@ -29,6 +55,9 @@ internal fun TraineeSignUpRoute(
when (effect) {
TraineeSignUpEffect.NavigateToBack -> navigateToPrevious()
TraineeSignUpEffect.NavigateToConnect -> navigateToConnect()
+ is TraineeSignUpEffect.ShowToast -> {
+ Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show()
+ }
}
}
}
@@ -37,29 +66,53 @@ internal fun TraineeSignUpRoute(
@Composable
private fun TraineeSignUpScreen(
state: TraineeSignUpUiState,
+ onProfileImageSelect: (Uri) -> Unit,
+ onNameChange: (String) -> Unit,
+ onHeightChange: (String) -> Unit,
+ onWeightChange: (String) -> Unit,
+ onCautionChange: (String) -> Unit,
+ onBirthdayChange: (LocalDate) -> Unit,
+ onPurposeSelected: (String) -> Unit,
+ onSubmitSignUp: (Uri?) -> Unit,
onNextClick: () -> Unit,
onBackClick: () -> Unit,
) {
when (state.page) {
TraineeSignUpPage.ProfileSetUp -> TraineeProfileSetupPage(
+ state = state,
+ onProfileImageSelect = onProfileImageSelect,
+ onNameChange = onNameChange,
onBackClick = onBackClick,
onNextClick = onNextClick,
)
+
TraineeSignUpPage.BasicInfo -> TraineeBasicInfoPage(
+ state = state,
+ onHeightChange = onHeightChange,
+ onWeightChange = onWeightChange,
+ onBirthdayChange = onBirthdayChange,
onBackClick = onBackClick,
onNextClick = onNextClick,
)
+
TraineeSignUpPage.NoteForTrainer -> TraineeNoteForTrainerPage(
+ caution = state.caution,
+ onCautionChange = onCautionChange,
onBackClick = onBackClick,
onNextClick = onNextClick,
)
+
TraineeSignUpPage.PTPurpose -> TraineePTPurposePage(
+ state = state,
+ onPurposeSelected = onPurposeSelected,
onBackClick = onBackClick,
onNextClick = onNextClick,
)
+
TraineeSignUpPage.SignUpComplete -> TraineeSignUpCompletePage(
+ state = state,
onBackClick = onBackClick,
- onNextClick = onNextClick,
+ onNextClick = onSubmitSignUp,
)
}
}
diff --git a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpViewModel.kt b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpViewModel.kt
index 8c2d38d..137ba1a 100644
--- a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpViewModel.kt
+++ b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpViewModel.kt
@@ -1,46 +1,137 @@
package co.kr.tnt.trainee.signup
+import android.content.Context
+import android.net.Uri
+import androidx.lifecycle.viewModelScope
+import co.kr.tnt.core.ui.R
+import co.kr.tnt.domain.model.UserType
+import co.kr.tnt.domain.repository.SignUpRepository
import co.kr.tnt.trainee.signup.TraineeSignUpContract.TraineeSignUpEffect
import co.kr.tnt.trainee.signup.TraineeSignUpContract.TraineeSignUpPage
import co.kr.tnt.trainee.signup.TraineeSignUpContract.TraineeSignUpUiEvent
import co.kr.tnt.trainee.signup.TraineeSignUpContract.TraineeSignUpUiState
import co.kr.tnt.ui.base.BaseViewModel
+import co.kr.tnt.ui.util.toFile
import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import java.time.LocalDate
import javax.inject.Inject
@HiltViewModel
-internal class TraineeSignUpViewModel @Inject constructor() :
- BaseViewModel(
+internal class TraineeSignUpViewModel @Inject constructor(
+ private val signUpRepository: SignUpRepository,
+) : BaseViewModel(
TraineeSignUpUiState(),
) {
- override suspend fun handleEvent(event: TraineeSignUpUiEvent) {
- when (event) {
- TraineeSignUpUiEvent.OnBackClick -> navigateToBack()
- TraineeSignUpUiEvent.OnNextClick -> navigateToNext()
+ override suspend fun handleEvent(event: TraineeSignUpUiEvent) {
+ when (event) {
+ is TraineeSignUpUiEvent.OnImageChange -> updateProfileImage(event.imageUri)
+ is TraineeSignUpUiEvent.OnNameChange -> updateName(event.name)
+ is TraineeSignUpUiEvent.OnHeightChange -> updateHeight(event.height)
+ is TraineeSignUpUiEvent.OnWeightChange -> updateWeight(event.weight)
+ is TraineeSignUpUiEvent.OnBirthdayChange -> updateBirthday(event.birthday)
+ is TraineeSignUpUiEvent.OnPurposeSelected -> updateSelectedPurposes(event.purpose)
+ is TraineeSignUpUiEvent.OnCautionChange -> updateCaution(event.text)
+ TraineeSignUpUiEvent.OnBackClick -> navigateToBack()
+ TraineeSignUpUiEvent.OnNextClick -> navigateToNext()
+ is TraineeSignUpUiEvent.RequestSignUp -> signUp(
+ context = event.context,
+ imageUri = event.imageUri,
+ id = event.id,
+ email = event.email,
+ authType = event.authType,
+ )
+ }
+ }
+
+ private fun signUp(
+ context: Context,
+ imageUri: Uri?,
+ id: String,
+ email: String,
+ authType: String,
+ ) {
+ viewModelScope.launch {
+ val state = currentState
+ val profileImagePart = imageUri?.toFile(context)
+
+ runCatching {
+ signUpRepository.signUp(
+ profileImage = profileImagePart,
+ userType = UserType.Trainee(
+ id = id,
+ name = state.name,
+ image = state.image.toString(),
+ birthday = state.birthday,
+ age = null,
+ weight = state.weight.toDouble(),
+ height = state.height.toInt(),
+ ptPurpose = state.ptPurpose,
+ caution = state.caution,
+ ),
+ socialId = id,
+ socialType = authType,
+ email = email,
+ )
+ }.onSuccess {
+ sendEffect(TraineeSignUpEffect.NavigateToConnect)
+ }.onFailure {
+ // TODO 디자인 시스템 Toast 적용
+ val message = context.getString(R.string.error_server_request_failed)
+ sendEffect(TraineeSignUpEffect.ShowToast(message))
}
}
+ }
+
+ private fun updateProfileImage(imageUri: Uri) {
+ updateState { copy(image = imageUri) }
+ }
+
+ private fun updateName(name: String) {
+ updateState { copy(name = name) }
+ }
- private fun navigateToNext() {
- val nextPage = when (currentState.page) {
- TraineeSignUpPage.SignUpComplete -> {
- sendEffect(TraineeSignUpEffect.NavigateToConnect)
- return
- }
+ private fun updateHeight(height: String) {
+ updateState { copy(height = height) }
+ }
+
+ private fun updateWeight(weight: String) {
+ updateState { copy(weight = weight) }
+ }
+
+ private fun updateBirthday(birthday: LocalDate) {
+ updateState { copy(birthday = birthday) }
+ }
- else -> TraineeSignUpPage.getNextPage(currentState.page)
+ private fun updateSelectedPurposes(purpose: String) {
+ val updatedPurposes = currentState.ptPurpose.toMutableList().apply {
+ if (contains(purpose)) {
+ remove(purpose)
+ } else {
+ add(purpose)
}
- updateState { copy(page = nextPage) }
}
+ updateState { copy(ptPurpose = updatedPurposes) }
+ }
- private fun navigateToBack() {
- val previousPage = when (currentState.page) {
- TraineeSignUpPage.ProfileSetUp -> {
- sendEffect(TraineeSignUpEffect.NavigateToBack)
- return
- }
+ private fun updateCaution(text: String) {
+ updateState { copy(caution = text) }
+ }
- else -> TraineeSignUpPage.getPreviousPage(currentState.page)
+ private fun navigateToNext() {
+ val nextPage = TraineeSignUpPage.getNextPage(currentState.page)
+ updateState { copy(page = nextPage) }
+ }
+
+ private fun navigateToBack() {
+ val previousPage = when (currentState.page) {
+ TraineeSignUpPage.ProfileSetUp -> {
+ sendEffect(TraineeSignUpEffect.NavigateToBack)
+ return
}
- updateState { copy(page = previousPage) }
+
+ else -> TraineeSignUpPage.getPreviousPage(currentState.page)
}
+ updateState { copy(page = previousPage) }
}
+}
diff --git a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/navigation/TraineeSignUpNavigation.kt b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/navigation/TraineeSignUpNavigation.kt
index c5fde06..a30ef28 100644
--- a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/navigation/TraineeSignUpNavigation.kt
+++ b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/navigation/TraineeSignUpNavigation.kt
@@ -9,9 +9,16 @@ import co.kr.tnt.navigation.Route
import co.kr.tnt.trainee.signup.TraineeSignUpRoute
fun NavController.navigateToTraineeSignUp(
+ authId: String,
+ authType: String,
+ email: String,
navOptions: NavOptionsBuilder.() -> Unit = {},
) = navigate(
- route = Route.TraineeSignUp,
+ route = Route.TraineeSignUp(
+ authId = authId,
+ authType = authType,
+ email = email,
+ ),
builder = navOptions,
)
@@ -22,6 +29,9 @@ fun NavGraphBuilder.traineeSignUpScreen(
composable { backstackEntry ->
backstackEntry.toRoute().apply {
TraineeSignUpRoute(
+ authId = authId,
+ authType = authType,
+ email = email,
navigateToPrevious = navigateToPrevious,
navigateToConnect = { navigateToConnect() },
)
diff --git a/feature/trainee/signup/src/main/res/values/strings.xml b/feature/trainee/signup/src/main/res/values/strings.xml
index dc06f29..c81f3af 100644
--- a/feature/trainee/signup/src/main/res/values/strings.xml
+++ b/feature/trainee/signup/src/main/res/values/strings.xml
@@ -22,7 +22,7 @@
트레이너가 꼭 알아야 할\n주의사항이 있나요?
트레이너에게 알려드릴게요.
- 글자 수를 초과했어요
+ 100자 미만으로 입력해주세요
만나서 반가워요\n%s 트레이니님!
트레이너와 함께\n케미를 터뜨려보세요! 🧨
diff --git a/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/CodeGenerationPage.kt b/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/CodeGenerationPage.kt
index 7e3bd00..e11c649 100644
--- a/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/CodeGenerationPage.kt
+++ b/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/CodeGenerationPage.kt
@@ -177,7 +177,7 @@ private fun copyToClipboard(context: Context, text: String) {
private fun CodeGenerationPagePreview() {
TnTTheme {
CodeGenerationPage(
- state = TODO(),
+ state = TrainerConnectUiState(),
onBackClick = {},
onSkipClick = {},
onRegenerateClick = {},
diff --git a/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/TraineeProfilePage.kt b/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/TraineeProfilePage.kt
index 59b5805..5dfea4a 100644
--- a/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/TraineeProfilePage.kt
+++ b/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/TraineeProfilePage.kt
@@ -29,6 +29,7 @@ import androidx.compose.ui.unit.dp
import co.kr.tnt.designsystem.component.button.TnTBottomButton
import co.kr.tnt.designsystem.component.image.TnTProfileImage
import co.kr.tnt.designsystem.theme.TnTTheme
+import co.kr.tnt.domain.model.UserType
import co.kr.tnt.feature.trainer.connect.R
import co.kr.tnt.trainer.connect.TrainerConnectContract.TrainerConnectUiState
import co.kr.tnt.ui.model.DefaultUserProfile
@@ -106,10 +107,12 @@ internal fun TraineeProfilePage(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth(),
) {
- TextWithLabel(
- label = stringResource(uiResource.string.age_label),
- text = trainee.age.toString() + stringResource(uiResource.string.age_unit),
- )
+ if (trainee.age.toString() != "null") {
+ TextWithLabel(
+ label = stringResource(uiResource.string.age_label),
+ text = trainee.age.toString() + stringResource(uiResource.string.age_unit),
+ )
+ }
TextWithLabel(
label = stringResource(uiResource.string.height_label),
text = trainee.height.toString() + stringResource(uiResource.string.height_unit),
@@ -198,7 +201,19 @@ private fun TextWithBackground(
private fun TraineeProfilePagePreview() {
TnTTheme {
TraineeProfilePage(
- state = TODO(),
+ state = TrainerConnectUiState(
+ traineeState = UserType.Trainee(
+ id = "",
+ name = "김회원",
+ image = null,
+ birthday = null,
+ age = null,
+ weight = 55.0,
+ height = 150,
+ ptPurpose = listOf("체중 감량", "자세 교정"),
+ caution = null,
+ ),
+ ),
onNextClick = {},
onBackClick = {},
)
diff --git a/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/TrainerConnectCompletePage.kt b/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/TrainerConnectCompletePage.kt
index d2c6e33..eaa9e33 100644
--- a/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/TrainerConnectCompletePage.kt
+++ b/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/TrainerConnectCompletePage.kt
@@ -129,7 +129,7 @@ private fun ProfileSection(
private fun TrainerConnectCompletePagePreview() {
TnTTheme {
TrainerConnectCompletePage(
- state = TODO(),
+ state = TrainerConnectUiState(),
onNextClick = {},
onBackClick = {},
)
diff --git a/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/TrainerConnectContract.kt b/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/TrainerConnectContract.kt
index 2ccf95a..87ed5be 100644
--- a/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/TrainerConnectContract.kt
+++ b/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/TrainerConnectContract.kt
@@ -9,8 +9,22 @@ internal class TrainerConnectContract {
data class TrainerConnectUiState(
val page: TrainerConnectPage = TrainerConnectPage.CodeGeneration,
val inviteCode: String = "",
- val trainerState: UserType.Trainer = UserType.Trainer(),
- val traineeState: UserType.Trainee = UserType.Trainee(),
+ val trainerState: UserType.Trainer = UserType.Trainer(
+ id = "",
+ name = "",
+ image = null,
+ ),
+ val traineeState: UserType.Trainee = UserType.Trainee(
+ id = "",
+ name = "",
+ image = null,
+ birthday = null,
+ age = 0,
+ weight = 0.0,
+ height = 0,
+ ptPurpose = emptyList(),
+ caution = null,
+ ),
) : UiState
sealed interface TrainerConnectUiEvent : UiEvent {
diff --git a/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/TrainerConnectViewModel.kt b/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/TrainerConnectViewModel.kt
index 200c723..93f32b0 100644
--- a/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/TrainerConnectViewModel.kt
+++ b/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/TrainerConnectViewModel.kt
@@ -25,7 +25,6 @@ internal class TrainerConnectViewModel @Inject constructor() :
TrainerConnectUiEvent.OnNextClick -> navigateToNext()
TrainerConnectUiEvent.OnBackClick -> navigateToBack()
TrainerConnectUiEvent.OnSkipClick -> navigateToHome()
- else -> {}
}
}
@@ -42,8 +41,9 @@ internal class TrainerConnectViewModel @Inject constructor() :
id = "trainee",
name = "김회원",
image = "https://buly.kr/3j7VVqN",
+ birthday = null,
age = 25,
- weight = 100F,
+ weight = 100.0,
height = 165,
ptPurpose = listOf("체중 감량", "자세 교정"),
caution = "발목이 안좋아서 발목에 무리가는 행동을 하면 안돼요. 잘 부탁드려요!",
diff --git a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerProfileSetupPage.kt b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerProfileSetupPage.kt
index 0cfce9d..4f2901a 100644
--- a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerProfileSetupPage.kt
+++ b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerProfileSetupPage.kt
@@ -19,9 +19,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@@ -36,14 +34,20 @@ import co.kr.tnt.designsystem.component.image.TnTProfileImage
import co.kr.tnt.designsystem.theme.TnTTheme
import co.kr.tnt.domain.IMAGE_MAX_SIZE
import co.kr.tnt.feature.trainer.signup.R
+import co.kr.tnt.trainer.signup.TrainerSignUpContract.TrainerSignUpUiState
import co.kr.tnt.ui.coil.ResizeTransformation
import co.kr.tnt.ui.model.DefaultUserProfile
import coil.compose.rememberAsyncImagePainter
import coil.request.ImageRequest
import co.kr.tnt.core.ui.R as uiResource
+private const val MAX_LENGTH = 15
+
@Composable
-fun TrainerProfileSetupPage(
+internal fun TrainerProfileSetupPage(
+ state: TrainerSignUpUiState,
+ onProfileImageSelect: (Uri) -> Unit,
+ onNameChange: (String) -> Unit,
onBackClick: () -> Unit,
onNextClick: () -> Unit,
) {
@@ -51,20 +55,18 @@ fun TrainerProfileSetupPage(
val context = LocalContext.current
- // TODO 상태 관리 따로 빼기
- val maxLength = 15
- var text by remember { mutableStateOf("") }
- val isWarning by remember { derivedStateOf { text.length > maxLength } }
- var profileImage by remember { mutableStateOf(null) }
+ val isWarning by remember(state.name) {
+ derivedStateOf { state.name.length > MAX_LENGTH }
+ }
val pickMediaLauncher = rememberLauncherForActivityResult(PickVisualMedia()) { uri ->
if (uri != null) {
- profileImage = uri
+ onProfileImageSelect(uri)
}
}
val painter = rememberAsyncImagePainter(
model = ImageRequest.Builder(context)
- .data(profileImage)
+ .data(state.image)
.transformations(ResizeTransformation(IMAGE_MAX_SIZE))
.build(),
)
@@ -92,7 +94,7 @@ fun TrainerProfileSetupPage(
.fillMaxWidth()
.padding(vertical = 12.dp),
defaultImage = painterResource(DefaultUserProfile.Trainer.image),
- image = profileImage?.let { painter },
+ image = state.image?.let { painter },
onEditClick = {
pickMediaLauncher.launch(
PickVisualMediaRequest(
@@ -104,24 +106,24 @@ fun TrainerProfileSetupPage(
Spacer(Modifier.padding(top = 60.dp))
TnTLabeledTextFieldWithCounter(
title = stringResource(uiResource.string.name),
- value = text,
+ value = state.name,
onValueChange = { newValue ->
val filteredText = validateInput(newValue)
- text = filteredText
+ onNameChange(filteredText)
},
modifier = Modifier.padding(horizontal = 20.dp),
placeholder = stringResource(R.string.name_placeholder),
- maxLength = maxLength,
+ maxLength = MAX_LENGTH,
isSingleLine = true,
showWarning = isWarning,
isRequired = true,
- warningMessage = stringResource(R.string.text_length_warning, maxLength),
+ warningMessage = stringResource(R.string.text_length_warning, MAX_LENGTH),
)
}
TnTBottomButton(
text = stringResource(uiResource.string.next),
modifier = Modifier.align(Alignment.BottomCenter),
- enabled = text.isNotBlank() && !isWarning,
+ enabled = state.name.isNotBlank() && !isWarning,
onClick = onNextClick,
)
}
@@ -140,6 +142,9 @@ private fun validateInput(input: String): String {
private fun TrainerProfileSetupPagePreview() {
TnTTheme {
TrainerProfileSetupPage(
+ state = TrainerSignUpUiState(),
+ onNameChange = {},
+ onProfileImageSelect = {},
onBackClick = {},
onNextClick = {},
)
diff --git a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpCompletePage.kt b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpCompletePage.kt
index c5c0840..7ebd3b9 100644
--- a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpCompletePage.kt
+++ b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpCompletePage.kt
@@ -1,5 +1,6 @@
package co.kr.tnt.trainer.signup
+import android.net.Uri
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
@@ -22,21 +23,19 @@ import co.kr.tnt.designsystem.component.button.TnTBottomButton
import co.kr.tnt.designsystem.component.image.TnTProfileImage
import co.kr.tnt.designsystem.theme.TnTTheme
import co.kr.tnt.feature.trainer.signup.R
+import co.kr.tnt.trainer.signup.TrainerSignUpContract.TrainerSignUpUiState
import co.kr.tnt.ui.model.DefaultUserProfile
import coil.compose.rememberAsyncImagePainter
import co.kr.tnt.core.ui.R as uiResource
@Composable
-fun TrainerSignUpCompletePage(
+internal fun TrainerSignUpCompletePage(
+ state: TrainerSignUpUiState,
onBackClick: () -> Unit,
- onNextClick: () -> Unit,
+ onNextClick: (Uri?) -> Unit,
) {
BackHandler { onBackClick() }
- // TODO 이름, 프로필 이미지 불러오기
- val name = "김헬짱"
- val profileImage = "https://buly.kr/7FQeS5M"
-
Scaffold(
containerColor = TnTTheme.colors.commonColors.Common0,
) { innerPadding ->
@@ -54,7 +53,7 @@ fun TrainerSignUpCompletePage(
.padding(bottom = 66.dp),
) {
Text(
- text = stringResource(R.string.nice_to_meet_you_trainer, name),
+ text = stringResource(R.string.nice_to_meet_you_trainer, state.name),
color = TnTTheme.colors.neutralColors.Neutral950,
style = TnTTheme.typography.h1,
textAlign = Center,
@@ -70,14 +69,14 @@ fun TrainerSignUpCompletePage(
Spacer(Modifier.padding(top = 28.dp))
TnTProfileImage(
defaultImage = painterResource(DefaultUserProfile.Trainer.image),
- image = rememberAsyncImagePainter(profileImage),
+ image = rememberAsyncImagePainter(state.image),
imageSize = 200.dp,
showEditButton = false,
)
}
TnTBottomButton(
text = stringResource(uiResource.string.start),
- onClick = onNextClick,
+ onClick = { onNextClick(state.image) },
modifier = Modifier.align(Alignment.BottomCenter),
)
}
@@ -91,6 +90,7 @@ private fun TrainerSignUpCompletePagePreview() {
TrainerSignUpCompletePage(
onBackClick = {},
onNextClick = {},
+ state = TrainerSignUpUiState(),
)
}
}
diff --git a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpContract.kt b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpContract.kt
index a77a505..4b35640 100644
--- a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpContract.kt
+++ b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpContract.kt
@@ -1,5 +1,7 @@
package co.kr.tnt.trainer.signup
+import android.content.Context
+import android.net.Uri
import co.kr.tnt.ui.base.UiEvent
import co.kr.tnt.ui.base.UiSideEffect
import co.kr.tnt.ui.base.UiState
@@ -8,14 +10,25 @@ internal class TrainerSignUpContract {
data class TrainerSignUpUiState(
val page: TrainerSignUpPage = TrainerSignUpPage.ProfileSetUp,
val name: String = "",
+ val image: Uri? = null,
) : UiState
sealed interface TrainerSignUpUiEvent : UiEvent {
+ data class OnImageChange(val imageUri: Uri) : TrainerSignUpUiEvent
+ data class OnNameChange(val name: String) : TrainerSignUpUiEvent
data object OnNextClick : TrainerSignUpUiEvent
data object OnBackClick : TrainerSignUpUiEvent
+ data class RequestSignUp(
+ val context: Context,
+ val imageUri: Uri?,
+ val id: String,
+ val email: String,
+ val authType: String,
+ ) : TrainerSignUpUiEvent
}
sealed interface TrainerSignUpEffect : UiSideEffect {
+ data class ShowToast(val message: String) : TrainerSignUpEffect
data object NavigateToBack : TrainerSignUpEffect
data object NavigateToConnect : TrainerSignUpEffect
}
diff --git a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpScreen.kt b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpScreen.kt
index d0a6d44..0aecbd5 100644
--- a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpScreen.kt
+++ b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpScreen.kt
@@ -1,8 +1,11 @@
package co.kr.tnt.trainer.signup
+import android.net.Uri
+import android.widget.Toast
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
+import androidx.compose.ui.platform.LocalContext
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.kr.tnt.trainer.signup.TrainerSignUpContract.TrainerSignUpUiEvent
@@ -10,16 +13,33 @@ import co.kr.tnt.trainer.signup.TrainerSignUpContract.TrainerSignUpUiState
@Composable
internal fun TrainerSignUpRoute(
+ authId: String,
+ authType: String,
+ email: String,
navigateToPrevious: () -> Unit,
navigateToConnect: () -> Unit,
viewModel: TrainerSignUpViewModel = hiltViewModel(),
) {
+ val context = LocalContext.current
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
TrainerSignUpScreen(
state = uiState,
+ onNameChange = { viewModel.setEvent(TrainerSignUpUiEvent.OnNameChange(it)) },
+ onProfileImageSelect = { viewModel.setEvent(TrainerSignUpUiEvent.OnImageChange(it)) },
onNextClick = { viewModel.setEvent(TrainerSignUpUiEvent.OnNextClick) },
onBackClick = { viewModel.setEvent(TrainerSignUpUiEvent.OnBackClick) },
+ onSubmitSignUp = { uri ->
+ viewModel.setEvent(
+ TrainerSignUpUiEvent.RequestSignUp(
+ context = context,
+ imageUri = uri,
+ id = authId,
+ email = email,
+ authType = authType,
+ ),
+ )
+ },
)
LaunchedEffect(viewModel.effect) {
@@ -27,6 +47,9 @@ internal fun TrainerSignUpRoute(
when (effect) {
TrainerSignUpContract.TrainerSignUpEffect.NavigateToBack -> navigateToPrevious()
TrainerSignUpContract.TrainerSignUpEffect.NavigateToConnect -> navigateToConnect()
+ is TrainerSignUpContract.TrainerSignUpEffect.ShowToast -> {
+ Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show()
+ }
}
}
}
@@ -35,16 +58,24 @@ internal fun TrainerSignUpRoute(
@Composable
private fun TrainerSignUpScreen(
state: TrainerSignUpUiState,
+ onProfileImageSelect: (Uri) -> Unit,
+ onNameChange: (String) -> Unit,
+ onSubmitSignUp: (Uri?) -> Unit,
onNextClick: () -> Unit,
onBackClick: () -> Unit,
) {
when (state.page) {
TrainerSignUpContract.TrainerSignUpPage.ProfileSetUp -> TrainerProfileSetupPage(
+ state = state,
+ onProfileImageSelect = onProfileImageSelect,
+ onNameChange = onNameChange,
onNextClick = onNextClick,
onBackClick = onBackClick,
)
+
TrainerSignUpContract.TrainerSignUpPage.SignUpComplete -> TrainerSignUpCompletePage(
- onNextClick = onNextClick,
+ state = state,
+ onNextClick = onSubmitSignUp,
onBackClick = onBackClick,
)
}
diff --git a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpViewModel.kt b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpViewModel.kt
index 0ec565c..886876f 100644
--- a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpViewModel.kt
+++ b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpViewModel.kt
@@ -1,46 +1,97 @@
package co.kr.tnt.trainer.signup
+import android.content.Context
+import android.net.Uri
+import androidx.lifecycle.viewModelScope
+import co.kr.tnt.domain.model.UserType
+import co.kr.tnt.domain.repository.SignUpRepository
import co.kr.tnt.trainer.signup.TrainerSignUpContract.TrainerSignUpEffect
import co.kr.tnt.trainer.signup.TrainerSignUpContract.TrainerSignUpPage
import co.kr.tnt.trainer.signup.TrainerSignUpContract.TrainerSignUpUiEvent
import co.kr.tnt.trainer.signup.TrainerSignUpContract.TrainerSignUpUiState
import co.kr.tnt.ui.base.BaseViewModel
+import co.kr.tnt.ui.util.toFile
import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
import javax.inject.Inject
+import co.kr.tnt.core.ui.R as uiResource
@HiltViewModel
-internal class TrainerSignUpViewModel @Inject constructor() :
- BaseViewModel(
+internal class TrainerSignUpViewModel @Inject constructor(
+ private val signUpRepository: SignUpRepository,
+) : BaseViewModel(
TrainerSignUpUiState(),
) {
- override suspend fun handleEvent(event: TrainerSignUpUiEvent) {
- when (event) {
- TrainerSignUpUiEvent.OnNextClick -> navigateToNext()
- TrainerSignUpUiEvent.OnBackClick -> navigateToBack()
- }
+ override suspend fun handleEvent(event: TrainerSignUpUiEvent) {
+ when (event) {
+ is TrainerSignUpUiEvent.OnImageChange -> setProfileImage(event.imageUri)
+ is TrainerSignUpUiEvent.OnNameChange -> setName(event.name)
+ TrainerSignUpUiEvent.OnNextClick -> navigateToNext()
+ TrainerSignUpUiEvent.OnBackClick -> navigateToBack()
+ is TrainerSignUpUiEvent.RequestSignUp -> signUp(
+ context = event.context,
+ imageUri = event.imageUri,
+ id = event.id,
+ email = event.email,
+ authType = event.authType,
+ )
}
+ }
- private fun navigateToNext() {
- val nextPage = when (currentState.page) {
- TrainerSignUpPage.SignUpComplete -> {
- sendEffect(TrainerSignUpEffect.NavigateToConnect)
- return
- }
+ private fun signUp(
+ context: Context,
+ imageUri: Uri?,
+ id: String,
+ email: String,
+ authType: String,
+ ) {
+ viewModelScope.launch {
+ val profileImagePart = imageUri?.toFile(context)
- else -> TrainerSignUpPage.getNextPage(currentState.page)
+ runCatching {
+ signUpRepository.signUp(
+ profileImage = profileImagePart,
+ userType = UserType.Trainer(
+ id = id,
+ name = currentState.name,
+ image = currentState.image.toString(),
+ ),
+ socialId = id,
+ socialType = authType,
+ email = email,
+ )
+ }.onSuccess {
+ sendEffect(TrainerSignUpEffect.NavigateToConnect)
+ }.onFailure {
+ // TODO 디자인 시스템 Toast 적용
+ val message = context.getString(uiResource.string.error_server_request_failed)
+ sendEffect(TrainerSignUpEffect.ShowToast(message))
}
- updateState { copy(page = nextPage) }
}
+ }
+
+ private fun setProfileImage(imageUri: Uri) {
+ updateState { copy(image = imageUri) }
+ }
- private fun navigateToBack() {
- val previousPage = when (currentState.page) {
- TrainerSignUpPage.ProfileSetUp -> {
- sendEffect(TrainerSignUpEffect.NavigateToBack)
- return
- }
+ private fun setName(name: String) {
+ updateState { copy(name = name) }
+ }
- else -> TrainerSignUpPage.getPreviousPage(currentState.page)
+ private fun navigateToNext() {
+ val nextPage = TrainerSignUpPage.getNextPage(currentState.page)
+ updateState { copy(page = nextPage) }
+ }
+
+ private fun navigateToBack() {
+ val previousPage = when (currentState.page) {
+ TrainerSignUpPage.ProfileSetUp -> {
+ sendEffect(TrainerSignUpEffect.NavigateToBack)
+ return
}
- updateState { copy(page = previousPage) }
+
+ else -> TrainerSignUpPage.getPreviousPage(currentState.page)
}
+ updateState { copy(page = previousPage) }
}
+}
diff --git a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/navigation/TrainerSignUpNavigation.kt b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/navigation/TrainerSignUpNavigation.kt
index c17b09b..06811d9 100644
--- a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/navigation/TrainerSignUpNavigation.kt
+++ b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/navigation/TrainerSignUpNavigation.kt
@@ -9,9 +9,16 @@ import co.kr.tnt.navigation.Route
import co.kr.tnt.trainer.signup.TrainerSignUpRoute
fun NavController.navigateToTrainerSignUp(
+ authId: String,
+ authType: String,
+ email: String,
navOptions: NavOptionsBuilder.() -> Unit = {},
) = navigate(
- route = Route.TrainerSignUp,
+ route = Route.TrainerSignUp(
+ authId = authId,
+ authType = authType,
+ email = email,
+ ),
builder = navOptions,
)
@@ -21,8 +28,10 @@ fun NavGraphBuilder.trainerSignUpScreen(
) {
composable { backstackEntry ->
backstackEntry.toRoute().apply {
- // TODO 115 머지되면 connect로 이동
TrainerSignUpRoute(
+ authId = authId,
+ authType = authType,
+ email = email,
navigateToPrevious = navigateToPrevious,
navigateToConnect = { navigateToConnect() },
)