Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
4ee1dda
build: 라이브러리 카탈로그 및 빌드 설정 정비
seunghee17 Jul 14, 2026
53a5b74
chore: core 모듈을 common과 ui로 분리
seunghee17 Jul 15, 2026
0f6aa2a
chore: core-ktx 의존성 버전 카탈로그 추가
seunghee17 Jul 15, 2026
6b02c82
refactor: UseCase 패키지 경로 정리
seunghee17 Jul 15, 2026
0670c2c
chore: data 모듈 Android Library 설정 정리
seunghee17 Jul 15, 2026
8039435
chore: app 모듈 core 세부 모듈 의존성 반영
seunghee17 Jul 15, 2026
983c226
chore: core 모듈 종속성 삭제
seunghee17 Jul 15, 2026
4518635
feat: Navigation3 기반 네비게이션 상태 관리 추가
seunghee17 Jul 16, 2026
b4579d1
chore: feature:home, core:ui 모듈에 Compose·Navigation3 의존성 추가
seunghee17 Jul 16, 2026
663a1c1
feat: HomeScreen과 HomeKey를 feature:home 모듈로 이동
seunghee17 Jul 16, 2026
231b1ae
feat: core:ui에 GamssBottomBar 컴포넌트 추가
seunghee17 Jul 16, 2026
71efd55
feat: 하단 네비게이션 바를 NavigationState·Navigator와 연결
seunghee17 Jul 16, 2026
a42d5e1
chore: settings.gradle에 feature:home 모듈 등록
seunghee17 Jul 16, 2026
d8cb091
chore: .gitignore에 IDE 빌드 산출물(bin/) 추가
seunghee17 Jul 16, 2026
9412bb2
feat: Auth 인증 흐름 mock 구조 추가 (network, repository, domain)
seunghee17 Jul 16, 2026
6d1cc64
feat: feature:home에 Hilt·Orbit 의존성 추가
seunghee17 Jul 16, 2026
ce90e83
feat: HomeScreen에 Orbit 상태관리 mock 구조 추가
seunghee17 Jul 16, 2026
c7bcbca
feat: app 모듈에 Firebase Crashlytics 종속성 추가
seunghee17 Jul 16, 2026
96083c0
feat: app 모듈에 Firebase Analytics 종속성 추가
seunghee17 Jul 16, 2026
6646689
chore: README 프로젝트 구조를 실제 멀티모듈 구성으로 갱신
seunghee17 Jul 16, 2026
7db6a49
Merge branch 'develop' into feature/setting-module
seunghee17 Jul 16, 2026
8d2568c
chore: CI에 google-services.json 시크릿 주입 스텝 추가
seunghee17 Jul 16, 2026
2b8c5d5
fix: develop 병합 후 발생한 detekt 위반 사항 수정
seunghee17 Jul 16, 2026
f2dfd2d
refactor: BottomBar 아이템 모델 파일 분리
seunghee17 Jul 17, 2026
a5fa87c
fix: develop 병합 후 발생한 detekt 위반 사항 수정
seunghee17 Jul 17, 2026
3d24487
fix: Kotlin JvmTarget import로 Gradle 설정 정리
seunghee17 Jul 20, 2026
a0e4c52
fix: domain 모듈 인증 응답 토큰 기본값을 null로 변경
seunghee17 Jul 20, 2026
9b1a439
fix: 네비게이션 엔트리 변환에서 불필요한 mutable 변환 제거
seunghee17 Jul 20, 2026
30f0195
fix: 네비게이션 뒤로가기 처리 책임을 Host로 이동
seunghee17 Jul 20, 2026
5df0448
refactor: BottomBarItem 상태와 동작 분리
seunghee17 Jul 20, 2026
88f1a45
feat: 채팅 목록, 달력 화면 추가
seunghee17 Jul 20, 2026
794f28d
refactor: core:common 모듈 kotlinJvm 전환
seunghee17 Jul 20, 2026
10c119c
fix: AuthRepository login 결과를 AppResult로 감싸기
seunghee17 Jul 20, 2026
dbaaaa5
feat: top-level 화면에서만 BottomBar 표시되도록 조건 추가
seunghee17 Jul 20, 2026
ce3e4cd
fix: feature:calendar, feature:chat 모듈 detekt 위반 수정
seunghee17 Jul 20, 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: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ jobs:
with:
validate-wrappers: true

