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
8 changes: 8 additions & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"mcpServers": {
"figma-desktop": {
"type": "http",
"url": "http://127.0.0.1:3845/mcp"
}
}
}
Comment thread
lluke0 marked this conversation as resolved.
1 change: 1 addition & 0 deletions composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ kotlin {
implementation(libs.compose.runtime)
implementation(libs.kakao.sdk.v2.user)
implementation(libs.ktor.client.okhttp)
implementation(libs.androidx.core.splashscreen)
}
iosMain.dependencies {
implementation(libs.ktor.client.darwin)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,25 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import com.chukchukhaksa.mobile.common.provider.LocalAppContext
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import com.chukchukhaksa.mobile.common.provider.LocalAppContext

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen()
super.onCreate(savedInstanceState)

var isReady = false
splashScreen.setKeepOnScreenCondition { !isReady }

enableEdgeToEdge()
setContent {
val context = LocalContext.current
CompositionLocalProvider(LocalAppContext provides context) {
App()
App(onReady = { isReady = true })
Comment thread
lluke0 marked this conversation as resolved.
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M17.05,20.28C16.07,21.23 15,21.08 13.97,20.63C12.88,20.17 11.88,20.15 10.73,20.63C9.29,21.25 8.53,21.07 7.67,20.28C2.79,15.25 3.51,7.59 9.05,7.31C10.4,7.38 11.34,8.05 12.13,8.11C13.31,7.87 14.44,7.18 15.7,7.27C17.21,7.39 18.35,7.99 19.1,9.07C15.98,10.94 16.72,15.05 19.57,16.2C19.01,17.7 18.27,19.19 17.04,20.29L17.05,20.28ZM12.03,7.25C11.88,5.02 13.69,3.18 15.77,3C16.06,5.58 13.43,7.5 12.03,7.25Z"
android:fillColor="#FFFFFF"/>
</vector>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,3C6.477,3 2,6.463 2,10.691C2,13.392 3.833,15.783 6.585,17.166L5.588,20.69C5.513,20.952 5.816,21.163 6.044,21.006L10.298,18.213C10.855,18.303 11.423,18.382 12,18.382C17.523,18.382 22,14.919 22,10.691C22,6.463 17.523,3 12,3Z"
android:fillColor="#000000"/>
</vector>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
85 changes: 55 additions & 30 deletions composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,31 +27,46 @@ import com.chukchukhaksa.mobile.common.designsystem.theme.White
import com.chukchukhaksa.mobile.common.kmp.Platform.*
import com.chukchukhaksa.mobile.common.kmp.getPlatform
import com.chukchukhaksa.mobile.common.ui.collectWithLifecycle
import com.chukchukhaksa.mobile.domain.auth.usecase.CheckAuthStateUseCase
import com.chukchukhaksa.mobile.presentation.landing.navigation.LandingRoute
import com.chukchukhaksa.mobile.presentation.landing.navigation.landingNavGraph
import com.chukchukhaksa.mobile.presentation.timetable.navigation.TimetableRoute
import com.chukchukhaksa.mobile.presentation.timetable.navigation.timetableNavGraph
import dev.gitlive.firebase.Firebase
import dev.gitlive.firebase.analytics.FirebaseAnalytics
import dev.gitlive.firebase.analytics.analytics
import kotlinx.coroutines.delay
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.KoinContext
import org.koin.compose.koinInject
import org.koin.compose.viewmodel.koinViewModel

@Composable
fun App(
modifier: Modifier = Modifier,
viewModel: MainViewModel = koinViewModel(),
navigator: MainNavigator = rememberMainNavigator(),
onReady: () -> Unit = {},
) {
CchTheme {
KoinContext {
val uiState = viewModel.mviStore.uiState.collectAsState().value
val uriHandler = LocalUriHandler.current
val checkAuthStateUseCase: CheckAuthStateUseCase = koinInject()
var startDestination by remember { mutableStateOf<String?>(null) }
Comment on lines +55 to +56

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

๐Ÿงน Nitpick | ๐Ÿ”ต Trivial

koinInject๋ฅผ Composable ๋ณธ๋ฌธ์—์„œ ์ง์ ‘ ํ˜ธ์ถœํ•˜๋ฉด ๋ฆฌ์ปดํฌ์ง€์…˜๋งˆ๋‹ค ์ƒˆ ์ธ์Šคํ„ด์Šค๊ฐ€ ์ƒ์„ฑ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

CheckAuthStateUseCase๋Š” LaunchedEffect(Unit)์—์„œ ํ•œ ๋ฒˆ๋งŒ ์‚ฌ์šฉ๋˜์ง€๋งŒ, koinInject๊ฐ€ ๋ฆฌ์ปดํฌ์ง€์…˜๋  ๋•Œ๋งˆ๋‹ค ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค. remember๋กœ ๊ฐ์‹ธ๊ฑฐ๋‚˜ MainViewModel์— ์˜์กด์„ฑ์„ ์ฃผ์ž…ํ•˜๋Š” ๊ฒƒ์ด ๋” ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

โ™ป๏ธ ๊ถŒ์žฅ ์ˆ˜์ •์•ˆ
-            val checkAuthStateUseCase: CheckAuthStateUseCase = koinInject()
+            val checkAuthStateUseCase: CheckAuthStateUseCase = remember { koinInject() }
             var startDestination by remember { mutableStateOf<String?>(null) }

๋˜๋Š” MainViewModel์— CheckAuthStateUseCase๋ฅผ ์ฃผ์ž…ํ•˜๊ณ  ViewModel์—์„œ ์ธ์ฆ ์ƒํƒœ๋ฅผ ํ™•์ธํ•˜๋Š” ๊ฒƒ์ด ๋” ์ข‹์€ ์•„ํ‚คํ…์ฒ˜์ž…๋‹ˆ๋‹ค.

๐Ÿ“ Committable suggestion

โ€ผ๏ธ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
val checkAuthStateUseCase: CheckAuthStateUseCase = koinInject()
var startDestination by remember { mutableStateOf<String?>(null) }
val checkAuthStateUseCase: CheckAuthStateUseCase = remember { koinInject() }
var startDestination by remember { mutableStateOf<String?>(null) }
๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/App.kt` around
lines 55 - 56, koinInject์„ Composable ๋ณธ๋ฌธ์—์„œ ์ง์ ‘ ํ˜ธ์ถœํ•˜๋ฉด ๋ฆฌ์ปดํฌ์ง€์…˜๋งˆ๋‹ค ์ƒˆ ์ธ์Šคํ„ด์Šค๊ฐ€ ์ƒ์„ฑ๋  ์ˆ˜ ์žˆ์œผ๋‹ˆ,
CheckAuthStateUseCase ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ ์œ„์น˜๋ฅผ ๋ณ€๊ฒฝํ•˜์„ธ์š”: Composable ๋‚ด๋ถ€์—์„œ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ val
checkAuthStateUseCase by remember {
mutableStateOf(koinInject<CheckAuthStateUseCase>()) }์ฒ˜๋Ÿผ remember๋กœ ํ•œ ๋ฒˆ๋งŒ ์ƒ์„ฑํ•˜๊ฑฐ๋‚˜ ๋”
๊ถŒ์žฅ๋˜๋Š” ๋ฐฉ์‹์œผ๋กœ MainViewModel์— CheckAuthStateUseCase๋ฅผ ์ฃผ์ž…ํ•˜๊ณ  Composable์—์„œ๋Š” viewModel()์„
ํ†ตํ•ด MainViewModel์˜ ๊ณต๊ฐœ๋œ ๋ฉ”์„œ๋“œ/ํ”„๋กœํผํ‹ฐ๋ฅผ ์‚ฌ์šฉํ•ด LaunchedEffect(Unit)์—์„œ ์ธ์ฆ ์ƒํƒœ๋ฅผ ํ™•์ธํ•˜๋„๋ก ์ˆ˜์ •ํ•˜์„ธ์š”;
๊ด€๋ จ ์‹ฌ๋ณผ: koinInject, CheckAuthStateUseCase, LaunchedEffect(Unit), MainViewModel,
startDestination.


viewModel.mviStore.sideEffects.collectWithLifecycle { sideEffect ->
when (sideEffect) {
is MainSideEffect.OpenUrl -> uriHandler.openUri(sideEffect.url)
}
}

LaunchedEffect(Unit) {
val isAuthenticated = checkAuthStateUseCase().getOrDefault(false)
startDestination = if (isAuthenticated) TimetableRoute.route else LandingRoute.route
onReady()
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

LaunchedEffect(key1 = Unit) {
viewModel.checkNeedForceUpdate()

Expand All @@ -73,38 +88,48 @@ fun App(
contentWindowInsets = WindowInsets(0.dp),
modifier = modifier,
content = { innerPadding ->
val navGraphBuilder: NavGraphBuilder.() -> Unit = {
timetableNavGraph(
padding = innerPadding,
popBackStack = navigator::popBackStackIfNotHome,
navigateTimetableNameInput = navigator::navigateTimetableNameInput,
navigateTimetableEditor = navigator::navigateTimetableEditor,
navigateTimetableList = navigator::navigateTimetableList,
navigateOpenLecture = navigator::navigateOpenLecture,
handleException = viewModel::handleException,
onShowToast = viewModel::onShowToast,
navigateCellEditor = navigator::navigateCellEditor,
navigateSemesterSelect = navigator::navigateSemesterSelect,
navigateTimetable = navigator::navigateTimetable,
)
}
val currentStartDestination = startDestination

when (getPlatform()) {
Android -> NavHost(
navController = navigator.navController,
startDestination = navigator.startDestination,
enterTransition = { slideInHorizontally(tween(350, easing = FastOutSlowInEasing)) { it } },
exitTransition = { slideOutHorizontally(tween(350, easing = FastOutSlowInEasing)) { -it / 3 } },
popEnterTransition = { slideInHorizontally(tween(350, easing = FastOutSlowInEasing)) { -it / 3 } },
popExitTransition = { slideOutHorizontally(tween(350, easing = FastOutSlowInEasing)) { it } },
builder = navGraphBuilder,
)
if (currentStartDestination != null) {
val navGraphBuilder: NavGraphBuilder.() -> Unit = {
landingNavGraph(
handleException = viewModel::handleException,
onShowToast = viewModel::onShowToast,
navigateToHome = navigator::navigateFromLandingToHome,
)

IOS -> NavHost(
navController = navigator.navController,
startDestination = navigator.startDestination,
builder = navGraphBuilder,
)
timetableNavGraph(
padding = innerPadding,
popBackStack = navigator::popBackStackIfNotHome,
navigateTimetableNameInput = navigator::navigateTimetableNameInput,
navigateTimetableEditor = navigator::navigateTimetableEditor,
navigateTimetableList = navigator::navigateTimetableList,
navigateOpenLecture = navigator::navigateOpenLecture,
handleException = viewModel::handleException,
onShowToast = viewModel::onShowToast,
navigateCellEditor = navigator::navigateCellEditor,
navigateSemesterSelect = navigator::navigateSemesterSelect,
navigateTimetable = navigator::navigateTimetable,
)
}

when (getPlatform()) {
Android -> NavHost(
navController = navigator.navController,
startDestination = currentStartDestination,
enterTransition = { slideInHorizontally(tween(350, easing = FastOutSlowInEasing)) { it } },
exitTransition = { slideOutHorizontally(tween(350, easing = FastOutSlowInEasing)) { -it / 3 } },
popEnterTransition = { slideInHorizontally(tween(350, easing = FastOutSlowInEasing)) { -it / 3 } },
popExitTransition = { slideOutHorizontally(tween(350, easing = FastOutSlowInEasing)) { it } },
builder = navGraphBuilder,
)

IOS -> NavHost(
navController = navigator.navController,
startDestination = currentStartDestination,
builder = navGraphBuilder,
)
}
}

if (uiState.showNetworkErrorDialog) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import com.chukchukhaksa.mobile.presentation.landing.navigation.LandingRoute
import com.chukchukhaksa.mobile.presentation.timetable.navigation.TimetableRoute
import com.chukchukhaksa.mobile.presentation.timetable.navigation.argument.CellEditorArgument
import com.chukchukhaksa.mobile.presentation.timetable.navigation.argument.TimetableEditorArgument
Expand Down Expand Up @@ -49,6 +50,14 @@ class MainNavigator(
navController.navigateTimetable()
}

fun navigateFromLandingToHome() {
navController.navigate(TimetableRoute.route) {
popUpTo(LandingRoute.route) {
inclusive = true
}
}
}

fun popBackStackIfNotHome() {
if (!isSameCurrentDestination(TimetableRoute.route)) {
navController.popBackStack()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.chukchukhaksa.mobile.data.auth.datasource

import com.chukchukhaksa.mobile.domain.auth.model.RefreshTokenResult
import com.chukchukhaksa.mobile.domain.auth.model.SignInResult

interface RemoteAuthDataSource {
suspend fun signIn(idToken: String, nonce: String): SignInResult
suspend fun refreshToken(refreshToken: String): RefreshTokenResult
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ class AuthRepositoryImpl(
return remoteAuthDataSource.signIn(idToken = idToken, nonce = nonce)
}

override suspend fun refreshToken() {
val token = localAuthDataSource.getRefreshToken()
?: throw IllegalStateException("No refresh token")
val result = remoteAuthDataSource.refreshToken(token)
saveTokens(
accessToken = result.accessToken,
refreshToken = result.refreshToken,
)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

override suspend fun saveTokens(accessToken: String, refreshToken: String) {
localAuthDataSource.saveAccessToken(accessToken)
localAuthDataSource.saveRefreshToken(refreshToken)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.chukchukhaksa.mobile.di

import com.chukchukhaksa.mobile.domain.auth.usecase.AppleLoginUseCase
import com.chukchukhaksa.mobile.domain.auth.usecase.CheckAuthStateUseCase
import com.chukchukhaksa.mobile.domain.auth.usecase.KakaoLoginUseCase
import com.chukchukhaksa.mobile.domain.config.usecase.CheckNeedForceUpdateUseCase
import com.chukchukhaksa.mobile.domain.timetable.usecase.DeleteTimetableCellUseCase
Expand Down Expand Up @@ -45,5 +46,6 @@ val domainModule = module {

// Auth use cases
factory { AppleLoginUseCase(get()) }
factory { CheckAuthStateUseCase(get()) }
factory { KakaoLoginUseCase(get(), get()) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.chukchukhaksa.mobile.MainViewModel
import com.chukchukhaksa.mobile.presentation.timetable.timetable.component.bottomsheet.openmajor.OpenMajorBottomSheetViewModel
import com.chukchukhaksa.mobile.presentation.timetable.celleditor.CellEditorViewModel
import com.chukchukhaksa.mobile.presentation.timetable.openlecture.OpenLectureViewModel
import com.chukchukhaksa.mobile.presentation.landing.LandingViewModel
import com.chukchukhaksa.mobile.presentation.timetable.timetable.TimetableViewModel
import com.chukchukhaksa.mobile.presentation.timetable.timetablenameinput.TimetableNameInputViewModel
import com.chukchukhaksa.mobile.presentation.timetable.timetableeditor.TimetableEditorViewModel
Expand All @@ -13,6 +14,7 @@ import org.koin.core.module.dsl.viewModelOf
import org.koin.dsl.module

val presentationModule = module {
viewModelOf(::LandingViewModel)
viewModelOf(::TimetableViewModel)
viewModelOf(::TimetableListViewModel)
viewModelOf(::TimetableNameInputViewModel)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.chukchukhaksa.mobile.domain.auth.model

data class RefreshTokenResult(
val accessToken: String,
val refreshToken: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.chukchukhaksa.mobile.domain.auth.model.SignInResult

interface AuthRepository {
suspend fun signIn(idToken: String, nonce: String): SignInResult
suspend fun refreshToken()
suspend fun saveTokens(accessToken: String, refreshToken: String)
suspend fun getAccessToken(): String?
suspend fun getRefreshToken(): String?
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.chukchukhaksa.mobile.domain.auth.usecase

import com.chukchukhaksa.mobile.domain.auth.repository.AuthRepository
import com.chukchukhaksa.mobile.domain.common.runCatchingIgnoreCancelled
import com.chukchukhaksa.mobile.remote.common.ApiException
import kotlin.coroutines.cancellation.CancellationException

class CheckAuthStateUseCase(
private val authRepository: AuthRepository,
) {
suspend operator fun invoke(): Result<Boolean> = runCatchingIgnoreCancelled {
val refreshToken = authRepository.getRefreshToken()
?: return@runCatchingIgnoreCancelled false

try {
authRepository.refreshToken()
true
} catch (e: CancellationException) {
throw e
} catch (e: ApiException) {
authRepository.clearTokens()
false
} catch (e: Exception) {
false
}
}
Comment thread
lluke0 marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.chukchukhaksa.mobile.presentation.landing

data class LandingState(
val isLoading: Boolean = false,
)

sealed interface LandingSideEffect {
data object NavigateHome : LandingSideEffect
data class ShowToast(val message: String) : LandingSideEffect
data class HandleException(val throwable: Throwable) : LandingSideEffect
}
Loading
Loading