diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/response/AddClubResponseDto.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/response/AddClubResponseDto.kt new file mode 100644 index 0000000..bde6b8f --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/response/AddClubResponseDto.kt @@ -0,0 +1,16 @@ +package org.whosin.client.data.dto.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class AddClubResponseDto( + @SerialName("success") + val success: Boolean, + @SerialName("status") + val status: Int, + @SerialName("message") + val message: String, + @SerialName("data") + val data: String? = null +) diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/response/ClubCodeConfirmResponseDto.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/response/ClubCodeConfirmResponseDto.kt new file mode 100644 index 0000000..197b6b3 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/response/ClubCodeConfirmResponseDto.kt @@ -0,0 +1,24 @@ +package org.whosin.client.data.dto.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ClubCodeConfirmResponseDto( + @SerialName("success") + val success: Boolean, + @SerialName("status") + val status: Int, + @SerialName("message") + val message: String, + @SerialName("data") + val data: ClubCodeConfirmData +) + +@Serializable +data class ClubCodeConfirmData( + @SerialName("clubId") + val clubId: Int, + @SerialName("clubName") + val clubName: String +) diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/response/ErrorResponseDto.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/response/ErrorResponseDto.kt new file mode 100644 index 0000000..41ce98e --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/response/ErrorResponseDto.kt @@ -0,0 +1,16 @@ +package org.whosin.client.data.dto.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ErrorResponseDto( + @SerialName("success") + val success: Boolean, + @SerialName("status") + val status: Int, + @SerialName("message") + val message: String, + @SerialName("timestamp") + val timestamp: String? = null +) diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/data/remote/RemoteClubDataSource.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/data/remote/RemoteClubDataSource.kt index 7cb797b..1b07754 100644 --- a/composeApp/src/commonMain/kotlin/org/whosin/client/data/remote/RemoteClubDataSource.kt +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/data/remote/RemoteClubDataSource.kt @@ -4,11 +4,16 @@ import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.request.delete import io.ktor.client.request.get +import io.ktor.client.request.parameter import io.ktor.client.request.post import io.ktor.client.statement.HttpResponse import io.ktor.http.isSuccess +import io.ktor.http.parameters import org.whosin.client.core.network.ApiResult +import org.whosin.client.data.dto.response.AddClubResponseDto +import org.whosin.client.data.dto.response.ClubCodeConfirmResponseDto import org.whosin.client.data.dto.response.ClubPresencesResponseDto +import org.whosin.client.data.dto.response.ErrorResponseDto import org.whosin.client.data.dto.response.MyClubResponseDto class RemoteClubDataSource( @@ -93,4 +98,68 @@ class RemoteClubDataSource( ApiResult.Error(message = t.message, cause = t) } } + + // 동아리 번호 확인 + suspend fun confirmClubCode(clubCode: String): ApiResult{ + return try { + val response: HttpResponse = client.get(urlString = "clubs"){ + parameter("clubNumber", clubCode) + } + + if (response.status.isSuccess()) { + ApiResult.Success( + data = response.body(), + statusCode = response.status.value + ) + } else { + // 에러 응답 파싱 시도 + try { + val errorResponse: ErrorResponseDto = response.body() + ApiResult.Error( + code = response.status.value, + message = errorResponse.message + ) + } catch (e: Exception) { + // 파싱 실패 시 기본 에러 메시지 + ApiResult.Error( + code = response.status.value, + message = "HTTP Error: ${response.status.value}" + ) + } + } + } catch (t: Throwable){ + ApiResult.Error(message = t.message, cause = t) + } + } + + // 동아리 추가 함수 + suspend fun addClub(clubId: Int): ApiResult { + return try { + val response: HttpResponse = client.post(urlString = "clubs/$clubId") + + if (response.status.isSuccess()) { + ApiResult.Success( + data = response.body(), + statusCode = response.status.value + ) + } else { + // 에러 응답 파싱 시도 + try { + val errorResponse: ErrorResponseDto = response.body() + ApiResult.Error( + code = response.status.value, + message = errorResponse.message + ) + } catch (e: Exception) { + // 파싱 실패 시 기본 에러 메시지 + ApiResult.Error( + code = response.status.value, + message = "HTTP Error: ${response.status.value}" + ) + } + } + } catch (t: Throwable){ + ApiResult.Error(message = t.message, cause = t) + } + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/data/repository/ClubRepository.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/data/repository/ClubRepository.kt index a11850c..37452aa 100644 --- a/composeApp/src/commonMain/kotlin/org/whosin/client/data/repository/ClubRepository.kt +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/data/repository/ClubRepository.kt @@ -1,6 +1,8 @@ package org.whosin.client.data.repository import org.whosin.client.core.network.ApiResult +import org.whosin.client.data.dto.response.AddClubResponseDto +import org.whosin.client.data.dto.response.ClubCodeConfirmResponseDto import org.whosin.client.data.dto.response.ClubPresencesResponseDto import org.whosin.client.data.dto.response.MyClubResponseDto import org.whosin.client.data.remote.RemoteClubDataSource @@ -19,4 +21,10 @@ class ClubRepository( suspend fun checkOut(clubId: Int): ApiResult = dataSource.checkOut(clubId) + + suspend fun confirmClubCode(clubCode: String): ApiResult = + dataSource.confirmClubCode(clubCode) + + suspend fun addClub(clubId: Int): ApiResult = + dataSource.addClub(clubId) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/di/DIModules.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/di/DIModules.kt index 8d749dc..4838b27 100644 --- a/composeApp/src/commonMain/kotlin/org/whosin/client/di/DIModules.kt +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/di/DIModules.kt @@ -10,6 +10,7 @@ import org.whosin.client.data.remote.RemoteMemberDataSource import org.whosin.client.data.repository.DummyRepository import org.whosin.client.data.repository.ClubRepository import org.whosin.client.data.repository.MemberRepository +import org.whosin.client.presentation.auth.clubcode.AddClubViewModel import org.whosin.client.presentation.dummy.DummyViewModel import org.whosin.client.presentation.dummy.TokenTestViewModel import org.whosin.client.presentation.auth.login.viewmodel.LoginViewModel @@ -49,4 +50,5 @@ val viewModelModule = module { viewModelOf(::MyPageViewModel) viewModelOf(::DummyViewModel) // TODO: 이후에 삭제 예정 viewModelOf(::TokenTestViewModel) // TODO: 이후에 삭제 예정 + viewModelOf(::AddClubViewModel) } diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/clubcode/AddClubViewModel.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/clubcode/AddClubViewModel.kt new file mode 100644 index 0000000..58bb3cc --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/clubcode/AddClubViewModel.kt @@ -0,0 +1,96 @@ +package org.whosin.client.presentation.auth.clubcode + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.whosin.client.core.network.ApiResult +import org.whosin.client.data.repository.ClubRepository + +data class AddClubUiState( + val isLoading: Boolean = false, + val verificationState: ClubCodeState = ClubCodeState.INPUT, + val clubName: String? = null, + val clubId: Int? = null, + val errorMessage: String? = null, + val isAddClubSuccess: Boolean = false +) + +class AddClubViewModel( + private val repository: ClubRepository +) : ViewModel() { + private val _uiState = MutableStateFlow(AddClubUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + fun confirmClubCode(clubCode: String) { + viewModelScope.launch { + _uiState.update { it.copy(isLoading = true) } + when (val result = repository.confirmClubCode(clubCode = clubCode)) { + is ApiResult.Success -> { + val response = result.data.data + _uiState.update { + it.copy( + isLoading = false, + verificationState = ClubCodeState.SUCCESS, + clubName = response.clubName, + clubId = response.clubId, + errorMessage = null + ) + } + println("AddClubViewModel : 조회 성공") + } + is ApiResult.Error -> { + _uiState.update { + it.copy( + isLoading = false, + verificationState = ClubCodeState.ERROR, + errorMessage = result.message?: "동아리 이름 조회에 오류가 발생했습니다." + ) + } + println("AddClubViewModel : 조회 실패") + } + } + } + } + + // 에러 상태 리셋 함수 + fun resetErrorState() { + _uiState.update { + it.copy( + verificationState = ClubCodeState.INPUT, + errorMessage = null, + clubName = null, + clubId = null + ) + } + } + + // 동아리 추가 함수 + fun addClub(clubId: Int) { + viewModelScope.launch { + _uiState.update { it.copy(isLoading = true) } + when (val result = repository.addClub(clubId = clubId)) { + is ApiResult.Success -> { + _uiState.update { + it.copy( + isLoading = false, + errorMessage = null, + isAddClubSuccess = true + ) + } + } + is ApiResult.Error -> { + _uiState.update { + it.copy( + isLoading = false, + errorMessage = result.message ?: "동아리 추가에 오류가 발생했습니다." + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/clubcode/ClubCodeInputScreen.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/clubcode/ClubCodeInputScreen.kt index 0cdec61..037e787 100644 --- a/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/clubcode/ClubCodeInputScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/clubcode/ClubCodeInputScreen.kt @@ -31,10 +31,12 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.delay import coil3.compose.AsyncImage import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.ui.tooling.preview.Preview +import org.koin.compose.viewmodel.koinViewModel import org.whosin.client.presentation.auth.login.component.CommonLoginButton import org.whosin.client.presentation.auth.login.component.NumberInputBox import whosinclient.composeapp.generated.resources.Res @@ -50,15 +52,14 @@ import whosinclient.composeapp.generated.resources.confirm_button fun ClubCodeInputScreen( modifier: Modifier = Modifier, onNavigateBack: () -> Unit = {}, - onNavigateToHome: (String) -> Unit = {}, - onVerifyClubCode: (String) -> Unit = {}, - onErrorReset: () -> Unit = {}, - verificationState: ClubCodeState = ClubCodeState.INPUT, - clubName: String = "" + onNavigateToHome: () -> Unit = {}, + viewModel: AddClubViewModel = koinViewModel() ) { + val uiState = viewModel.uiState.collectAsStateWithLifecycle().value + var clubCode by remember { mutableStateOf(arrayOf("", "", "", "", "", "")) } var currentFocusIndex by remember { mutableStateOf(0) } - val currentState = verificationState + val currentState = uiState.verificationState val focusRequesters = remember { List(6) { FocusRequester() } } val keyboardController = LocalSoftwareKeyboardController.current @@ -81,7 +82,17 @@ fun ClubCodeInputScreen( clubCode = arrayOf("", "", "", "", "", "") currentFocusIndex = 0 focusRequesters[0].requestFocus() - onErrorReset() + viewModel.resetErrorState() + } + if (currentState == ClubCodeState.SUCCESS){ + keyboardController?.hide() + } + } + + // 동아리 추가 성공 시 홈으로 이동 + LaunchedEffect(uiState.isAddClubSuccess) { + if (uiState.isAddClubSuccess) { + onNavigateToHome() } } @@ -185,7 +196,7 @@ fun ClubCodeInputScreen( // 에러 메시지 if (currentState == ClubCodeState.ERROR) { Text( - text = stringResource(Res.string.club_code_error_message), + text = uiState.errorMessage?:"예상치 못한 오류가 발생했습니다", fontSize = 16.sp, fontWeight = FontWeight.W500, color = Color(0xFFFF3636), @@ -196,6 +207,7 @@ fun ClubCodeInputScreen( ) } + Box( modifier = Modifier .padding(top = if (currentState != ClubCodeState.ERROR) 48.dp else 0.dp) @@ -206,7 +218,10 @@ fun ClubCodeInputScreen( text = stringResource(Res.string.club_code_confirm_button), onClick = { if (isComplete) { - onVerifyClubCode(fullCode) + viewModel.confirmClubCode(clubCode = fullCode) + if (currentState == ClubCodeState.SUCCESS){ + keyboardController?.hide() + } } }, enabled = isComplete, @@ -237,13 +252,27 @@ fun ClubCodeInputScreen( contentAlignment = Alignment.Center ) { Text( - text = clubName, + text = uiState.clubName ?: "", fontSize = 16.sp, fontWeight = FontWeight.W500, color = Color.Black, textAlign = TextAlign.Center ) } + + // 동아리 추가 실패 에러 메시지 + if (uiState.errorMessage != null && !uiState.isAddClubSuccess) { + Text( + text = uiState.errorMessage, + fontSize = 14.sp, + fontWeight = FontWeight.W500, + color = Color(0xFFFF3636), + textAlign = TextAlign.Center, + modifier = Modifier + .padding(top = 16.dp) + .fillMaxWidth() + ) + } } } @@ -252,7 +281,11 @@ fun ClubCodeInputScreen( text = stringResource(Res.string.confirm_button), onClick = { if (currentState == ClubCodeState.SUCCESS) { - onNavigateToHome(clubName) + if (uiState.clubId != null){ + viewModel.addClub(uiState.clubId) + } + } else { + println("ClubCodeInputScreen : 확인 버튼 오류") } }, enabled = currentState == ClubCodeState.SUCCESS, @@ -267,25 +300,9 @@ fun ClubCodeInputScreen( @Preview @Composable fun ClubCodeInputScreenPreview() { - var verificationState by remember { mutableStateOf(ClubCodeState.INPUT) } - var clubName by remember { mutableStateOf("") } - ClubCodeInputScreen( modifier = Modifier, - verificationState = verificationState, - clubName = clubName, onNavigateBack = {}, - onNavigateToHome = { name -> println("Navigate to home with: $name") }, - onVerifyClubCode = { code -> - if (code == "123456") { - verificationState = ClubCodeState.SUCCESS - clubName = "메이커스팜" - } else { - verificationState = ClubCodeState.ERROR - } - }, - onErrorReset = { - verificationState = ClubCodeState.INPUT - } + onNavigateToHome = { } ) } \ No newline at end of file