Skip to content

Commit ffb5f09

Browse files
authored
Merge pull request #287 from YAPP-Github/develop
release v1.4.0
2 parents a7c76f1 + 74a8ce9 commit ffb5f09

275 files changed

Lines changed: 5928 additions & 4078 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/CLAUDE.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Reed 프로젝트 작업 지침
2+
3+
## 빌드 관련
4+
5+
- **빌드는 사용자가 직접 수행합니다**
6+
- 기능 작업 완료 후 빌드를 자동으로 실행하지 마세요 (시간이 오래 걸림)
7+
- 빌드가 필요한 경우 사용자에게 알리고 사용자가 직접 실행하도록 합니다
8+
9+
## 커밋 관련
10+
11+
- **커밋 메시지에서 Claude 관련 문구를 제거합니다**
12+
- 다음 문구들을 커밋 메시지에 포함하지 마세요:
13+
- `🤖 Generated with [Claude Code](https://claude.com/claude-code)`
14+
- `Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>`
15+
- 커밋 작업은 사용자가 직접 수행하는 경우가 많으므로, 요청받지 않은 경우 커밋하지 마세요
16+
17+
## MCP 설정 관련
18+
19+
- **Claude Code CLI의 MCP 설정 파일 위치**
20+
- Claude Desktop이 아니라 **Claude Code CLI**를 사용 중입니다
21+
- MCP 설정은 `~/.claude.json``projects` 섹션에서 프로젝트별로 관리됩니다
22+
- Claude Desktop 설정 파일(`~/Library/Application Support/Claude/claude_desktop_config.json`)을 수정하지 마세요
23+
- **Figma MCP 설정 경로**
24+
- `~/.claude.json``projects``/Users/medi/AndroidStudioProjects/YeoBee-Android``mcpServers``figma`

.claude/CODING.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# 코딩 가이드
2+
3+
## 코드 작성 원칙
4+
5+
- 한글 주석 사용
6+
- Kotlin 코딩 컨벤션 준수
7+
- 기존 코드 스타일 유지
8+
- 파일 끝에 빈 줄(newline) 추가
9+
10+
## Compose 관련
11+
12+
- **Composable 함수 내 Collection 타입**
13+
- `List`, `Set`, `Map` 등의 Collection 대신 `ImmutableList`, `ImmutableSet`, `ImmutableMap` 사용
14+
- `kotlinx.collections.immutable` 라이브러리 사용
15+
- 예시:
16+
```kotlin
17+
// ❌ 사용하지 않음
18+
@Composable
19+
fun TripList(trips: List<Trip>) { ... }
20+
21+
// ✅ 사용
22+
@Composable
23+
fun TripList(trips: ImmutableList<Trip>) { ... }
24+
```
25+
- 변환 시 `toImmutableList()`, `persistentListOf()` 등 사용

.github/workflows/android-ci.yml

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ env:
66

77
on:
88
pull_request:
9+
push:
10+
branches: [ main, develop ]
911

1012
concurrency:
1113
group: build-${{ github.ref }}
@@ -15,7 +17,7 @@ jobs:
1517
ci-build:
1618
runs-on: ubuntu-latest
1719

18-
if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip-ci') }}
20+
if: ${{ github.event_name == 'push' || !contains(github.event.pull_request.labels.*.name, 'skip-ci') }}
1921

2022
steps:
2123
- name: Checkout
@@ -30,10 +32,14 @@ jobs:
3032
- name: Setup Android SDK
3133
uses: android-actions/setup-android@v2
3234

35+
# 캐시 저장/정리는 job 완료 후 post action 단계에서 수행
3336
- name: Setup Gradle
34-
uses: gradle/gradle-build-action@v2
37+
uses: gradle/actions/setup-gradle@v4
3538
with:
36-
gradle-home-cache-cleanup: true
39+
# 빌드 성공 시 미사용 캐시 엔트리 자동 정리 (default: on-success)
40+
# cache-cleanup: on-success
41+
# PR에서는 캐시 읽기만 허용, push(develop/main)에서만 캐시 갱신하여 용량 절약
42+
cache-read-only: ${{ github.event_name == 'pull_request' }}
3743