- name: Write google-services.json
run: echo "${{ secrets.GOOGLE_SERVICES_JSON }}" | base64 -d > app/google-services.json

- name: Run ${{ matrix.task }}
run: ./gradlew ${{ matrix.task }} --stacktrace

Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
.idea/
.kotlin/
build/
bin/
local.properties
*/build/
.worktree-*
app/google-services.json
32 changes: 20 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,23 +69,31 @@ API 키 등 비밀 값이 필요한 경우에도 `local.properties`에 두고

## 프로젝트 구조

멀티모듈 구조로 구성한다. 세부 모듈 구성은 진행 상황에 따라 확정한다.
멀티모듈 구조로 구성한다. 새 feature나 데이터 소스를 추가할 때도 아래 구조와 의존성 방향을 따른다.

```
GAMSS-Android/
├─ app/ # 앱 진입점, DI 구성, 네비게이션 호스트 (예정)
├─ core/ # 공통 유틸, 디자인 시스템, 네트워크/DB 기반 (예정)
├─ feature/ # 화면 단위 기능 모듈 (예정)
├─ data/ # 데이터 소스, 저장소 구현 (예정)
└─ gradle/ # 버전 카탈로그, wrapper
├─ app/ # 앱 진입점, Hilt DI 그래프 조립, Navigation3 호스트
├─ domain/ # UseCase, Repository 인터페이스, 도메인 모델 (순수 Kotlin/JVM, Android 의존성 없음)
├─ data/ # Repository 구현체, 원격(remote)·로컬 데이터 소스, DI 모듈(di)
├─ core/
│ ├─ common/ # 모듈 간 공유되는 순수 유틸 (AppResult 등)
│ └─ ui/ # 공용 Compose UI 컴포넌트 (GamssBottomBar 등)
├─ feature/
│ └─ home/ # 화면 단위 기능 모듈. 화면별로 하나씩 추가한다
└─ gradle/ # 버전 카탈로그, wrapper
```

| 모듈 | 역할 |
| --- | --- |
| `app` | 앱 진입점, DI 그래프 구성, 네비게이션 호스트 |
| `core` | 여러 기능에서 공유하는 공통 코드 (UI 컴포넌트, 네트워크·DB 기반, 유틸) |
| `feature` | 화면 단위 기능 모듈 (기능별로 분리) |
| `data` | 데이터 소스 및 저장소(Repository) 구현 |
| 모듈 | 역할 | 의존하는 모듈 |
| --- | --- | --- |
| `app` | 앱 진입점, Hilt DI 그래프 조립, Navigation3 호스트 | `domain`, `data`, `core:common`, `core:ui`, `feature:*` |
| `domain` | UseCase, Repository 인터페이스, 도메인 모델 | 없음 |
| `data` | Repository 구현, 원격/로컬 데이터 소스, Hilt DI 모듈 | `domain`, `core:common` |
| `core:common` | 여러 모듈이 공유하는 순수 유틸 | 없음 |
| `core:ui` | 공용 Compose UI 컴포넌트 | 없음 |
| `feature:*` | 화면 단위 기능 모듈 (기능별로 분리) | `core:ui` (필요 시 `domain`) |

**의존성 규칙**: 안쪽 레이어(`domain`)는 바깥쪽 어떤 모듈도 참조하지 않는다. `data`, `feature`, `app`은 `domain`이 정의한 인터페이스(Repository, UseCase)에 의존하고, 구현은 바깥쪽(`data`)에 둔다. `app`은 조립부이므로 예외적으로 전체 모듈을 참조한다.

## 기술 스택

Expand Down
19 changes: 17 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
alias(libs.plugins.androidApplication)
alias(libs.plugins.composeCompiler)
alias(libs.plugins.kotlinSerialization)
alias(libs.plugins.ksp)
alias(libs.plugins.hilt)
alias(libs.plugins.googleServices)
alias(libs.plugins.firebaseCrashlytics)
}

