Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
0528152
feat: 리소스 수정
seunghee17 Mar 11, 2026
a45c519
feat: Navigation 배경색상 수정
seunghee17 Mar 11, 2026
cf95238
feat: 설정화면 리소스 수정
seunghee17 Mar 11, 2026
2d52f23
feat: 설정 -> 마이페이지 명칭 수정
seunghee17 Mar 21, 2026
e685417
feat: 앱 아이콘 추가
seunghee17 Mar 21, 2026
a300b3c
feat: 시스템바 여백 적용해서 ui 그리도록 수정
seunghee17 Mar 21, 2026
f16c832
feat: 이력서 제출가능 여부 검증 추가
seunghee17 Mar 21, 2026
83fb98b
feat: 이력서 임시 저장 함수 suspend 처리
seunghee17 Mar 21, 2026
f41b1e1
feat: 생년월일 현 날짜 초과하지 못하게 수정
seunghee17 Mar 21, 2026
28f563b
feat: 자격증 chip간 간격 수정
seunghee17 Mar 21, 2026
a04a8d7
feat: 이력서 제출여부 관찰하고 상태 업데이트하는 구조 수정
seunghee17 Mar 21, 2026
9d035f6
feat: 근무 시작날짜 현재 날짜 초과하지 않도록 조건 추가
seunghee17 Mar 21, 2026
e045c49
feat: 이력서 작성 3단계 화면 스크롤 추가
seunghee17 Mar 21, 2026
ddedb8e
feat: 이력서 리스트 정렬수정
seunghee17 Mar 21, 2026
8c54035
feat: 로그아웃 버튼추가
seunghee17 Mar 21, 2026
9e4c5cc
feat: 로그아웃시 세션 정리 및 화면 이동
seunghee17 Mar 21, 2026
2c54a97
feat: 다운로드 앱바 추가
seunghee17 Mar 21, 2026
7973fcb
feat: resume 상태에서 로그인 상태 갱신하도록 수정
seunghee17 Mar 21, 2026
b2ca4c3
feat: 경력 작성 화면 스크롤 가능하도록 수정
seunghee17 Mar 21, 2026
0adafee
feat: 총 경력 계산로직 수정
seunghee17 Mar 21, 2026
16a078b
feat: 로그아웃 요청 api 주소 수정
seunghee17 Mar 22, 2026
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
3 changes: 2 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -73,5 +73,6 @@ dependencies {
implementation(project(":feature:download:impl"))
implementation(project(":feature:resume:impl"))
implementation(project(":feature:resume_view:impl"))
implementation(project(":feature:setting"))
implementation(project(":feature:mypage:api"))
implementation(project(":feature:mypage:impl"))
}
10 changes: 7 additions & 3 deletions app/src/main/java/com/nonggle/nonggleresume/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material3.Surface
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
Expand Down Expand Up @@ -97,7 +99,10 @@ class MainActivity : ComponentActivity() {
darkTheme = themeSettings.darkTheme,
) {
Surface(
modifier = Modifier.fillMaxSize(),
modifier = Modifier
.fillMaxSize()
.systemBarsPadding()
.statusBarsPadding(),
color = NonggleTheme.colorScheme.white
) {
if (isLoggedIn) {
Expand Down Expand Up @@ -133,8 +138,7 @@ class MainActivity : ComponentActivity() {

override fun onResume() {
super.onResume()
/// TODO: loading 상태 true로 업데이트
/// TODO: 백그라운드 -> 포그라운드 전환일 경우 로그인 유효성을 미리 확인
viewModel.onResume()
}

override fun onPause() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import javax.inject.Inject

Expand All @@ -17,7 +18,6 @@ import javax.inject.Inject
class MainActivityViewModel @Inject constructor(
authEventBus: AuthEventBus,
private val loginRepository: LoginRepository,
// TODO: Add a UseCase to check the initial login status
) : ViewModel() {
private val _isLoggedIn = MutableStateFlow(false)
val isLoggedIn: StateFlow<Boolean> = _isLoggedIn.asStateFlow()
Expand All @@ -26,29 +26,39 @@ class MainActivityViewModel @Inject constructor(
val uiState: StateFlow<MainActivityUiState> = _uiState

init {
refreshLoginState()

viewModelScope.launch {
loginRepository.isLoggedIn().collect { isLoggedIn ->
_isLoggedIn.value = isLoggedIn
}
}
// Listen for session expiration events
viewModelScope.launch {
/// TODO: 로그인 상태 확인 -> 토큰이 정상적으로 존재하는지 확인
// _isLoggedIn.value = authRepository.isLoggedIn()
authEventBus.events.collect { event ->
if (event is AuthEvent.SessionExpired) {
loginRepository.logOut() // 토큰 삭제
_isLoggedIn.value = false // 이것만으로 로그인 화면 전환 + backstack 자동 소멸
when (event) {
AuthEvent.SessionExpired -> {
loginRepository.logOut()
_isLoggedIn.value = false
}

AuthEvent.LoggedOut -> {
_isLoggedIn.value = false
}
}
}
}

_uiState.value = MainActivityUiState.Success
}

fun onLoginSuccess() {
_isLoggedIn.value = true
}

fun onResume() {
refreshLoginState()
}

private fun refreshLoginState() {
viewModelScope.launch {
_uiState.value = MainActivityUiState.Loading
_isLoggedIn.value = loginRepository.isLoggedIn().first()
_uiState.value = MainActivityUiState.Success
}
}
Comment on lines +55 to +61
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

onResume()마다 Loading 상태로 전환되어 UX 저하 가능성이 있습니다.

앱이 백그라운드에서 복귀할 때마다 _uiStateLoading으로 설정되면 스플래시 화면이 잠깐 다시 표시될 수 있습니다. 초기 로딩 시에만 Loading 상태를 사용하거나, 이미 Success 상태인 경우 Loading으로 전환하지 않는 로직이 필요합니다.

또한, loginRepository.isLoggedIn().first()가 DataStore 등 I/O를 수반한다면 Main dispatcher에서 실행될 수 있으므로, repository 내부에서 적절한 dispatcher 전환이 이루어지는지 확인이 필요합니다.

💡 제안된 수정
     private fun refreshLoginState() {
         viewModelScope.launch {
-            _uiState.value = MainActivityUiState.Loading
+            // 초기 로딩 시에만 Loading 표시
+            if (_uiState.value == MainActivityUiState.Loading) {
+                // 이미 Loading 상태면 유지
+            }
             _isLoggedIn.value = loginRepository.isLoggedIn().first()
             _uiState.value = MainActivityUiState.Success
         }
     }

또는 초기화와 resume 로직을 분리:

private fun refreshLoginState(showLoading: Boolean = false) {
    viewModelScope.launch {
        if (showLoading) {
            _uiState.value = MainActivityUiState.Loading
        }
        _isLoggedIn.value = loginRepository.isLoggedIn().first()
        _uiState.value = MainActivityUiState.Success
    }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/nonggle/nonggleresume/MainActivityViewModel.kt` around
lines 55 - 61, The refreshLoginState function currently unconditionally sets
_uiState to Loading which causes a flash on every resume; change
refreshLoginState (and callers) to accept a showLoading: Boolean = false and
only set _uiState = MainActivityUiState.Loading when showLoading is true or when
current _uiState is not already Success, then proceed to read
loginRepository.isLoggedIn().first() and set _uiState =
MainActivityUiState.Success; also ensure the potentially blocking
loginRepository.isLoggedIn().first() runs off the Main thread by either making
the repository switch dispatchers internally or wrapping the call in
withContext(Dispatchers.IO) inside the coroutine to avoid main-thread I/O.

}

sealed interface MainActivityUiState {
Expand All @@ -58,9 +68,6 @@ sealed interface MainActivityUiState {
override fun shouldUseDarkTheme(isSystemDarkTheme: Boolean) = isSystemDarkTheme
}

// 스플래시 화면 상태를 유지해야하는지에 대한 여부
fun shouldKeepSplashScreen() = this is Loading

// 다크 테마 사용 필요성
fun shouldUseDarkTheme(isSystemDarkTheme: Boolean) = isSystemDarkTheme
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import androidx.compose.ui.res.stringResource
import com.nonggle.designsystem.icon.NonggleIcons
import com.nonggle.api.HomeNavKey
import com.nonggle.nonggleresume.R
import com.nonggle.setting.navigation.SettingNavKey
import com.nonggle.feature.download.api.DownLoadNavKey
import com.nonggle.mypage.api.MyPageNavKey

data class TopLevelNavItem(
@DrawableRes val selectedIconRes: Int,
Expand All @@ -27,7 +27,7 @@ val DOWNLOAD = TopLevelNavItem(
title = { stringResource(R.string.nav_item_download) }
)

val SETTING = TopLevelNavItem(
val MYPAGE = TopLevelNavItem(
selectedIconRes = NonggleIcons.settingSelected,
unselectedIconRes = NonggleIcons.settingUnselected,
title = { stringResource(R.string.nav_item_setting) }
Expand All @@ -36,5 +36,5 @@ val SETTING = TopLevelNavItem(
val TOP_LEVEL_NAV_ITEMS = mapOf(
HomeNavKey to HOME,
DownLoadNavKey to DOWNLOAD,
SettingNavKey to SETTING,
MyPageNavKey to MYPAGE,
)
17 changes: 12 additions & 5 deletions app/src/main/java/com/nonggle/nonggleresume/ui/NonggleApp.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.nonggle.nonggleresume.ui

import android.util.Log
import android.widget.Toast
import com.nonggle.nonggleresume.R
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
Expand All @@ -19,7 +20,9 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.ui.NavDisplay
Expand All @@ -30,8 +33,8 @@ import com.nonggle.navigation.toEntries
import com.nonggle.feature.home.impl.navigation.homeEntryProvider
import com.nonggle.feature.resume_view.impl.navigation.resumeViewEntryProvider
import com.nonggle.nonggleresume.navigation.TOP_LEVEL_NAV_ITEMS
import com.nonggle.setting.navigation.settingEntryProvider
import com.nonggle.feature.download.impl.navigation.downLoadEntryProvider
import com.nonggle.mypage.impl.navigation.myPageEntryProvider
import com.nonggle.resume.impl.navigation.resumeEntryProvider

// 로그인 후에만 호출됨
Expand All @@ -42,9 +45,13 @@ internal fun NonggleApp(
appState: NonggleAppState,
) {
val isOffline by appState.isOffline.collectAsStateWithLifecycle()
val context = LocalContext.current
val offlineToastMessage = stringResource(R.string.offline_Message)

LaunchedEffect(isOffline) {/// TODO: 네트워크 미연결시 다이얼로그 처리
if (isOffline) Log.d("NOTCONNECT", "네트워크 미연결")
LaunchedEffect(isOffline) {
if (isOffline) {
Toast.makeText(context, offlineToastMessage, Toast.LENGTH_LONG).show()
}
}

val mainNavigator = remember { Navigator(appState.mainNavigationState) }
Expand Down Expand Up @@ -84,7 +91,7 @@ internal fun NonggleApp(
resumeEntryProvider(mainNavigator)
resumeViewEntryProvider(mainNavigator)
downLoadEntryProvider(mainNavigator)
settingEntryProvider(mainNavigator)
myPageEntryProvider(mainNavigator)
}

NavDisplay(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,4 @@ class NonggleAppState(
started = SharingStarted.WhileSubscribed(5_000),
initialValue = false
)

}
4 changes: 3 additions & 1 deletion app/src/main/res/drawable/ic_launcher_background.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">

<path
android:fillColor="#3DDC84"
android:fillColor="#FFFFFF"
android:pathData="M0,0h108v108h-108z" />

<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
Expand Down
47 changes: 22 additions & 25 deletions app/src/main/res/drawable/ic_launcher_foreground.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,26 @@
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
android:viewportWidth="130"
android:viewportHeight="130">
<group android:scaleX="0.6"
android:scaleY="0.6"
android:translateX="26"
android:translateY="26">
<path
android:pathData="M53.2944 30.8049C53.2944 25.7582 49.2032 21.667 44.1565 21.667C39.1097 21.667 35.0186 25.7582 35.0186 30.8049V34.2553C35.0186 39.3021 39.1097 43.3932 44.1565 43.3932C49.2032 43.3932 53.2944 39.3021 53.2944 34.2553V30.8049Z"
android:fillColor="#326B48" />
<path
android:pathData="M78.3033 34.2556C78.3033 39.3024 82.3945 43.3936 87.4412 43.3936C92.4879 43.3936 96.5791 39.3024 96.5791 34.2556V30.8052C96.5791 25.7585 92.4879 21.6673 87.4412 21.6673C82.3945 21.6673 78.3033 25.7585 78.3033 30.8052V34.2556Z"
android:fillColor="#326B48" />
<path
android:pathData="M61.6525 71.2404C43.6583 54.2591 14.7929 52.8129 10.335 52.6937C10.01 52.6829 9.75 52.9429 9.75 53.2679C9.80417 58.782 10.6817 64.1012 12.2417 69.117C27.1808 67.7574 41.7571 75.2595 55.6508 82.5233C56.5121 80.6653 57.6658 78.9754 58.6463 77.5237C59.4317 76.3645 60.5258 74.8154 61.8042 73.1633C62.2537 72.5783 62.1833 71.7495 61.6471 71.2458L61.6525 71.2404Z"
android:fillColor="#326B48" />
<path
android:pathData="M71.3538 93.0591C69.0896 95.502 66.2404 96.8182 63.4075 96.8182C62.6059 96.8182 61.8042 96.7153 61.0188 96.4987C57.8013 95.6157 55.2717 92.9887 54.5621 89.8037C53.9934 87.2253 54.6163 84.7553 55.6509 82.5182C41.7517 75.2545 27.1809 67.7524 12.2417 69.112C18.2325 88.4062 34.4879 103.177 54.6271 107.034C54.7625 107.121 54.9196 107.186 55.0821 107.218C58.3267 107.809 61.6742 108.123 65.0921 108.123C78.0109 108.123 89.895 103.692 99.3092 96.2766C90.3446 95.7837 81.4342 94.3049 73.0654 90.9845C72.5346 91.7103 71.9604 92.4091 71.3538 93.0645V93.0591Z"
android:fillColor="#49845D" />
<path
android:pathData="M119.074 52.8187C113.067 53.0678 88.3727 54.9366 71.2019 68.9874C67.5727 72.0478 63.5102 77.6487 62.0315 79.8262C61.0781 81.2291 60.0056 82.8053 59.2798 84.4195C58.6027 85.9307 58.229 87.4691 58.554 88.9207C58.9277 90.6162 60.3523 92.0732 62.0911 92.5499C65.0702 93.3678 67.459 91.2337 68.3419 90.2749C68.629 89.9662 68.9052 89.6412 69.1761 89.3107C70.7252 87.4041 71.9277 85.1995 72.6102 82.9841C72.9406 81.9007 74.089 81.2941 75.1669 81.6299C76.2502 81.9603 76.8569 83.1087 76.5211 84.1866C75.7952 86.5482 74.5927 88.8882 73.0598 90.9845C81.4286 94.3049 90.339 95.7837 99.3036 96.2766C111.843 86.402 120.006 71.2245 120.412 54.1187C120.429 53.382 119.811 52.7862 119.074 52.8187Z"
android:fillColor="#326B48" />
</group>
</vector>
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>
Binary file modified app/src/main/res/mipmap-hdpi/ic_launcher.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified app/src/main/res/mipmap-mdpi/ic_launcher.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
<string name="app_name">농글농글</string>
<string name="nav_item_home">홈</string>
<string name="nav_item_download">다운로드</string>
<string name="nav_item_setting">설정</string>
<string name="nav_item_setting">마이페이지</string>
<string name="offline_Message">농글 서비스에 연결할 수 없습니다. 네트워크 상태를 확인해주세요.</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,45 @@ class MainActivityViewModelTest {
coVerify(exactly = 1) { loginRepository.logOut() }
assertFalse(viewModel.isLoggedIn.value)
}

@Test
fun `LoggedOut 이벤트 수신시 isLoggedIn을 false로 변경하고 logOut을 다시 호출하지 않는다`() = runTest {
coEvery { loginRepository.isLoggedIn() } returns flowOf(true)

val viewModel = MainActivityViewModel(
authEventBus = authEventBus,
loginRepository = loginRepository,
)

advanceUntilIdle()

authEventBus.emit(AuthEvent.LoggedOut)

advanceUntilIdle()

coVerify(exactly = 0) { loginRepository.logOut() }
assertFalse(viewModel.isLoggedIn.value)
}

@Test
fun `onResume 호출시 로그인 상태를 다시 확인해 isLoggedIn을 갱신한다`() = runTest {
coEvery { loginRepository.isLoggedIn() } returnsMany listOf(
flowOf(true),
flowOf(false)
)

val viewModel = MainActivityViewModel(
authEventBus = authEventBus,
loginRepository = loginRepository,
)

advanceUntilIdle()
assertFalse(viewModel.uiState.value is MainActivityUiState.Loading)

viewModel.onResume()

advanceUntilIdle()

assertFalse(viewModel.isLoggedIn.value)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import javax.inject.Singleton

sealed interface AuthEvent {
data object SessionExpired : AuthEvent
data object LoggedOut : AuthEvent
}

interface AuthEventBus {
Expand All @@ -23,4 +24,4 @@ class DefaultAuthEventBus @Inject constructor() : AuthEventBus {
override suspend fun emit(event: AuthEvent) {
_events.emit(event)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,13 @@ class LoginRepositoryImpl @Inject constructor(
}

override suspend fun logOut(): AppResult<Unit> {
/// TODO: 로그아웃 구현
return AppResult.Success(Unit)
val response = loginService.logout()
tokenManager.deleteToken()

return when(response) {
is AppResult.Success -> AppResult.Success(Unit)
is AppResult.Error -> response
}
}
Comment on lines 44 to 52
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: 로그아웃 관련 다른 구현이나 주석을 확인하여 의도된 동작 파악

rg -n -C3 "logOut|logout|deleteToken" --type=kt

Repository: Nonggle/Android

Length of output: 85


🏁 Script executed:

#!/bin/bash
# Kotlin 파일에서 logout, logOut, deleteToken 패턴 검색
rg -n "logOut|logout|deleteToken" --type-list | head -5

Repository: Nonggle/Android

Length of output: 448


🏁 Script executed:

#!/bin/bash
# .kt 확장자 파일에서 logout 관련 코드 검색
fd -e kt | head -20

Repository: Nonggle/Android

Length of output: 1480


🏁 Script executed:

#!/bin/bash
# LoginRepository 관련 파일 찾기
fd -e kt "LoginRepository"

Repository: Nonggle/Android

Length of output: 212


🏁 Script executed:

#!/bin/bash
# core/data 디렉토리의 구조와 파일 확인
find core/data -name "*.kt" -type f | grep -i login

Repository: Nonggle/Android

Length of output: 276


🏁 Script executed:

#!/bin/bash
# LoginRepositoryImpl.kt 파일 내용 확인
cat core/data/src/main/java/com/nonggle/data/repositoryimpl/LoginRepositoryImpl.kt

Repository: Nonggle/Android

Length of output: 1868


🏁 Script executed:

#!/bin/bash
# LoginRepository 인터페이스 확인
cat core/domain/src/main/java/com/nonggle/domain/repository/LoginRepository.kt

Repository: Nonggle/Android

Length of output: 401


🏁 Script executed:

#!/bin/bash
# 로그아웃 관련 테스트 케이스 검색
find . -name "*Test.kt" -o -name "*Spec.kt" | xargs grep -l "logOut\|logout" 2>/dev/null

Repository: Nonggle/Android

Length of output: 133


🏁 Script executed:

#!/bin/bash
# 로그아웃 호출 코드 찾기
rg "logOut\(\)" -A3 -B3

Repository: Nonggle/Android

Length of output: 5368


🏁 Script executed:

#!/bin/bash
# TokenManager의 deleteToken 구현 확인
fd -e kt -path "*TokenManager*"

Repository: Nonggle/Android

Length of output: 227


🏁 Script executed:

#!/bin/bash
# MainActivityViewModel 전체 코드 확인
cat app/src/main/java/com/nonggle/nonggleresume/MainActivityViewModel.kt

Repository: Nonggle/Android

Length of output: 2247


🏁 Script executed:

#!/bin/bash
# TokenManager 파일 찾기
find . -name "*TokenManager*" -type f

Repository: Nonggle/Android

Length of output: 180


🏁 Script executed:

#!/bin/bash
# AppResult 타입 확인
rg "class AppResult|sealed class AppResult" -A5

Repository: Nonggle/Android

Length of output: 41


🏁 Script executed:

#!/bin/bash
# TokenManager 인터페이스 확인
cat core/auth/src/main/java/com/nonggle/auth/TokenManager.kt

Repository: Nonggle/Android

Length of output: 460


🏁 Script executed:

#!/bin/bash
# TinkTokenManager 구현 확인
cat core/auth/src/main/java/com/nonggle/auth/TinkTokenManager.kt

Repository: Nonggle/Android

Length of output: 2456


🏁 Script executed:

#!/bin/bash
# AppResult 정의 찾기
find . -name "*.kt" -type f | xargs grep -l "sealed class AppResult\|class AppResult" | head -5

Repository: Nonggle/Android

Length of output: 41


서버 로그아웃 실패 시에도 토큰 삭제 - 설계 의도는 명확하나, 에러 처리 개선 필요

현재 구현은 loginService.logout() 결과와 무관하게 항상 tokenManager.deleteToken()을 호출합니다. 코드 분석 결과 이는 의도된 동작입니다:

  • MainActivityViewModel에서 SessionExpired 이벤트 발생 시 logOut() 반환값을 무시하고 즉시 로컬 상태를 업데이트
  • 세션 만료 시나리오에서는 토큰이 이미 서버에서 무효화되었으므로 로컬만 정리하면 됨

다만 개선이 필요한 부분:

  1. 에러 상황의 혼동성: 서버 로그아웃이 실패(AppResult.Error)해도 토큰은 이미 삭제됨. 호출자가 재시도하려 해도 토큰이 없어서 불가능
  2. 비대칭 패턴: login()은 성공 시에만 저장하지만, logOut()은 실패해도 삭제 - 의도가 코드에서 드러나지 않음

개선 방안:

  • deleteToken() 호출을 성공 여부 확인 후로 이동하거나
  • 에러 무시 의도를 명확히 문서화하고, 에러 발생 시에도 토큰이 이미 삭제된 상태임을 처리
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/data/src/main/java/com/nonggle/data/repositoryimpl/LoginRepositoryImpl.kt`
around lines 44 - 52, The current logOut in LoginRepositoryImpl always calls
tokenManager.deleteToken() regardless of loginService.logout() result; change
logOut so it first calls val response = loginService.logout(), then only call
tokenManager.deleteToken() when response is AppResult.Success (i.e., on
successful server logout) and return AppResult.Success(Unit); for the
AppResult.Error branch, return the error without deleting the token (or if you
intentionally want to always delete, add a clear comment and change the return
to wrap the logout error with a marker that the token was deleted); key symbols:
LoginRepositoryImpl.logOut, loginService.logout(), tokenManager.deleteToken(),
MainActivityViewModel.SessionExpired.


}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ import javax.inject.Singleton
@Singleton
class ResumeDraftStore @Inject constructor(): ResumeDraftStoreInterface {
private val _draft = MutableStateFlow(ResumeWritingModel())
val draft: StateFlow<ResumeWritingModel> = _draft
override val draft: StateFlow<ResumeWritingModel> = _draft

override fun update(reducer: (ResumeWritingModel) -> ResumeWritingModel) {
override suspend fun update(reducer: (ResumeWritingModel) -> ResumeWritingModel) {
_draft.update(reducer)
}

override fun snapshot(): ResumeWritingModel = _draft.value
override suspend fun snapshot(): ResumeWritingModel = _draft.value

}
}
Loading
Loading