3844
- name: Generate local.properties
3945
run: echo '${{ secrets.LOCAL_PROPERTIES }}' | base64 -d > ./local.properties
@@ -70,10 +76,14 @@ jobs:
7076
- name: Setup Android SDK
7177
uses: android-actions/setup-android@v2
7278

79+
# 캐시 저장/정리는 job 완료 후 post action 단계에서 수행
7380
- name: Setup Gradle
74-
uses: gradle/gradle-build-action@v2
81+
uses: gradle/actions/setup-gradle@v4
7582
with:
76-
gradle-home-cache-cleanup: true
83+
# 빌드 성공 시 미사용 캐시 엔트리 자동 정리 (default: on-success)
84+
# cache-cleanup: on-success
85+
# PR에서는 캐시 읽기만 허용, push(develop/main)에서만 캐시 갱신하여 용량 절약
86+
cache-read-only: ${{ github.event_name == 'pull_request' }}
7787

7888
- name: Generate local.properties
7989
run: echo '${{ secrets.LOCAL_PROPERTIES }}' | base64 -d > ./local.properties
@@ -84,5 +94,8 @@ jobs:
8494
- name: Generate google-services.json
8595
run: echo '${{ secrets.GOOGLE_SERVICES }}' | base64 -d > ./app/google-services.json
8696

97+
- name: Compose Stability Dump
98+
run: ./gradlew stabilityDump
99+
87100
- name: Compose Stability Check
88101
run: ./gradlew stabilityCheck

README.md

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Reed - 문장과 감정을 함께 담는 독서 기록
22

