Skip to content
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -225,3 +225,7 @@ fabric.properties
# 개인 프롬프트 파일
.daegeun
.daegeun/**

# oh-my-openagent files
.sisyphus
.sisyphus/**
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ class UserPreferencesDataStore @Inject constructor(
}
}

// TODO(@이대근): 도메인 모델로 격상 및 실제 서버에서 받는 사용자 데이터를 저장 가능하도록 형식 변경 2026.05.14.
data class UserData(
val gender: String? = null,
val birthYear: Int? = null,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.lyrics.feelin.core.data.repository

import android.util.Log
import com.lyrics.feelin.core.data.datasource.remote.AuthRemoteDataSource
import com.lyrics.feelin.core.data.datasource.remote.dto.exception.FeelinServerException
import com.lyrics.feelin.core.data.datasource.sdk.GoogleAuthDataSource
Expand All @@ -20,6 +21,7 @@ import kotlinx.coroutines.flow.StateFlow
import retrofit2.HttpException

private const val UNKNOWN_SERVER_ERROR_CODE = "-1"
private const val TAG = "AuthRepository"

sealed interface RestoreSessionResult {
data object Authenticated : RestoreSessionResult
Expand Down Expand Up @@ -212,36 +214,40 @@ class AuthRepository @Inject constructor(
* 로그아웃
*
* **플로우:**
* 1. Backend에 로그아웃 요청 (옵션)
* 1. Backend에 로그아웃 요청
* 2. SDK 로그아웃 (Kakao/Google)
* 3. AuthManager에서 토큰 삭제
* 3. 성공/실패 여부와 무관하게 AuthManager에서 토큰 삭제
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

iOS 앱에서는 로그아웃 엔드포인트를 호출하지 않고 로컬 토큰만 삭제하더라고요. 안드로이드가 해당 방식을 동일하게 따라가지는 않더라도 서버 실패를 무조건 로그아웃 실패로 취급할 필요는 없어 보여서 로그아웃 메소드의 세부 구현을 변경했습니다.

*/
@Suppress("ReturnCount") // TODO(@이대근): 구글 로그인 구현 이후 어노테이션 삭제 2025.10.04.
suspend fun logout(): Result<Unit> {
// 1. Backend 로그아웃
authRemoteDataSource.signOut().onFailure {
return Result.failure(exception = it)
}
suspend fun logout() {
val provider = authManager.oauthProvider.value

// 2. SDK 로그아웃
when (authManager.oauthProvider.value) {
OAuthProvider.KAKAO -> {
kakaoAuthDataSource.logout()
}
try {
authRemoteDataSource.signOut()
.onFailure { error ->
Log.w(TAG, "logout: Backend sign-out failed", error)
}

OAuthProvider.GOOGLE -> {
return Result.failure(exception = NotImplementedError("Google login not implemented yet"))
}
when (provider) {
OAuthProvider.KAKAO -> {
kakaoAuthDataSource.logout()
.onFailure { error ->
Log.w(TAG, "logout: Kakao SDK logout failed", error)
}
}

null -> {
return Result.failure(exception = IllegalStateException("OAuth provider is null"))
OAuthProvider.GOOGLE -> {
// MARK(@이대근): Google login not implemented yet 2025.10.04.
Unit
}

null -> {
Unit
}
}
} finally {
// Cancellation can skip Result.onFailure, but local session cleanup must still run.
authManager.clearTokens()
}

// 3. 토큰 삭제
authManager.clearTokens()

return Result.success(Unit)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// ========== 회원가입 ==========
Expand Down
30 changes: 27 additions & 3 deletions app/src/main/java/com/lyrics/feelin/navigation/FeelinNavHost.kt
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ import com.lyrics.feelin.core.designsystem.icon.NoteSearchingInactiveIcon
import com.lyrics.feelin.presentation.util.openExternalBrowser
import com.lyrics.feelin.presentation.view.community.CommunityMainScreen
import com.lyrics.feelin.presentation.view.login.LoginScreen
import com.lyrics.feelin.presentation.view.mypage.MyPageLogoutStatus
import com.lyrics.feelin.presentation.view.mypage.MyPageScreen
import com.lyrics.feelin.presentation.view.mypage.MyPageViewModel
import com.lyrics.feelin.presentation.view.mypage.setting.SettingScreen
import com.lyrics.feelin.presentation.view.mypage.userinfo.UserInfoScreen
import com.lyrics.feelin.presentation.view.note.search.NoteSearchScreen
Expand Down Expand Up @@ -285,27 +287,49 @@ private fun NavGraphBuilder.mainNavGraph(navController: NavHostController) {

private fun NavGraphBuilder.myPageNavGraph(navController: NavHostController) {
navigation(startDestination = FeelinDestination.MyPage.route, route = FeelinDestination.MyPageGraph.route) {
composable(FeelinDestination.MyPage.route) {
composable(FeelinDestination.MyPage.route) { backStackEntry ->
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

마이페이지, 설정 화면, 프로필 변경 화면 등이 동일한 사용자 정보를 공유하는 것을 고려해 마이페이지 뷰모델을 마이페이지 그래프의 공용 뷰모델로 사용하는 방향으로 정했습니다.

val parentEntry = remember(backStackEntry) {
navController.getBackStackEntry(FeelinDestination.MyPageGraph.route)
}
val viewModel: MyPageViewModel = hiltViewModel(parentEntry)

MainScaffold(navController = navController, selectedIndex = 2) {
MyPageScreen(
onSettingClick = { navController.navigate(FeelinDestination.Setting.route) }
onSettingClick = { navController.navigate(FeelinDestination.Setting.route) },
viewModel = viewModel,
)
}
}

composable(FeelinDestination.Setting.route) {
composable(FeelinDestination.Setting.route) { backStackEntry ->
val parentEntry = remember(backStackEntry) {
navController.getBackStackEntry(FeelinDestination.MyPageGraph.route)
}
val viewModel: MyPageViewModel = hiltViewModel(parentEntry)
val logoutStatus by viewModel.logoutStatus.collectAsState()
val context = LocalContext.current

LaunchedEffect(logoutStatus) {
if (logoutStatus == MyPageLogoutStatus.SUCCESS) {
viewModel.clearLogoutStatus()
navController.navigate(FeelinDestination.OnboardingGraph.route) {
popUpTo(FeelinDestination.MainGraph.route) { inclusive = true }
Comment on lines +312 to +316
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 설정 이탈 후에도 로그아웃 완료를 처리하세요

로그아웃 완료 내비게이션을 Setting destination 내부에서만 관찰하면, 로그아웃 요청 후 네트워크가 느린 동안 사용자가 시스템 Back으로 설정 화면을 벗어난 경우 이 LaunchedEffect가 dispose되어 SUCCESS 상태를 처리할 곳이 없어집니다. 이때 토큰과 사용자 DataStore는 이미 지워졌지만 사용자는 메인 그래프에 남아 하단 탭을 계속 사용할 수 있으므로, 로그아웃 상태 관찰/내비게이션을 MyPageGraph처럼 설정 화면 생명주기에 묶이지 않는 위치로 올리거나 로딩 중 Back을 막아야 합니다.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

@gdaegeun539 gdaegeun539 May 18, 2026

Choose a reason for hiding this comment

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

SCRUM-276 의 전역 로딩 인디케이터에서 뒤로가기 버튼/제스처를 막도록 같이 구현할 예정.

}
}
}
Comment thread
gdaegeun539 marked this conversation as resolved.

MainScaffold(navController = navController, selectedIndex = 2) {
SettingScreen(
onBackClick = { navController.popBackStack() },
onUserInfoClick = { navController.navigate(FeelinDestination.UserInfo.route) },
onLogoutClick = viewModel::logout,
onInternalWebViewClick = { url ->
navController.navigate(FeelinDestination.InternalWebView.createRoute(url))
},
onExternalBrowserClick = { url ->
context.openExternalBrowser(url)
},
isLogoutLoading = logoutStatus == MyPageLogoutStatus.LOADING,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ val DarkBackgroundTertiary = Color(0xFF2A2A2E)
val LightSystemModal = Color(0xFFFFFFFF)
val DarkSystemModal = Color(0xFF494955)

val CommonSystemDim = Color(0x10122366)
val CommonSystemDim = Color(0x66101223)

val CommonPoint = Color(0xFFFA5454)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ enum class MyPageTabScreenStatus {
ERROR,
}

enum class MyPageLogoutStatus {
IDLE,
LOADING,
SUCCESS,
}

data class MyPageScreenState(
val status: MyPageScreenStatus,
val tabStatus: MyPageTabScreenStatus,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.lyrics.feelin.presentation.view.mypage

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.lyrics.feelin.core.data.repository.AuthRepository
import com.lyrics.feelin.core.data.repository.UserRepository
import com.lyrics.feelin.core.domain.model.ProfileType
import dagger.hilt.android.lifecycle.HiltViewModel
Expand All @@ -14,11 +15,15 @@ import kotlinx.coroutines.launch

@HiltViewModel
class MyPageViewModel @Inject constructor(
private val authRepository: AuthRepository,
private val userRepository: UserRepository
) : ViewModel() {
private val _myPageScreenStatus: MutableStateFlow<MyPageScreenState> = MutableStateFlow(MyPageScreenState.initial())
val myPageScreenState: StateFlow<MyPageScreenState> = _myPageScreenStatus.asStateFlow()

private val _logoutStatus: MutableStateFlow<MyPageLogoutStatus> = MutableStateFlow(MyPageLogoutStatus.IDLE)
val logoutStatus: StateFlow<MyPageLogoutStatus> = _logoutStatus.asStateFlow()

fun loadMyPageData() {
viewModelScope.launch {
val userData = userRepository.userData.first()
Expand Down Expand Up @@ -56,6 +61,22 @@ class MyPageViewModel @Inject constructor(
}
}

fun logout() {
if (_logoutStatus.value == MyPageLogoutStatus.LOADING) return

viewModelScope.launch {
_logoutStatus.value = MyPageLogoutStatus.LOADING
authRepository.logout()
userRepository.logout()
_myPageScreenStatus.value = _logoutSample
_logoutStatus.value = MyPageLogoutStatus.SUCCESS
Comment thread
gdaegeun539 marked this conversation as resolved.
Comment thread
gdaegeun539 marked this conversation as resolved.
}
}

fun clearLogoutStatus() {
_logoutStatus.value = MyPageLogoutStatus.IDLE
}

private val _logoutSample = MyPageScreenState(
status = MyPageScreenStatus.SUCCESS_LOAD,
tabStatus = MyPageTabScreenStatus.SUCCESS_LOAD,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,18 @@ fun SettingInfoItem(
title: String,
modifier: Modifier = Modifier,
titleColor: Color? = null,
onClick: (() -> Unit)? = null,
trailingContent: @Composable (RowScope.() -> Unit)? = null,
) {
val feelinColors = LocalFeelinColors.current
val rowModifier = if (onClick != null) {
modifier.clickable(onClick = onClick)
} else {
modifier
}

MyPageItemRowShell(
modifier = modifier,
modifier = rowModifier,
leadingContent = {
Text(
text = title,
Expand Down
Loading
Loading