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
15 changes: 15 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"hooks": {
"PreToolUse": [
{
"matcher": "Read|Edit|Write",
"hooks": [
{
"type": "command",
"command": "TOOL_INPUT=$(cat); if echo \"$TOOL_INPUT\" | grep -q 'google-services.json'; then echo 'BLOCK: google-services.json์€ ๋ฏผ๊ฐํ•œ ํŒŒ์ผ์ด๋ฏ€๋กœ ์ ‘๊ทผํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.' >&2; exit 2; fi"
}
]
}
]
}
}
95 changes: 62 additions & 33 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## Project Overview

**ChukChukHaksa** (formerly Suwiki) is a Kotlin Multiplatform Compose project for timetable and course evaluation services for Suwon University students. The project has been simplified from a 40+ module architecture to a single `composeApp` module while maintaining Clean Architecture with MVI pattern.
**ChukChukHaksa** (formerly Suwiki) is a Kotlin Multiplatform Compose project for timetable and course evaluation services for Suwon University students. The project uses a single `composeApp` module with Clean Architecture and MVI pattern. Supports Android and iOS with platform-specific widgets.

## Build System and Commands

Expand Down Expand Up @@ -43,27 +43,31 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

### MVI + Clean Architecture Pattern
- **Presentation Layer**: MVI with custom `MviStore` implementation using StateFlow/Flow
- **Domain Layer**: Use cases following single responsibility principle
- **Data Layer**: Repository pattern with Room database and Firebase integration
- **Domain Layer**: Use cases with `suspend operator fun invoke()`, returns `Result<T>`
- **Data Layer**: Repository pattern with local (Room/Datastore) and remote (Firebase) data sources

### Key Architectural Components
- **State Management**: Unidirectional Data Flow (UDF) with StateFlow
- **Dependency Injection**: Koin 4.1.0-Beta10 with feature-based modules
- **Navigation**: Jetpack Navigation Compose with type-safe routes
- **Database**: Room 2.7.1 with SQLite for local storage
- **Remote Data**: Firebase via GitLive KMP wrappers
- **Navigation**: Jetpack Navigation Compose 2.9.0-beta02 with type-safe routes (kotlinx.serialization for argument passing)
- **Database**: Room 2.7.1 with SQLite for local storage (schema in `composeApp/schemas`)
- **Remote Data**: Firebase via GitLive KMP wrappers 2.1.0
- **Widgets**: Android (Glance 1.1.1) / iOS (Native WidgetKit) timetable widgets

## Design System

### Dual Theme System
The project uses two parallel design systems:

1. **SuwikiTheme**: Legacy design system with NotoSans fonts
- 17 text styles (header1-7, body1-7, caption1-7)
- 21 text styles (header1-7, body1-7, caption1-7)

2. **CCHaksaTheme**: New design system with Paperlogy + SUIT fonts
- Typography: 10 text styles (titleExlg2, titleExlg, titleLg, bodyExlg, bodyLgStrong, bodyLg, bodyMdStrong, bodyMd, bodySm, bodyXs)
- Colors: Organized palette with Gray, Purple, Red, Yellow, Green variants (100-600 naming)
2. **CchTheme**: New design system with Paperlogy + SUIT fonts
- Typography: 13 text styles (titleExlg2, titleExlg, titleLg, bodyExlg, bodyLgStrong, bodyLg, bodyMdStrong, bodyMd, bodySmStrong, bodySm, bodyXsStrong, bodyXs, bodyXxs)
- Colors: Organized palette with Gray (100-600), Purple (100-600), Red (100-400), Yellow (100-200), Green (100-200)

### Design System Components
appbar, badge, bottomsheet, button, chip, container, dialog, loading, searchbar, tabbar, textfield, toast

### Design System Location
- **Components**: `composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/common/designsystem/component/`
Expand All @@ -73,50 +77,75 @@ The project uses two parallel design systems:
## Project Structure

### Multiplatform Targets
- **Android**: Primary target (minSdk 28, targetSdk 35)
- **iOS**: Framework generation for iOS app (iosX64, iosArm64, iosSimulatorArm64)
- **Android**: Primary target (minSdk 28, targetSdk 35, compileSdk 35)
- **iOS**: Framework generation via `iosApp.xcworkspace` (iosX64, iosArm64, iosSimulatorArm64)

### Key Dependencies
- **Compose Multiplatform**: 1.8.1
- **Kotlin**: 2.1.20
- **Kotlin**: 2.2.21
- **KSP**: 2.2.21-2.0.4
- **Compose Multiplatform**: 1.10.0
- **Koin DI**: 4.1.0-Beta10
- **Room Database**: 2.7.1
- **Firebase**: Via GitLive KMP wrappers
- **Firebase**: GitLive KMP 2.1.0 (Android BOM 33.2.0)
- **kotlinx-coroutines**: 1.10.1
- **kotlinx-serialization**: 1.8.1
- **kotlinx-datetime**: 0.7.1
- **kotlinx-immutable**: 0.3.8
- **Datastore Preferences**: 1.1.7
- **Napier (logging)**: 2.7.1
- **Glance (Android Widgets)**: 1.1.1

