Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.whosin.client.data.dto.request

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class LogoutRequestDto(
@SerialName("refreshToken")
val refreshToken: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.whosin.client.data.dto.response

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class LogoutResponseDto(
@SerialName("success")
val success: Boolean,
@SerialName("status")
val status: Int,
@SerialName("message")
val message: String,
@SerialName("data")
val data: String? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ import org.whosin.client.data.dto.request.EmailValidationRequestDto
import org.whosin.client.data.dto.request.EmailVerificationRequestDto
import org.whosin.client.data.dto.request.FindPasswordRequestDto
import org.whosin.client.data.dto.request.LoginRequestDto
import org.whosin.client.data.dto.request.LogoutRequestDto
import org.whosin.client.data.dto.request.SignupRequestDto
import org.whosin.client.data.dto.response.EmailVerificationResponseDto
import org.whosin.client.data.dto.response.ErrorResponseDto
import org.whosin.client.data.dto.response.FindPasswordResponseDto
import org.whosin.client.data.dto.response.LoginResponseDto
import org.whosin.client.data.dto.response.LogoutResponseDto
import org.whosin.client.data.dto.response.SignupResponseDto

class RemoteAuthDataSource(
Expand Down Expand Up @@ -161,4 +163,42 @@ class RemoteAuthDataSource(
ApiResult.Error(message = t.message, cause = t)
}
}

suspend fun logout(refreshToken: String): ApiResult<LogoutResponseDto> {
return try {
val response: HttpResponse = client
.post("auth/logout") {
setBody(
LogoutRequestDto(
refreshToken = refreshToken
)
)
}
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)
}
}

// TODO: 회원탈퇴
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import org.whosin.client.data.dto.response.LoginResponseDto
import org.whosin.client.data.dto.response.EmailVerificationResponseDto
import org.whosin.client.data.dto.response.SignupResponseDto
import org.whosin.client.data.dto.response.FindPasswordResponseDto
import org.whosin.client.data.dto.response.LogoutResponseDto

class AuthRepository(
private val dataSource: RemoteAuthDataSource
Expand Down Expand Up @@ -37,4 +38,7 @@ class AuthRepository(
suspend fun sendPasswordResetEmail(email: String): ApiResult<FindPasswordResponseDto> =
dataSource.sendPasswordResetEmail(email)

suspend fun logout(refreshToken: String): ApiResult<LogoutResponseDto> =
dataSource.logout(refreshToken)

}
Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
package org.whosin.client.presentation.mypage

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
Expand All @@ -25,6 +35,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import org.jetbrains.compose.ui.tooling.preview.Preview
import org.koin.compose.viewmodel.koinViewModel
import org.whosin.client.data.dto.response.ClubData
import org.whosin.client.presentation.component.CommonBackHandler
import org.whosin.client.presentation.mypage.component.MyClubComponent
import org.whosin.client.presentation.mypage.component.MyPageButton
Expand All @@ -43,6 +54,8 @@ fun MyPageScreen(
) {
val viewModel: MyPageViewModel = koinViewModel()
val uiState = viewModel.uiState.collectAsStateWithLifecycle().value

var showDeleteDialog by remember { mutableStateOf(false) }

// MyPage로 돌아올 때마다 수정 모드 해제 및 데이터 새로고침
LaunchedEffect(Unit) {
Expand All @@ -61,14 +74,17 @@ fun MyPageScreen(
}
}

Box(
Column(
modifier = modifier
.fillMaxSize()
.background(Color.White)
.padding(top = 16.dp, start = 16.dp, end = 16.dp)
) {
Column(
modifier = Modifier.fillMaxWidth()
modifier = Modifier
.fillMaxWidth()
.weight(1f) // MyPageButton 위 공간만 사용
.verticalScroll(rememberScrollState())
) {
MyPageTopAppBar(onNavigateBack)
Spacer(modifier = Modifier.size(16.dp))
Expand Down Expand Up @@ -113,19 +129,57 @@ fun MyPageScreen(
// 내 동아리 / 학과 목록
MyClubComponent(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.padding(bottom = 72.dp),
.fillMaxWidth(),
isEditable = uiState.isEditable,
myClubs = uiState.clubs,
onDeleteClub = { clubId ->
viewModel.deleteClub(clubId)
},
onNavigateToAddClub = onNavigateToAddClub
)
Spacer(modifier = Modifier.size(24.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 32.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp)
){
Button(
modifier = Modifier.weight(1f),
shape = RoundedCornerShape(10.dp),
onClick = {
viewModel.logout()
},
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFF5F5F5),
contentColor = Color.Black
)
){
Text(
text = "로그아웃",
fontWeight = FontWeight.Medium
)
}
Button(
modifier = Modifier.weight(1f),
shape = RoundedCornerShape(10.dp),
onClick = {
showDeleteDialog = true
},
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFFF3636),
contentColor = Color.White
)
){
Text(
text = "회원 탈퇴",
fontWeight = FontWeight.Medium
)
}
}
}

// 내 정보 수정 버튼 - 하단에 고정
// 내 정보 수정 버튼
MyPageButton(
onClick = {
if (uiState.isEditable) {
Expand All @@ -139,10 +193,63 @@ fun MyPageScreen(
),
enabled = uiState.nickname.isNotEmpty(),
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.padding(bottom = 52.dp)
)
}

// 회원 탈퇴 확인 다이얼로그
if (showDeleteDialog) {
AlertDialog(
containerColor = Color(0xFFFFFFFF),
onDismissRequest = { showDeleteDialog = false },
title = {
Text(
text = "회원 탈퇴",
fontWeight = FontWeight.SemiBold,
fontSize = 18.sp
)
},
text = {
Text(
text = "탈퇴 후에는 모든 데이터가 삭제되며 복구할 수 없습니다.",
fontSize = 16.sp
)
},
confirmButton = {
Button(
shape = RoundedCornerShape(10.dp),
onClick = {
showDeleteDialog = false
// TODO: 회원 탈퇴 api 연결
},
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFFF3636),
contentColor = Color.White
)
) {
Text(
text = "확인",
fontWeight = FontWeight.Medium
)
}
},
dismissButton = {
OutlinedButton(
shape = RoundedCornerShape(10.dp),
onClick = { showDeleteDialog = false },
colors = ButtonDefaults.outlinedButtonColors(
contentColor = Color.Black
)
) {
Text(
text = "취소",
fontWeight = FontWeight.Medium
)
}
}
)
}

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.whosin.client.core.auth.TokenExpiredManager
import org.whosin.client.core.datastore.TokenManager
import org.whosin.client.core.network.ApiResult
import org.whosin.client.data.dto.response.ClubData
import org.whosin.client.data.repository.AuthRepository
import org.whosin.client.data.repository.MemberRepository

data class MyPageUiState(
Expand All @@ -20,7 +23,9 @@ data class MyPageUiState(
)

class MyPageViewModel(
private val repository: MemberRepository
private val memberRepository: MemberRepository,
private val authRepository: AuthRepository,
private val tokenManager: TokenManager
): ViewModel() {
private val _uiState = MutableStateFlow(MyPageUiState())
val uiState: StateFlow<MyPageUiState> = _uiState.asStateFlow()
Expand All @@ -43,7 +48,7 @@ class MyPageViewModel(
fun getMyInfo() {
viewModelScope.launch {
_uiState.update{ it.copy(isLoading = true) }
when (val result = repository.getMyInfo()) {
when (val result = memberRepository.getMyInfo()) {
is ApiResult.Success -> {
val response = result.data.data
_uiState.update { it ->
Expand Down Expand Up @@ -79,7 +84,7 @@ class MyPageViewModel(
val newClubs = clubList?.map {
it.clubId
}
when (val result = repository.updateMyInfo(newNickName = newNickName, clubList = newClubs)) {
when (val result = memberRepository.updateMyInfo(newNickName = newNickName, clubList = newClubs)) {
is ApiResult.Success -> {
_uiState.update {
it.copy(isEditable = false)
Expand Down Expand Up @@ -108,4 +113,33 @@ class MyPageViewModel(
println("MyPageViewModel: 클럽 삭제 - clubId: $clubId")
}

// 로그아웃
fun logout(){
viewModelScope.launch {
val refreshToken = tokenManager.getRefreshToken()
if (refreshToken.isNullOrEmpty()) {
// 리프레시 토큰이 없으면 바로 토큰 삭제 및 로그인 화면으로 이동
tokenManager.clearToken()
TokenExpiredManager.setTokenExpired()
return@launch
}

when (val result = authRepository.logout(refreshToken)) {
is ApiResult.Success -> {
println("MyPageViewModel: 로그아웃 성공")
// 토큰 삭제 및 로그인 화면으로 이동
tokenManager.clearToken()
TokenExpiredManager.setTokenExpired()
}
is ApiResult.Error -> {
_uiState.value = _uiState.value.copy(
errorMessage = result.message ?: "로그아웃에 실패했습니다."
)
println("MyPageViewModel: 로그아웃 실패 - ${result.message}")
}
}
}
}

// TODO: 회원 탈퇴
}
Loading