android {
Expand Down Expand Up @@ -36,28 +40,39 @@ android {

kotlin {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
jvmTarget = JvmTarget.JVM_17
}
}

dependencies {
implementation(projects.core)

implementation(projects.domain)
implementation(projects.data)
implementation(projects.core.common)
implementation(projects.core.ui)
implementation(projects.feature.home)
implementation(projects.feature.chat)
implementation(projects.feature.calendar)

implementation(platform(libs.compose.bom))
implementation(libs.compose.ui)
implementation(libs.compose.ui.graphics)
implementation(libs.compose.ui.tooling.preview)
implementation(libs.compose.material3)
implementation(libs.compose.material.icons.core)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.viewmodel.navigation3)

implementation(libs.navigation3.runtime)
implementation(libs.navigation3.ui)

implementation(libs.hilt.android)
ksp(libs.hilt.compiler)

implementation(platform(libs.firebase.bom))
implementation(libs.firebase.crashlytics)
implementation(libs.firebase.analytics)

debugImplementation(libs.compose.ui.tooling)
}
4 changes: 2 additions & 2 deletions app/src/main/kotlin/com/gamss/android/app/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import com.gamss.android.app.navigation.AppNavHost
import com.gamss.android.app.navigation.GamssNavHost
import com.gamss.android.app.ui.theme.GamssTheme
import dagger.hilt.android.AndroidEntryPoint