### Package Organization
```
com.chukchukhaksa.mobile/
โ”œโ”€โ”€ common/
โ”‚ โ”œโ”€โ”€ designsystem/ # UI components and themes
โ”‚ โ”œโ”€โ”€ model/ # Shared data models
โ”‚ โ””โ”€โ”€ util/ # Utility functions
โ”œโ”€โ”€ feature/ # Feature modules (MVI screens)
โ”œโ”€โ”€ data/ # Repository implementations
โ”œโ”€โ”€ domain/ # Use cases and domain models
โ””โ”€โ”€ di/ # Dependency injection modules
โ”‚ โ”œโ”€โ”€ extension/ # Kotlin extensions
โ”‚ โ”œโ”€โ”€ kmp/ # KMP platform-specific code
โ”‚ โ”œโ”€โ”€ model/ # Shared data models
โ”‚ โ”œโ”€โ”€ provider/ # Platform providers
โ”‚ โ””โ”€โ”€ ui/ # Common UI utilities (MviStore)
โ”œโ”€โ”€ data/ # Repository implementations
โ”œโ”€โ”€ di/ # Dependency injection modules
โ”œโ”€โ”€ domain/ # Use cases and repository interfaces
โ”œโ”€โ”€ local/ # Local data sources
โ”‚ โ”œโ”€โ”€ database/ # Room database
โ”‚ โ”œโ”€โ”€ datasource/ # Local data source implementations
โ”‚ โ””โ”€โ”€ datastore/ # Datastore preferences
โ”œโ”€โ”€ presentation/ # Feature screens (MVI pattern)
โ”œโ”€โ”€ remote/ # Remote data sources
โ””โ”€โ”€ widget/ # Widget support (expect/actual)
```

## Development Workflow

### Current Project State
- **Main Branch**: `develop`
- **Application ID**: `com.kunize.uswtimetable`
- **Version**: 2.3.7 (versionCode 41)
- **Version**: 3.0.1 (versionCode 51)
- **Java Compatibility**: Java 17

### MVI Implementation Pattern
Each screen follows this contract:
Each screen follows this pattern:
```kotlin
interface [Feature]Contract {
data class State(...)
sealed interface SideEffect
// State + SideEffect definitions
data class [Feature]State(...)
sealed interface [Feature]SideEffect

// ViewModel with MviStore delegate
class [Feature]ViewModel(
private val someUseCase: SomeUseCase
) : ViewModel(), MviStore<State, SideEffect> by mviStore(initialState) {
fun onAction(action: ...) { ... }
}

class [Feature]Store : MviStore<State, SideEffect>
```

Use cases use `runCatchingIgnoreCancelled` for error handling.

### Navigation Structure
- Feature-based navigation graphs
- Type-safe route arguments
- Type-safe route arguments via kotlinx.serialization (JSON encoding to URI)
- Route object pattern with constant routes and parameterized functions
- Nested navigation for complex flows (e.g., timetable โ†’ editor โ†’ cell editor โ†’ lecture search)

### Resource Management
Expand All @@ -132,9 +161,10 @@ class [Feature]Store : MviStore<State, SideEffect>
## Development Notes

### Performance Optimizations
- Gradle configuration cache enabled
- 4GB heap allocation for builds
- Gradle configuration cache and build cache enabled
- Kotlin daemon: 3GB heap / Gradle JVM: 4GB heap
- Non-transitive R classes enabled
- Core library desugaring enabled for Android

### Code Style
- Kotlin official code style
Expand All @@ -144,8 +174,7 @@ class [Feature]Store : MviStore<State, SideEffect>