3-
[![Kotlin](https://img.shields.io/badge/Kotlin-2.2.0-blue.svg)](https://kotlinlang.org)
3+
[![Kotlin](https://img.shields.io/badge/Kotlin-2.2.21-blue.svg)](https://kotlinlang.org)
44
[![Gradle](https://img.shields.io/badge/gradle-8.11.1-green.svg)](https://gradle.org/)
55
[![Android Studio](https://img.shields.io/badge/Android%20Studio-2025.1.2%20%28Narwhal%29-green)](https://developer.android.com/studio)
66
[![minSdkVersion](https://img.shields.io/badge/minSdkVersion-28-red)](https://developer.android.com/distribute/best-practices/develop/target-sdk)
@@ -37,15 +37,19 @@
3737
| <img width="230" alt="기록 카드 공유" src="https://github.com/user-attachments/assets/4c01a5ed-e5a2-4be4-b950-96a457c87ad7" /> |
3838

3939
## TroubleShooting
40-
- [[Compose] M3 ModalBottomSheet 드래그(터치 이벤트) 막는 법](https://velog.io/@mraz3068/Compose-M3-ModalBottomSheet-Drag-Disabled)
40+
- [Metro 적용해보기](https://velog.io/@mraz3068/Metro-Apply)
41+
- [Compose Stability Analyzer 사용 후기](https://velog.io/@mraz3068/compose-stability-analyzer-review)
42+
- [[Android] Toast 내부 구현 확인 해보기](https://velog.io/@mraz3068/Android-Toast-Deep-Dive)
43+
- [Coroutine CancellationException 따로 처리해야하는 케이스](https://velog.io/@mraz3068/Coroutine-CancellationException-UseCase)
44+
- [Coroutine 에러 처리 패턴: 여러 API 호출을 한 번에 성공/실패 판정하기](https://velog.io/@syoon513/Coroutine-%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC)
45+
- [[Circuit] ImpressionEffect](https://velog.io/@mraz3068/Circuit-ImpressionEffect)
46+
- [[Android] 일회성 이벤트를 StateFlow, Compose의 State로 처리할 때 주의해야할 점](https://velog.io/@mraz3068/Handle-One-Time-Event-As-State)
47+
- [Jetpack Compose에서 CameraX + MLKit으로 OCR을 구현해보자](https://velog.io/@syoon513/Jetpack-Compose%EC%97%90%EC%84%9C-CameraX-MLKit%EC%9C%BC%EB%A1%9C-OCR%EC%9D%84-%EA%B5%AC%ED%98%84%ED%95%B4%EB%B3%B4%EC%9E%90)
4148
- [Circuit 찍먹해보기(부제: Circuit 희망편)](https://speakerdeck.com/easyhooon/circuit-jjigmeoghaebogi-buje-circuit-hyimangpyeon)
4249
- [Circuit 찍먹해보기(부제: Circuit 절망편)](https://speakerdeck.com/easyhooon/circuit-jjigmeoghaebogi-buje-circuit-jeolmangpyeon)
43-
- [Jetpack Compose에서 CameraX + MLKit으로 OCR을 구현해보자](https://velog.io/@syoon513/Jetpack-Compose%EC%97%90%EC%84%9C-CameraX-MLKit%EC%9C%BC%EB%A1%9C-OCR%EC%9D%84-%EA%B5%AC%ED%98%84%ED%95%B4%EB%B3%B4%EC%9E%90)
44-
- [[Android] 일회성 이벤트를 StateFlow, Compose의 State로 처리할 때 주의해야할 점](https://velog.io/@mraz3068/Handle-One-Time-Event-As-State)
4550
- [Circuit Navigation 사용 시 feature 모듈간의 참조는 어떻게 해결했을까?](https://velog.io/@syoon513/Circuit-Navigation-%EC%82%AC%EC%9A%A9-%EC%8B%9C-feature-%EB%AA%A8%EB%93%88%EA%B0%84-%EC%88%9C%ED%99%98-%EC%B0%B8%EC%A1%B0%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%95%B4%EA%B2%B0%ED%96%88%EC%9D%84%EA%B9%8C)
46-
- [Coroutine 에러 처리 패턴: 여러 API 호출을 한 번에 성공/실패 판정하기](https://velog.io/@syoon513/Coroutine-%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC)
47-
- [[Circuit] ImpressionEffect](https://velog.io/@mraz3068/Circuit-ImpressionEffect)
48-
- [Coroutine CancellationException 따로 처리해야하는 케이스](https://velog.io/@mraz3068/Coroutine-CancellationException-UseCase)
51+
- [[Compose] M3 ModalBottomSheet 드래그(터치 이벤트) 막는 법](https://velog.io/@mraz3068/Compose-M3-ModalBottomSheet-Drag-Disabled)
52+
4953

5054
## Development
5155

@@ -55,7 +59,7 @@
5559
- JDK : Java 17을 실행할 수 있는 JDK
5660
- (권장) Android Studio 설치 시 Embedded 된 JDK (Open JDK)
5761
- Java 17을 사용하는 JDK (Open JDK, AdoptOpenJDK, GraalVM)
58-
- Kotlin Language : 2.2.0
62+
- Kotlin Language : 2.2.21
5963

6064
### Language
6165

@@ -76,8 +80,8 @@
7680
- Material3
7781

7882
- [Circuit](https://github.com/slackhq/circuit)
79-
- ~~Google ML Kit~~ Google Cloud Vision
80-
- Dagger Hilt
83+
- ~~Google ML Kit~~ -> [Google Cloud Vision](https://cloud.google.com/vision)
84+
- ~~Dagger Hilt~~ -> [Metro](https://github.com/ZacSweers/metro)
8185
- Retrofit, OkHttp3
8286
- Lottie-Compose
8387
- Firebase(Analytics, Crashlytics, Remote Config)

app/build.gradle.kts

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
@file:Suppress("INLINE_FROM_HIGHER_PLATFORM")
2-
3-
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
1+
import com.google.devtools.ksp.gradle.KspExtension
2+
import com.ninecraft.booket.convention.getLocalProperty
3+
import org.gradle.kotlin.dsl.configure
44
import java.util.Properties
55

66
plugins {
77
alias(libs.plugins.booket.android.application)
88
alias(libs.plugins.booket.android.application.compose)
9-
alias(libs.plugins.booket.android.hilt)
109
alias(libs.plugins.booket.android.firebase)
10+
alias(libs.plugins.metro)
11+
alias(libs.plugins.ksp)
1112
}
1213

1314
android {
@@ -50,8 +51,9 @@ android {
5051
}
5152

5253
defaultConfig {
53-
buildConfigField("String", "KAKAO_NATIVE_APP_KEY", getApiKey("KAKAO_NATIVE_APP_KEY"))
54-
manifestPlaceholders["KAKAO_NATIVE_APP_KEY"] = getApiKey("KAKAO_NATIVE_APP_KEY").trim('"')
54+
buildConfigField("String", "GOOGLE_WEB_CLIENT_ID", getLocalProperty("GOOGLE_WEB_CLIENT_ID"))
55+
buildConfigField("String", "KAKAO_NATIVE_APP_KEY", getLocalProperty("KAKAO_NATIVE_APP_KEY"))
56+
manifestPlaceholders["KAKAO_NATIVE_APP_KEY"] = getLocalProperty("KAKAO_NATIVE_APP_KEY").trim('"')
5557
}
5658

5759
buildFeatures {
@@ -63,8 +65,8 @@ composeStabilityAnalyzer {
6365
enabled.set(true)
6466
}
6567

66-
ksp {
67-
arg("circuit.codegen.mode", "hilt")
68+
extensions.configure<KspExtension> {
69+
arg("circuit.codegen.mode", "metro")
6870
}
6971

7072
dependencies {
@@ -75,6 +77,7 @@ dependencies {
7577
projects.core.datastore.api,
7678
projects.core.datastore.impl,
7779
projects.core.designsystem,
80+
projects.core.di,
7881
projects.core.model,
7982
projects.core.network,
8083
projects.core.ui,
@@ -105,7 +108,3 @@ dependencies {
105108
api(libs.circuit.codegen.annotation)
106109
ksp(libs.circuit.codegen.ksp)
107110
}
108-
109-
fun getApiKey(propertyKey: String): String {
110-
return gradleLocalProperties(rootDir, providers).getProperty(propertyKey)
111-
}

app/src/main/AndroidManifest.xml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
<application
1414
android:name=".BooketApplication"
15+
android:appComponentFactory="com.ninecraft.booket.di.MetroAppComponentFactory"
1516
android:allowBackup="true"
1617
android:dataExtractionRules="@xml/data_extraction_rules"
1718
android:fullBackupContent="@xml/backup_rules"
@@ -21,7 +22,8 @@
2122
android:roundIcon="@mipmap/ic_launcher_round"
2223
android:supportsRtl="true"
2324
android:theme="@style/Theme.Booket"
24-
tools:targetApi="31">
25+
tools:targetApi="31"
26+
tools:replace="android:appComponentFactory">
2527

2628
<provider
2729
android:name="androidx.startup.InitializationProvider"
@@ -83,7 +85,8 @@
8385

8486
<service
8587
android:name=".ReedFirebaseMessagingService"
86-
android:exported="false">
88+
android:exported="false"
89+
tools:ignore="Instantiatable">
8790
<intent-filter>
8891
<action android:name="com.google.firebase.MESSAGING_EVENT" />
8992
</intent-filter>

app/src/main/kotlin/com/ninecraft/booket/BooketApplication.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@ import coil.ImageLoader
55
import coil.ImageLoaderFactory
66
import coil.disk.DiskCache
77
import coil.util.DebugLogger
8-
import dagger.hilt.android.HiltAndroidApp
8+
import com.ninecraft.booket.di.AppGraph
9+
import dev.zacsweers.metro.createGraphFactory
910

10-
@HiltAndroidApp
1111
class BooketApplication : Application(), ImageLoaderFactory {
12+
13+
val appGraph by lazy { createGraphFactory<AppGraph.Factory>().create(this) }
14+
1215
override fun newImageLoader(): ImageLoader {
1316
return ImageLoader.Builder(this)
1417
.diskCache {

app/src/main/kotlin/com/ninecraft/booket/ReedFirebaseMessagingService.kt

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.ninecraft.booket
33
import android.app.NotificationChannel
44
import android.app.NotificationManager
55
import android.app.PendingIntent
6+
import android.app.Service
67
import android.content.Context
78
import android.content.Intent
89
import androidx.core.app.NotificationCompat
@@ -11,20 +12,24 @@ import com.google.firebase.messaging.FirebaseMessagingService
1112
import com.google.firebase.messaging.RemoteMessage
1213
import com.ninecraft.booket.core.data.api.repository.UserRepository
1314
import com.ninecraft.booket.core.designsystem.R
15+
import com.ninecraft.booket.core.di.ServiceKey
1416
import com.ninecraft.booket.feature.main.MainActivity
15-
import dagger.hilt.android.AndroidEntryPoint
17+
import dev.zacsweers.metro.AppScope
18+
import dev.zacsweers.metro.ContributesIntoMap
19+
import dev.zacsweers.metro.Inject
20+
import dev.zacsweers.metro.binding
1621
import kotlinx.coroutines.CoroutineScope
1722
import kotlinx.coroutines.Dispatchers
1823
import kotlinx.coroutines.SupervisorJob
1924
import kotlinx.coroutines.cancel
2025
import kotlinx.coroutines.launch
21-
import javax.inject.Inject
2226

23-
@AndroidEntryPoint
24-
class ReedFirebaseMessagingService : FirebaseMessagingService() {
25-
26-
@Inject
27-
lateinit var userRepository: UserRepository
27+
@ContributesIntoMap(AppScope::class, binding = binding<Service>())
28+
@ServiceKey(ReedFirebaseMessagingService::class)
29+
@Inject
30+
class ReedFirebaseMessagingService(
31+
private val userRepository: UserRepository,
32+
) : FirebaseMessagingService() {
2833

2934
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
3035

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.ninecraft.booket.di
2+
3+
import android.app.Activity
4+
import android.app.Service
5+
import android.content.Context
6+
import com.ninecraft.booket.core.di.ApplicationContext
7+
import com.ninecraft.booket.core.di.DataScope
8+
import dev.zacsweers.metro.AppScope
9+
import dev.zacsweers.metro.DependencyGraph
10+
import dev.zacsweers.metro.Multibinds
11+
import dev.zacsweers.metro.Provider
12+
import dev.zacsweers.metro.Provides
13+
import kotlin.reflect.KClass
14+
15+
@DependencyGraph(
16+
scope = AppScope::class,
17+
additionalScopes = [DataScope::class],
18+
)
19+
interface AppGraph {
20+
21+
@Multibinds(allowEmpty = true)
22+
val activityProviders: Map<KClass<out Activity>, Provider<Activity>>
23+
24+
@Multibinds(allowEmpty = true)
25+
val serviceProviders: Map<KClass<out Service>, Provider<Service>>
26+
27+
@DependencyGraph.Factory
28+
fun interface Factory {
29+
fun create(@ApplicationContext @Provides context: Context): AppGraph
30+
}
31+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.ninecraft.booket.di
2+
3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.text.BasicText
5+
import androidx.compose.ui.graphics.Color
6+
import androidx.compose.ui.text.TextStyle
7+
import com.slack.circuit.foundation.Circuit
8+
import com.slack.circuit.foundation.LocalCircuit
9+
import com.slack.circuit.runtime.Navigator
10+
import com.slack.circuit.runtime.presenter.Presenter
11+
import com.slack.circuit.runtime.ui.Ui
12+
import dev.zacsweers.metro.AppScope
13+
import dev.zacsweers.metro.ContributesTo
14+
import dev.zacsweers.metro.Multibinds
15+
import dev.zacsweers.metro.Provides
16+
17+
@ContributesTo(AppScope::class)
18+
interface CircuitGraph {
19+
20+
@Multibinds(allowEmpty = true)
21+
fun presenterFactories(): Set<Presenter.Factory>
22+
23+
@Multibinds(allowEmpty = true)
24+
fun uiFactories(): Set<Ui.Factory>
25+
26+
@Provides
27+
fun provideCircuit(
28+
presenterFactories: Set<Presenter.Factory>,
29+
uiFactories: Set<Ui.Factory>,
30+
): Circuit {
31+
return Circuit.Builder()
32+
.addPresenterFactories(presenterFactories)
33+
.addUiFactories(uiFactories)
34+
.setAnimatedNavDecoratorFactory(CrossFadeNavDecoratorFactory())
35+
.setOnUnavailableContent { screen, modifier ->
36+
val circuit = LocalCircuit.current
37+
BasicText(
38+
text = """
39+
Route not available: ${screen.javaClass.name}.
40+
Presenter: ${circuit?.presenter(screen, Navigator.NoOp)?.javaClass}
41+
UI: ${circuit?.ui(screen)?.javaClass}
42+
All presenterFactories: ${circuit?.newBuilder()?.presenterFactories}
43+
All uiFactories: ${circuit?.newBuilder()?.uiFactories}
44+
"""
45+
.trimIndent(),
46+
modifier = modifier.background(Color.Red),
47+
style = TextStyle(color = Color.Yellow),
48+
)
49+
}
50+
.build()
51+
}
52+
}

0 commit comments

Comments
 (0)