From cf2f31277141c3f93a3db1b3f78b49197823bbdb Mon Sep 17 00:00:00 2001 From: soyeonLee126 Date: Fri, 30 Jan 2026 17:01:06 +0900 Subject: [PATCH 01/13] feat(domain): add ClassificationResult and ClassificationRepository --- .../fooddiary/domain/model/ClassificationResult.kt | 10 ++++++++++ .../domain/repository/ClassificationRepository.kt | 7 +++++++ 2 files changed, 17 insertions(+) create mode 100644 domain/src/main/kotlin/com/nexters/fooddiary/domain/model/ClassificationResult.kt create mode 100644 domain/src/main/kotlin/com/nexters/fooddiary/domain/repository/ClassificationRepository.kt diff --git a/domain/src/main/kotlin/com/nexters/fooddiary/domain/model/ClassificationResult.kt b/domain/src/main/kotlin/com/nexters/fooddiary/domain/model/ClassificationResult.kt new file mode 100644 index 0000000..268898d --- /dev/null +++ b/domain/src/main/kotlin/com/nexters/fooddiary/domain/model/ClassificationResult.kt @@ -0,0 +1,10 @@ +package com.nexters.fooddiary.domain.model + +data class ClassificationResult( + val isFood: Boolean, + val foodConfidence: Float, + val notFoodConfidence: Float +) { + val maxConfidence: Float + get() = maxOf(foodConfidence, notFoodConfidence) +} diff --git a/domain/src/main/kotlin/com/nexters/fooddiary/domain/repository/ClassificationRepository.kt b/domain/src/main/kotlin/com/nexters/fooddiary/domain/repository/ClassificationRepository.kt new file mode 100644 index 0000000..2691906 --- /dev/null +++ b/domain/src/main/kotlin/com/nexters/fooddiary/domain/repository/ClassificationRepository.kt @@ -0,0 +1,7 @@ +package com.nexters.fooddiary.domain.repository + +import com.nexters.fooddiary.domain.model.ClassificationResult + +interface ClassificationRepository { + suspend fun classifyImage(uriString: String): ClassificationResult? +} From 17b28f4561203ce95189eba2b7801b4e00646294 Mon Sep 17 00:00:00 2001 From: soyeonLee126 Date: Fri, 30 Jan 2026 17:01:10 +0900 Subject: [PATCH 02/13] feat(domain): add ClassifyImageUseCase and GetTodayFoodPhotosUseCase --- .../domain/repository/MediaRepository.kt | 20 --------- .../domain/usecase/ClassifyImageUseCase.kt | 13 ++++++ .../usecase/GetTodayFoodPhotosUseCase.kt | 44 +++++++++++++++++++ 3 files changed, 57 insertions(+), 20 deletions(-) create mode 100644 domain/src/main/kotlin/com/nexters/fooddiary/domain/usecase/ClassifyImageUseCase.kt create mode 100644 domain/src/main/kotlin/com/nexters/fooddiary/domain/usecase/GetTodayFoodPhotosUseCase.kt diff --git a/domain/src/main/kotlin/com/nexters/fooddiary/domain/repository/MediaRepository.kt b/domain/src/main/kotlin/com/nexters/fooddiary/domain/repository/MediaRepository.kt index 598ea49..69b543e 100644 --- a/domain/src/main/kotlin/com/nexters/fooddiary/domain/repository/MediaRepository.kt +++ b/domain/src/main/kotlin/com/nexters/fooddiary/domain/repository/MediaRepository.kt @@ -4,29 +4,9 @@ import com.nexters.fooddiary.domain.model.MediaItem import java.time.LocalDate import java.time.YearMonth -/** - * 미디어 데이터 Repository 인터페이스 - */ interface MediaRepository { - /** - * 특정 월의 사진을 날짜별로 그룹화하여 반환 - */ suspend fun getPhotosByMonth(yearMonth: YearMonth): Map> - - /** - * 특정 월의 날짜별 사진 개수를 반환 - */ suspend fun getPhotoCountByDate(yearMonth: YearMonth): Map - - /** - * 전체 앨범의 모든 사진을 날짜별로 그룹화하여 반환 - * 성능 비교를 위한 전체 스캔 함수 - */ suspend fun getAllPhotos(): Map> - - /** - * 전체 앨범의 날짜별 사진 개수를 반환 - * 성능 비교를 위한 전체 스캔 함수 - */ suspend fun getAllPhotoCountByDate(): Map } diff --git a/domain/src/main/kotlin/com/nexters/fooddiary/domain/usecase/ClassifyImageUseCase.kt b/domain/src/main/kotlin/com/nexters/fooddiary/domain/usecase/ClassifyImageUseCase.kt new file mode 100644 index 0000000..9c5be7b --- /dev/null +++ b/domain/src/main/kotlin/com/nexters/fooddiary/domain/usecase/ClassifyImageUseCase.kt @@ -0,0 +1,13 @@ +package com.nexters.fooddiary.domain.usecase + +import com.nexters.fooddiary.domain.model.ClassificationResult +import com.nexters.fooddiary.domain.repository.ClassificationRepository +import javax.inject.Inject + +class ClassifyImageUseCase @Inject constructor( + private val classificationRepository: ClassificationRepository +) { + suspend operator fun invoke(uriString: String): ClassificationResult? { + return classificationRepository.classifyImage(uriString) + } +} diff --git a/domain/src/main/kotlin/com/nexters/fooddiary/domain/usecase/GetTodayFoodPhotosUseCase.kt b/domain/src/main/kotlin/com/nexters/fooddiary/domain/usecase/GetTodayFoodPhotosUseCase.kt new file mode 100644 index 0000000..39b25ae --- /dev/null +++ b/domain/src/main/kotlin/com/nexters/fooddiary/domain/usecase/GetTodayFoodPhotosUseCase.kt @@ -0,0 +1,44 @@ +package com.nexters.fooddiary.domain.usecase + +import com.nexters.fooddiary.domain.repository.ClassificationRepository +import com.nexters.fooddiary.domain.repository.MediaRepository +import java.time.LocalDate +import java.time.YearMonth +import javax.inject.Inject + +class GetTodayFoodPhotosUseCase @Inject constructor( + private val mediaRepository: MediaRepository, + private val classificationRepository: ClassificationRepository +) { + suspend operator fun invoke(): List { + val today = LocalDate.now() + val currentMonth = YearMonth.now() + + val photosByMonth = mediaRepository.getPhotosByMonth(currentMonth) + val todayPhotos = photosByMonth[today] ?: emptyList() + + if (todayPhotos.isEmpty()) { + return emptyList() + } + + val classifiedPhotos = todayPhotos.mapNotNull { mediaItem -> + val uriString = mediaItem.uri + val result = classificationRepository.classifyImage(uriString) + + if (result != null && result.isFood) { + ClassifiedPhoto(uriString, result) + } else { + null + } + } + + return classifiedPhotos + .sortedByDescending { it.result.foodConfidence } + .map { it.uriString } + } + + private data class ClassifiedPhoto( + val uriString: String, + val result: com.nexters.fooddiary.domain.model.ClassificationResult + ) +} From c156dca008b8aa51804aa7fafd8dd06ca04d123c Mon Sep 17 00:00:00 2001 From: soyeonLee126 Date: Fri, 30 Jan 2026 17:01:17 +0900 Subject: [PATCH 03/13] feat(data): implement ClassificationRepository with caching --- data/build.gradle.kts | 2 + .../nexters/fooddiary/data/di/MediaModule.kt | 8 +++ .../ClassificationRepositoryImpl.kt | 57 +++++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 data/src/main/java/com/nexters/fooddiary/data/repository/ClassificationRepositoryImpl.kt diff --git a/data/build.gradle.kts b/data/build.gradle.kts index 1611bbf..be09033 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -47,6 +47,7 @@ android { dependencies { // Modules implementation(projects.core.common) + implementation(projects.core.classification) implementation(projects.domain) // Coroutines @@ -65,6 +66,7 @@ dependencies { // Hilt implementation(libs.hilt.android) + implementation(project(":core:classification")) ksp(libs.hilt.compiler) // Firebase Auth diff --git a/data/src/main/java/com/nexters/fooddiary/data/di/MediaModule.kt b/data/src/main/java/com/nexters/fooddiary/data/di/MediaModule.kt index c0b8963..ebf28c2 100644 --- a/data/src/main/java/com/nexters/fooddiary/data/di/MediaModule.kt +++ b/data/src/main/java/com/nexters/fooddiary/data/di/MediaModule.kt @@ -2,7 +2,9 @@ package com.nexters.fooddiary.data.di import android.content.ContentResolver import android.content.Context +import com.nexters.fooddiary.data.repository.ClassificationRepositoryImpl import com.nexters.fooddiary.data.repository.MediaRepositoryImpl +import com.nexters.fooddiary.domain.repository.ClassificationRepository import com.nexters.fooddiary.domain.repository.MediaRepository import dagger.Binds import dagger.Module @@ -32,4 +34,10 @@ abstract class MediaRepositoryModule { internal abstract fun bindMediaRepository( mediaRepositoryImpl: MediaRepositoryImpl ): MediaRepository + + @Binds + @Singleton + internal abstract fun bindClassificationRepository( + classificationRepositoryImpl: ClassificationRepositoryImpl + ): ClassificationRepository } diff --git a/data/src/main/java/com/nexters/fooddiary/data/repository/ClassificationRepositoryImpl.kt b/data/src/main/java/com/nexters/fooddiary/data/repository/ClassificationRepositoryImpl.kt new file mode 100644 index 0000000..b45241a --- /dev/null +++ b/data/src/main/java/com/nexters/fooddiary/data/repository/ClassificationRepositoryImpl.kt @@ -0,0 +1,57 @@ +package com.nexters.fooddiary.data.repository + +import android.content.Context +import android.net.Uri +import com.nexters.fooddiary.core.classification.FoodClassificationResult +import com.nexters.fooddiary.core.classification.FoodClassifier +import com.nexters.fooddiary.core.classification.ImageUtils +import com.nexters.fooddiary.domain.model.ClassificationResult +import com.nexters.fooddiary.domain.repository.ClassificationRepository +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import javax.inject.Inject +import javax.inject.Singleton + + +@Singleton +class ClassificationRepositoryImpl @Inject constructor( + private val foodClassifier: FoodClassifier, + @ApplicationContext private val context: Context +) : ClassificationRepository { + + private val classificationCache = mutableMapOf() + + override suspend fun classifyImage(uriString: String): ClassificationResult? { + return withContext(Dispatchers.IO) { + val cachedResult = classificationCache[uriString] + + cachedResult?.let { + return@let cachedResult.toDomainModel() + } + + val uri = Uri.parse(uriString) + val bitmap = ImageUtils.uriToBitmap(context, uri) ?: return@withContext null + + try { + val result = foodClassifier.classifyAsync(bitmap) + classificationCache[uriString] = result + result.toDomainModel() + } catch (e: Exception) { + null // 에러 notify 정책 논의 필요 + } + } + } + + fun clearCache() { + classificationCache.clear() + } + + private fun FoodClassificationResult.toDomainModel(): ClassificationResult { + return ClassificationResult( + isFood = this.isFood, + foodConfidence = this.foodConfidence, + notFoodConfidence = this.notFoodConfidence + ) + } +} From 46d557ac710c290acfda60b8af08610d443a1508 Mon Sep 17 00:00:00 2001 From: soyeonLee126 Date: Fri, 30 Jan 2026 17:01:25 +0900 Subject: [PATCH 04/13] chore(gradle): downgrade Compose BOM, add Coil and catalog entries --- gradle/libs.versions.toml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4a05eed..32070ac 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,7 +11,7 @@ espressoCore = "3.7.0" lifecycleRuntimeKtx = "2.8.7" lifecycleViewmodelCompose = "2.8.7" activityCompose = "1.9.3" -composeBom = "2025.12.01" +composeBom = "2024.11.00" hilt = "2.58" hiltNavigationCompose = "1.3.0" retrofit = "3.0.0" @@ -37,6 +37,9 @@ minSdk = "26" compileSdk = "36" targetSdk = "36" calendar = "2.6.2" +uiToolingPreviewAndroid = "1.10.1" +coil = "2.7.0" +material3Android = "1.4.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -129,6 +132,11 @@ androidx-datastore-preferences = { group = "androidx.datastore", name = "datasto firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" } firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics" } firebase-auth = { group = "com.google.firebase", name = "firebase-auth" } +androidx-ui-tooling-preview-android = { group = "androidx.compose.ui", name = "ui-tooling-preview-android", version.ref = "uiToolingPreviewAndroid" } + +# Coil +coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" } +androidx-material3-android = { group = "androidx.compose.material3", name = "material3-android", version.ref = "material3Android" } [plugins] google-gms-services = { id = "com.google.gms.google-services", version = "4.4.4" } From e1a76425d84f85ab066ad2849adf5baee8b49784 Mon Sep 17 00:00:00 2001 From: soyeonLee126 Date: Fri, 30 Jan 2026 17:01:28 +0900 Subject: [PATCH 05/13] feat(theme): set default background to GrayBase (#222222) --- app/src/main/res/values/themes.xml | 2 +- .../nexters/fooddiary/core/ui/theme/Theme.kt | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index f3ddee4..098f78c 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,5 +1,5 @@ -