Expand All @@ -15,7 +15,7 @@ class MainActivity : ComponentActivity() {
enableEdgeToEdge()
setContent {
GamssTheme {
AppNavHost()
GamssNavHost()
}
}
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.gamss.android.app.navigation

import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.ui.NavDisplay
import com.gamss.android.core.ui.GamssBottomBar
import com.gamss.android.feature.calendar.CalendarScreen
import com.gamss.android.feature.calendar.navigation.CalendarKey
import com.gamss.android.feature.chat.ChattingListScreen
import com.gamss.android.feature.chat.navigation.ChatKey
import com.gamss.android.feature.home.HomeScreen
import com.gamss.android.feature.home.navigation.HomeKey

@Composable
fun GamssNavHost() {
val navigationState = rememberNavigationState(
startKey = HomeKey,
topLevelKeys = topLevelDestinationKeys,
)
val navigator = remember(navigationState) { Navigator(navigationState) }

Scaffold(
bottomBar = {
if (navigationState.currentKey == navigationState.currentTopLevelKey) {
GamssBottomBar(
items = topLevelBottomBarItems,
selectedValue = navigationState.currentTopLevelKey,
onItemClick = navigator::navigate,
)
}
},
) { innerPadding ->
NavDisplay(
modifier = Modifier.padding(innerPadding),
entries = navigationState.toEntries(
entryProvider = entryProvider {
entry<HomeKey> { HomeScreen() }
entry<ChatKey> { ChattingListScreen() }
entry<CalendarKey> { CalendarScreen() }
},
),
onBack = {
if (navigationState.canGoBack) {
navigator.goBack()
}
},
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.gamss.android.app.navigation

import androidx.compose.runtime.Composable
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.rememberDecoratedNavEntries
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator

/**
* GAMSS 메인 탭 네비게이션 상태를 생성한다.
*
* Navigation3의 back stack은 rememberSaveable 기반으로 동작하므로 화면 회전이나
* 프로세스 재생성 이후에도 복원 가능한 key를 사용해야 한다.
*
* @param startKey 앱의 메인 영역에서 처음 보여줄 최상위 key
* @param topLevelKeys bottom bar에 연결되는 최상위 key 목록
*/
@Composable
fun rememberNavigationState(
startKey: NavKey,
topLevelKeys: Set<NavKey>,
): NavigationState {
val topLevelStack = rememberNavBackStack(startKey)
val subStacks = topLevelKeys.associateWith { key -> rememberNavBackStack(key) }

return remember(startKey, topLevelKeys) {
NavigationState(
startKey = startKey,
topLevelStack = topLevelStack,
subStacks = subStacks,
)
}
}

/**
* bottom bar 기반 화면 전환을 위한 상태 홀더.
*
* topLevelStack은 사용자가 방문한 탭 순서를 저장하고, subStacks는 각 탭 내부의 상세 화면
* stack을 따로 저장한다. 탭을 전환해도 탭의 상세 화면 흐름을 유지할 수 있다.
*/
class NavigationState(
val startKey: NavKey,
val topLevelStack: NavBackStack<NavKey>,
val subStacks: Map<NavKey, NavBackStack<NavKey>>,
) {
val currentTopLevelKey: NavKey by derivedStateOf { topLevelStack.last() }

val topLevelKeys
get() = subStacks.keys

val currentSubStack: NavBackStack<NavKey>
get() = subStacks[currentTopLevelKey]
?: error("현재 탭($currentTopLevelKey)에 해당하는 back stack이 없습니다.")

val currentKey: NavKey by derivedStateOf { currentSubStack.last() }

val canGoBack: Boolean by derivedStateOf { currentKey != startKey }
}

/**
* NavigationState를 NavDisplay에서 사용할 entry 목록으로 변환한다.
*
* 각 탭의 sub stack에 saveable state와 ViewModelStore decorator를 붙여서, 탭 전환 중에도
* 화면 상태와 ViewModel 생명주기가 탭별 back stack에 맞게 유지되도록 한다.
*/
@Composable
fun NavigationState.toEntries(
entryProvider: (NavKey) -> NavEntry<NavKey>,
): List<NavEntry<NavKey>> {
val decoratedEntries = subStacks.mapValues { (_, stack) ->
val decorators = listOf(
rememberSaveableStateHolderNavEntryDecorator<NavKey>(),
rememberViewModelStoreNavEntryDecorator<NavKey>(),
)
rememberDecoratedNavEntries(
backStack = stack,
entryDecorators = decorators,
entryProvider = entryProvider,
)
}

return topLevelStack
Comment thread
soyeonLee126 marked this conversation as resolved.
.flatMap { decoratedEntries[it] ?: emptyList() }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.gamss.android.app.navigation

import androidx.navigation3.runtime.NavKey

/**
* NavigationState를 변경하는 앱 전용 navigator.
*
* GAMSS의 bottom bar 정책을 한 곳에 모아둔다.
* - 현재 탭을 다시 선택하면 해당 탭의 root 화면으로 이동
* - 다른 탭을 선택하면 방문한 탭 순서를 topLevelStack에 기록
* - 상세 화면은 현재 탭의 sub stack에 쌓되, 같은 key가 이미 있으면 마지막으로 이동
*/
class Navigator(val state: NavigationState) {

/**
* 지정한 key로 이동한다.
* top-level key인지, 현재 탭인지, 상세 key인지에 따라 stack 갱신 규칙이 달라진다.
*/
fun navigate(key: NavKey) {
when (key) {
state.currentTopLevelKey -> clearSubStack()
in state.topLevelKeys -> goToTopLevel(key)
else -> goToKey(key)
}
}

/**
* 현재 위치에서 뒤로 이동한다.
*
* 현재 탭의 root 화면에서는 이전에 방문한 탭으로 돌아가고, 상세 화면에서는 현재 탭의
* sub stack에서 한 단계 pop한다.
*/
fun goBack() {
when (state.currentKey) {
state.startKey -> Unit
state.currentTopLevelKey -> {
state.topLevelStack.removeLastOrNull()
}
else -> state.currentSubStack.removeLastOrNull()
}
}

/**
* 현재 탭의 상세 화면으로 이동한다.
*/
private fun goToKey(key: NavKey) {
state.currentSubStack.apply {
remove(key)
add(key)
}
}

/**
* 다른 top-level 탭으로 이동한다.
*/
private fun goToTopLevel(key: NavKey) {
state.topLevelStack.apply {
if (key == state.startKey) {
clear()
} else {
remove(key)
}
add(key)
}
}

/**
* 현재 탭을 다시 선택했을 때 root 화면만 남긴다.
*/
private fun clearSubStack() {
state.currentSubStack.run {
if (size > 1) subList(1, size).clear()
}
}
}
Loading
Loading