### Design System Usage
When creating new UI components:
1. Use `CCHaksaTheme` for new features
1. Use `CchTheme` for new features
2. Preview components in `preview/designsystem/` package
3. Follow existing color naming conventions (100-600 scale)
4. Prefer typography styles over hardcoded text styles
```
2 changes: 0 additions & 2 deletions composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,6 @@ kotlin {
implementation(libs.kmp.firebase.crashlytics)
implementation(libs.kmp.firebase.analytics)
implementation(libs.napier)

implementation(libs.accompanist.systemuicontroller)
}
commonTest.dependencies {
implementation(libs.kotlin.test)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,3 @@
package com.chukchukhaksa.mobile.common.kmp

import androidx.compose.ui.graphics.Color
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import com.google.accompanist.systemuicontroller.rememberSystemUiController

actual fun getPlatform(): Platform = Platform.Android

@Composable
actual fun SetStatusBarColor(){
val sysUiController = rememberSystemUiController()

SideEffect {
sysUiController.setSystemBarsColor(color = Color(0xFFFFFFFF))
}
}
31 changes: 24 additions & 7 deletions composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/App.kt

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

์ €๋Š” ์—ฌ๊ธฐ๋ถ€ํ„ฐ ์ฒœ์ฒœํžˆ ์กฐ์‚ฌํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.. Koin ๋ถ€ํ„ฐ ํ•ด์•ผ๊ฒ ๋„ค์š”
LGTM์ด์—์œ 

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.

์ฐธ๊ณ ๋กœ koin ์—„์ฒญ ์—‰๋ง์œผ๋กœ ์ ์šฉ๋˜์–ด์žˆ์–ด .. ใ…‹ใ…‹...

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.

์ฐธ๊ณ ๋กœ koin ์—„์ฒญ ์—‰๋ง์œผ๋กœ ์ ์šฉ๋˜์–ด์žˆ์–ด .. ใ…‹ใ…‹...

Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package com.chukchukhaksa.mobile

import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.navigationBars
Expand All @@ -8,6 +12,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.unit.dp
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.NavHost
import chukchukhaksa.composeapp.generated.resources.Res
import chukchukhaksa.composeapp.generated.resources.dialog_network_body
Expand All @@ -20,7 +25,6 @@ import com.chukchukhaksa.mobile.common.designsystem.component.toast.CchToast
import com.chukchukhaksa.mobile.common.designsystem.theme.CchTheme
import com.chukchukhaksa.mobile.common.designsystem.theme.White
import com.chukchukhaksa.mobile.common.kmp.Platform.*
import com.chukchukhaksa.mobile.common.kmp.SetStatusBarColor
import com.chukchukhaksa.mobile.common.kmp.getPlatform
import com.chukchukhaksa.mobile.common.ui.collectWithLifecycle
import com.chukchukhaksa.mobile.presentation.timetable.navigation.timetableNavGraph
Expand All @@ -38,7 +42,6 @@ fun App(
viewModel: MainViewModel = koinViewModel(),
navigator: MainNavigator = rememberMainNavigator(),
) {
SetStatusBarColor()
CchTheme {
KoinContext {
val uiState = viewModel.mviStore.uiState.collectAsState().value
Expand Down Expand Up @@ -70,11 +73,7 @@ fun App(
contentWindowInsets = WindowInsets(0.dp),
modifier = modifier,
content = { innerPadding ->
NavHost(
navController = navigator.navController,
startDestination = navigator.startDestination,
) {

val navGraphBuilder: NavGraphBuilder.() -> Unit = {
timetableNavGraph(
padding = innerPadding,
popBackStack = navigator::popBackStackIfNotHome,
Expand All @@ -90,6 +89,24 @@ fun App(
)
}

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,
)

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

if (uiState.showNetworkErrorDialog) {
CchDialog(
headerText = stringResource(Res.string.dialog_network_header),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.chukchukhaksa.mobile.common.designsystem.component

import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.MutableWindowInsets
import androidx.compose.foundation.layout.WindowInsets
Expand Down Expand Up @@ -42,14 +43,19 @@ fun SuwikiBackground(
.onConsumedWindowInsetsChanged { consumedWindowInsets ->
safeInsets.insets = contentWindowInsets.exclude(consumedWindowInsets)
}
.padding(safeInsets.insets.asPaddingValues())
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) {
focusManager.clearFocus()
},
) {
content()
Box(
modifier = Modifier
.fillMaxSize()
.padding(safeInsets.insets.asPaddingValues())
) {
content()
}
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
package com.chukchukhaksa.mobile.common.kmp

import androidx.compose.runtime.Composable

enum class Platform {
Android,
IOS
}

expect fun getPlatform(): Platform

@Composable
expect fun SetStatusBarColor()
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.chukchukhaksa.mobile.common.ui

import kotlinx.datetime.Clock
import kotlin.time.ExperimentalTime

/**
* ํด๋ฆญ์ด ์ง€์—ฐ๋  ์‹œ๊ฐ„์„ ์ •์˜ํ•˜๋Š” ๋ณ€์ˆ˜
Expand All @@ -17,8 +17,9 @@ internal fun MultipleEventsCutter.Companion.get(): MultipleEventsCutter =
MultipleEventsCutterImpl()

private class MultipleEventsCutterImpl : MultipleEventsCutter {
@OptIn(ExperimentalTime::class)
private val now: Long
get() = Clock.System.now().toEpochMilliseconds()
get() = kotlin.time.Clock.System.now().toEpochMilliseconds()

private var lastEventTimeMs: Long = 0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ package com.chukchukhaksa.mobile.presentation.timetable.semesterselect
import com.chukchukhaksa.mobile.presentation.timetable.navigation.argument.TimetableEditorArgument
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.toPersistentList
import kotlinx.datetime.Clock
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import kotlin.time.Clock
import kotlin.time.ExperimentalTime

data class SemesterSelectState(
val selectSemesterIndex: Int? = null,
Expand All @@ -17,6 +18,7 @@ sealed interface SemesterSelectSideEffect {
data class NavigateTimetableNameInput(val semester: TimetableEditorArgument): SemesterSelectSideEffect
}

@OptIn(ExperimentalTime::class)
val semesterList: PersistentList<Semester> = run {
val currentYear = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).year
val semesterList = mutableListOf<Semester>()
Expand Down
Loading
Loading