Skip to content

Commit c880642

Browse files
committed
Merge remote-tracking branch 'origin/develop' into BOOK-474-feature/#232
# Conflicts: # app/build.gradle.kts # core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Color.kt # feature/login/build.gradle.kts
2 parents ca2b3f0 + 4f07099 commit c880642

154 files changed

Lines changed: 4092 additions & 2284 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.

.github/workflows/android-ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,5 +84,8 @@ jobs:
8484
- name: Generate google-services.json
8585
run: echo '${{ secrets.GOOGLE_SERVICES }}' | base64 -d > ./app/google-services.json
8686

87+
- name: Compose Stability Dump
88+
run: ./gradlew stabilityDump
89+
8790
- name: Compose Stability Check
8891
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~~ Metro
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: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
@file:Suppress("INLINE_FROM_HIGHER_PLATFORM")
22

3-
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
43
import com.google.devtools.ksp.gradle.KspExtension
4+
import com.ninecraft.booket.convention.getLocalProperty
55
import org.gradle.kotlin.dsl.configure
66
import java.util.Properties
77

@@ -32,7 +32,7 @@ android {
3232
getByName("debug") {
3333
isDebuggable = true
3434
applicationIdSuffix = ".dev"
35-
buildConfigField("String", "GOOGLE_WEB_CLIENT_ID", getApiKey("DEBUG_GOOGLE_WEB_CLIENT_ID"))
35+
buildConfigField("String", "GOOGLE_WEB_CLIENT_ID", getLocalProperty("DEBUG_GOOGLE_WEB_CLIENT_ID"))
3636
manifestPlaceholders += mapOf(
3737
"appName" to "@string/app_name_dev",
3838
)
@@ -43,7 +43,7 @@ android {
4343
isMinifyEnabled = true
4444
isShrinkResources = true
4545
signingConfig = signingConfigs.getByName("release")
46-
buildConfigField("String", "GOOGLE_WEB_CLIENT_ID", getApiKey("RELEASE_GOOGLE_WEB_CLIENT_ID"))
46+
buildConfigField("String", "GOOGLE_WEB_CLIENT_ID", getLocalProperty("RELEASE_GOOGLE_WEB_CLIENT_ID"))
4747
manifestPlaceholders += mapOf(
4848
"appName" to "@string/app_name",
4949
)
@@ -55,8 +55,8 @@ android {
5555
}
5656

5757
defaultConfig {
58-
buildConfigField("String", "KAKAO_NATIVE_APP_KEY", getApiKey("KAKAO_NATIVE_APP_KEY"))
59-
manifestPlaceholders["KAKAO_NATIVE_APP_KEY"] = getApiKey("KAKAO_NATIVE_APP_KEY").trim('"')
58+
buildConfigField("String", "KAKAO_NATIVE_APP_KEY", getLocalProperty("KAKAO_NATIVE_APP_KEY"))
59+
manifestPlaceholders["KAKAO_NATIVE_APP_KEY"] = getLocalProperty("KAKAO_NATIVE_APP_KEY").trim('"')
6060
}
6161

6262
buildFeatures {
@@ -111,7 +111,3 @@ dependencies {
111111
api(libs.circuit.codegen.annotation)
112112
ksp(libs.circuit.codegen.ksp)
113113
}
114-
115-
fun getApiKey(propertyKey: String): String {
116-
return gradleLocalProperties(rootDir, providers).getProperty(propertyKey)
117-
}

app/stability/app.stability

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ public fun com.ninecraft.booket.di.CrossFadeNavDecorator.Decoration(targetState:
99
skippable: false
1010
restartable: true
1111
params:
12-
- targetState: RUNTIME (requires runtime check)
12+
- targetState: UNSTABLE (has mutable properties or unstable members)
1313
- innerContent: STABLE (composable function type)
1414

1515
@Composable

build-logic/src/main/kotlin/AndroidFeatureConventionPlugin.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ internal class AndroidFeatureConventionPlugin : Plugin<Project> {
3333
implementation(project(path = ":core:ui"))
3434
implementation(project(path = ":feature:screens"))
3535

36+
implementation(libs.kotlinx.collections.immutable)
3637
implementation(libs.compose.effects)
3738

3839
implementation(libs.bundles.circuit)

build-logic/src/main/kotlin/com/ninecraft/booket/convention/Extensions.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.ninecraft.booket.convention
22

3+
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
34
import org.gradle.accessors.dm.LibrariesForLibs
45
import org.gradle.api.Project
56
import org.gradle.kotlin.dsl.the
@@ -10,3 +11,7 @@ internal val Project.libs
1011
internal fun Project.applyPlugins(vararg plugins: String) {
1112
plugins.forEach(pluginManager::apply)
1213
}
14+
15+
fun Project.getLocalProperty(propertyKey: String): String {
16+
return gradleLocalProperties(rootDir, providers).getProperty(propertyKey)
17+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.ninecraft.booket.core.data.api.repository
2+
3+
import com.ninecraft.booket.core.model.EmotionGroupsModel
4+
5+
interface EmotionRepository {
6+
suspend fun getEmotions(): Result<EmotionGroupsModel>
7+
}

core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/RecordRepository.kt

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,37 @@
11
package com.ninecraft.booket.core.data.api.repository
22

3-
import com.ninecraft.booket.core.model.ReadingRecordModel
4-
import com.ninecraft.booket.core.model.RecordRegisterModel
3+
import com.ninecraft.booket.core.model.ReadingRecordModelV2
54
import com.ninecraft.booket.core.model.ReadingRecordsModel
6-
import com.ninecraft.booket.core.model.RecordDetailModel
75

86
interface RecordRepository {
97
suspend fun postRecord(
108
userBookId: String,
11-
pageNumber: Int,
9+
pageNumber: Int?,
1210
quote: String,
13-
emotionTags: List<String>,
1411
review: String,
15-
): Result<RecordRegisterModel>
12+
primaryEmotion: String,
13+
detailEmotionTagIds: List<String>,
14+
): Result<ReadingRecordModelV2>
1615

1716
suspend fun getReadingRecords(
1817
userBookId: String,
1918
sort: String,
2019
page: Int,
2120
size: Int,
22-
): Result<ReadingRecordsModel>
21+
): Result<ReadingRecordsModel> // TODO: V2로 변경 필요
2322

2423
suspend fun getRecordDetail(
2524
readingRecordId: String,
26-
): Result<RecordDetailModel>
25+
): Result<ReadingRecordModelV2>
2726

2827
suspend fun editRecord(
2928
readingRecordId: String,
30-
pageNumber: Int,
29+
pageNumber: Int?,
3130
quote: String,
32-
emotionTags: List<String>,
3331
review: String,
34-
): Result<ReadingRecordModel>
32+
primaryEmotion: String,
33+
detailEmotionTagIds: List<String>,
34+
): Result<ReadingRecordModelV2>
3535

3636
suspend fun deleteRecord(
3737
readingRecordId: String,

core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/di/DataGraph.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ package com.ninecraft.booket.core.data.impl.di
22

33
import com.ninecraft.booket.core.data.api.repository.AuthRepository
44
import com.ninecraft.booket.core.data.api.repository.BookRepository
5+
import com.ninecraft.booket.core.data.api.repository.EmotionRepository
56
import com.ninecraft.booket.core.data.api.repository.RecordRepository
67
import com.ninecraft.booket.core.data.api.repository.RemoteConfigRepository
78
import com.ninecraft.booket.core.data.api.repository.UserRepository
89
import com.ninecraft.booket.core.data.impl.repository.DefaultAuthRepository
910
import com.ninecraft.booket.core.data.impl.repository.DefaultBookRepository
11+
import com.ninecraft.booket.core.data.impl.repository.DefaultEmotionRepository
1012
import com.ninecraft.booket.core.data.impl.repository.DefaultRecordRepository
1113
import com.ninecraft.booket.core.data.impl.repository.DefaultRemoteConfigRepository
1214
import com.ninecraft.booket.core.data.impl.repository.DefaultUserRepository
@@ -23,6 +25,9 @@ interface DataGraph {
2325
@Binds
2426
val DefaultBookRepository.bind: BookRepository
2527

28+
@Binds
29+
val DefaultEmotionRepository.bind: EmotionRepository
30+
2631
@Binds
2732
val DefaultRecordRepository.bind: RecordRepository
2833

core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/mapper/ResponseToModel.kt

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,26 @@
11
package com.ninecraft.booket.core.data.impl.mapper
22

33
import com.ninecraft.booket.core.common.extensions.decodeHtmlEntities
4-
import com.ninecraft.booket.core.common.extensions.toFormattedDate
54
import com.ninecraft.booket.core.model.BookDetailModel
65
import com.ninecraft.booket.core.model.BookSearchModel
76
import com.ninecraft.booket.core.model.BookSummaryModel
87
import com.ninecraft.booket.core.model.BookUpsertModel
8+
import com.ninecraft.booket.core.model.DetailEmotionModel
99
import com.ninecraft.booket.core.model.Emotion
10+
import com.ninecraft.booket.core.model.EmotionCode
11+
import com.ninecraft.booket.core.model.EmotionGroupModel
12+
import com.ninecraft.booket.core.model.EmotionGroupsModel
1013
import com.ninecraft.booket.core.model.EmotionModel
1114
import com.ninecraft.booket.core.model.HomeModel
1215
import com.ninecraft.booket.core.model.LibraryBookSummaryModel
1316
import com.ninecraft.booket.core.model.LibraryBooksModel
1417
import com.ninecraft.booket.core.model.LibraryModel
1518
import com.ninecraft.booket.core.model.PageInfoModel
19+
import com.ninecraft.booket.core.model.PrimaryEmotionModel
1620
import com.ninecraft.booket.core.model.ReadingRecordModel
21+
import com.ninecraft.booket.core.model.ReadingRecordModelV2
1722
import com.ninecraft.booket.core.model.ReadingRecordsModel
1823
import com.ninecraft.booket.core.model.RecentBookModel
19-
import com.ninecraft.booket.core.model.RecordDetailModel
2024
import com.ninecraft.booket.core.model.RecordRegisterModel
2125
import com.ninecraft.booket.core.model.SeedModel
2226
import com.ninecraft.booket.core.model.TermsAgreementModel
@@ -26,17 +30,21 @@ import com.ninecraft.booket.core.network.response.BookSearchResponse
2630
import com.ninecraft.booket.core.network.response.BookSummary
2731
import com.ninecraft.booket.core.network.response.BookUpsertResponse
2832
import com.ninecraft.booket.core.network.response.Category
33+
import com.ninecraft.booket.core.network.response.DetailEmotion
34+
import com.ninecraft.booket.core.network.response.EmotionGroup
35+
import com.ninecraft.booket.core.network.response.EmotionGroupsResponse
2936
import com.ninecraft.booket.core.network.response.GuestBookSearchResponse
3037
import com.ninecraft.booket.core.network.response.GuestBookSummary
3138
import com.ninecraft.booket.core.network.response.HomeResponse
3239
import com.ninecraft.booket.core.network.response.LibraryBookSummary
3340
import com.ninecraft.booket.core.network.response.LibraryBooks
3441
import com.ninecraft.booket.core.network.response.LibraryResponse
3542
import com.ninecraft.booket.core.network.response.PageInfo
43+
import com.ninecraft.booket.core.network.response.PrimaryEmotion
3644
import com.ninecraft.booket.core.network.response.ReadingRecord
45+
import com.ninecraft.booket.core.network.response.ReadingRecordV2
3746
import com.ninecraft.booket.core.network.response.ReadingRecordsResponse
3847
import com.ninecraft.booket.core.network.response.RecentBook
39-
import com.ninecraft.booket.core.network.response.RecordDetailResponse
4048
import com.ninecraft.booket.core.network.response.RecordRegisterResponse
4149
import com.ninecraft.booket.core.network.response.SeedResponse
4250
import com.ninecraft.booket.core.network.response.TermsAgreementResponse
@@ -187,6 +195,28 @@ internal fun PageInfo.toModel(): PageInfoModel {
187195
)
188196
}
189197

198+
internal fun EmotionGroupsResponse.toModel(): EmotionGroupsModel {
199+
return EmotionGroupsModel(
200+
emotions = emotions.map { it.toModel() },
201+
)
202+
}
203+
204+
internal fun EmotionGroup.toModel(): EmotionGroupModel {
205+
val code = EmotionCode.fromCode(code) ?: EmotionCode.OTHER
206+
return EmotionGroupModel(
207+
code = code,
208+
displayName = displayName,
209+
detailEmotions = detailEmotions.map { it.toModel() },
210+
)
211+
}
212+
213+
internal fun DetailEmotion.toModel(): DetailEmotionModel {
214+
return DetailEmotionModel(
215+
id = id,
216+
name = name,
217+
)
218+
}
219+
190220
internal fun RecordRegisterResponse.toModel(): RecordRegisterModel {
191221
return RecordRegisterModel(
192222
id = id,
@@ -220,27 +250,36 @@ internal fun ReadingRecord.toModel(): ReadingRecordModel {
220250
emotionTags = emotionTags,
221251
createdAt = createdAt,
222252
updatedAt = updatedAt,
223-
bookTitle = bookTitle,
224-
bookPublisher = bookPublisher,
225-
bookCoverImageUrl = bookCoverImageUrl,
226-
author = author,
253+
bookTitle = bookTitle ?: "",
254+
bookPublisher = bookPublisher ?: "",
255+
bookCoverImageUrl = bookCoverImageUrl ?: "",
256+
author = author ?: "",
227257
)
228258
}
229259

230-
internal fun RecordDetailResponse.toModel(): RecordDetailModel {
231-
return RecordDetailModel(
260+
internal fun ReadingRecordV2.toModel(): ReadingRecordModelV2 {
261+
return ReadingRecordModelV2(
232262
id = id,
233263
userBookId = userBookId,
234264
pageNumber = pageNumber,
235265
quote = quote,
236266
review = review ?: "",
237-
emotionTags = emotionTags,
238-
createdAt = createdAt.toFormattedDate(),
239-
updatedAt = updatedAt.toFormattedDate(),
240-
bookTitle = bookTitle,
241-
bookPublisher = bookPublisher,
242-
bookCoverImageUrl = bookCoverImageUrl,
243-
author = author,
267+
primaryEmotion = primaryEmotion.toModel(),
268+
detailEmotions = detailEmotions.map { it.toModel() },
269+
createdAt = createdAt,
270+
updatedAt = updatedAt,
271+
bookTitle = bookTitle ?: "",
272+
bookPublisher = bookPublisher ?: "",
273+
bookCoverImageUrl = bookCoverImageUrl ?: "",
274+
author = author ?: "",
275+
)
276+
}
277+
278+
internal fun PrimaryEmotion.toModel(): PrimaryEmotionModel {
279+
val code = EmotionCode.fromCode(code) ?: EmotionCode.OTHER
280+
return PrimaryEmotionModel(
281+
code = code,
282+
displayName = displayName,
244283
)
245284
}
246285

0 commit comments

Comments
 (0)