From 857eb3dafbbfbe5df78160278d040bcc89955422 Mon Sep 17 00:00:00 2001 From: Ham BeomJoon Date: Mon, 19 Jan 2026 13:57:19 +0900 Subject: [PATCH 01/19] =?UTF-8?q?feat:=20core:data=20=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 새로운 데이터 레이어 모듈인 `core:data`를 추가하고 기본 설정을 완료했습니다. * `core:data` 모듈 생성 및 `settings.gradle.kts`에 포함 * `build.gradle.kts`에 Android 라이브러리 및 Hilt 플러그인 설정 * 기본 AndroidManifest.xml, ProGuard 규칙 및 테스트 예제 코드 추가 --- Prezel/core/data/.gitignore | 1 + Prezel/core/data/build.gradle.kts | 13 ++++++++++++ Prezel/core/data/consumer-rules.pro | 0 Prezel/core/data/proguard-rules.pro | 21 +++++++++++++++++++ .../java/com/team/prezel/core/data/.gitkeep | 0 Prezel/core/data/src/main/AndroidManifest.xml | 4 ++++ .../java/com/team/prezel/core/data/.gitkeep | 0 Prezel/settings.gradle.kts | 1 + 8 files changed, 40 insertions(+) create mode 100644 Prezel/core/data/.gitignore create mode 100644 Prezel/core/data/build.gradle.kts create mode 100644 Prezel/core/data/consumer-rules.pro create mode 100644 Prezel/core/data/proguard-rules.pro create mode 100644 Prezel/core/data/src/androidTest/java/com/team/prezel/core/data/.gitkeep create mode 100644 Prezel/core/data/src/main/AndroidManifest.xml create mode 100644 Prezel/core/data/src/test/java/com/team/prezel/core/data/.gitkeep diff --git a/Prezel/core/data/.gitignore b/Prezel/core/data/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/Prezel/core/data/.gitignore @@ -0,0 +1 @@ +/build diff --git a/Prezel/core/data/build.gradle.kts b/Prezel/core/data/build.gradle.kts new file mode 100644 index 00000000..8f8a25e2 --- /dev/null +++ b/Prezel/core/data/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + alias(libs.plugins.prezel.android.library) + alias(libs.plugins.prezel.hilt) +} + +android { + namespace = "com.team.prezel.core.data" + testOptions.unitTests.isIncludeAndroidResources = true +} + +dependencies { + // api(projects.core.network) +} diff --git a/Prezel/core/data/consumer-rules.pro b/Prezel/core/data/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/Prezel/core/data/proguard-rules.pro b/Prezel/core/data/proguard-rules.pro new file mode 100644 index 00000000..f1b42451 --- /dev/null +++ b/Prezel/core/data/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/Prezel/core/data/src/androidTest/java/com/team/prezel/core/data/.gitkeep b/Prezel/core/data/src/androidTest/java/com/team/prezel/core/data/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/Prezel/core/data/src/main/AndroidManifest.xml b/Prezel/core/data/src/main/AndroidManifest.xml new file mode 100644 index 00000000..8bdb7e14 --- /dev/null +++ b/Prezel/core/data/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/Prezel/core/data/src/test/java/com/team/prezel/core/data/.gitkeep b/Prezel/core/data/src/test/java/com/team/prezel/core/data/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/Prezel/settings.gradle.kts b/Prezel/settings.gradle.kts index a8f98851..be3b2335 100644 --- a/Prezel/settings.gradle.kts +++ b/Prezel/settings.gradle.kts @@ -31,3 +31,4 @@ rootProject.name = "Prezel" enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") include(":app") +include(":core:data") From ef77abfd2c466afa47575dca08b3e627bce60f80 Mon Sep 17 00:00:00 2001 From: Ham BeomJoon Date: Mon, 19 Jan 2026 14:11:46 +0900 Subject: [PATCH 02/19] =?UTF-8?q?feat:=20core:network=20=EB=AA=A8=EB=93=88?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EB=84=A4=ED=8A=B8=EC=9B=8C?= =?UTF-8?q?=ED=81=AC=20=EB=9D=BC=EC=9D=B4=EB=B8=8C=EB=9F=AC=EB=A6=AC=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 네트워크 통신을 위한 `core:network` 모듈을 신규 생성하고 관련 의존성을 추가했습니다. * `core:network` 모듈 신규 생성 및 프로젝트 포함 * Ktor, Ktorfit, Kotlinx Serialization 의존성 및 버전 설정 추가 * `local.properties`에서 `BACKEND_URL`을 읽어 `BuildConfig`에 생성하는 로직 추가 * Hilt 및 프로젝트 라이브러리 플러그인 적용 --- Prezel/core/network/.gitignore | 1 + Prezel/core/network/build.gradle.kts | 52 +++++++++++++++++++ Prezel/core/network/consumer-rules.pro | 0 Prezel/core/network/proguard-rules.pro | 21 ++++++++ .../com/team/prezel/core/network/.gitkeep | 0 .../core/network/src/main/AndroidManifest.xml | 4 ++ .../com/team/prezel/core/network/.gitkeep | 0 Prezel/gradle/libs.versions.toml | 15 ++++++ Prezel/settings.gradle.kts | 1 + 9 files changed, 94 insertions(+) create mode 100644 Prezel/core/network/.gitignore create mode 100644 Prezel/core/network/build.gradle.kts create mode 100644 Prezel/core/network/consumer-rules.pro create mode 100644 Prezel/core/network/proguard-rules.pro create mode 100644 Prezel/core/network/src/androidTest/java/com/team/prezel/core/network/.gitkeep create mode 100644 Prezel/core/network/src/main/AndroidManifest.xml create mode 100644 Prezel/core/network/src/test/java/com/team/prezel/core/network/.gitkeep diff --git a/Prezel/core/network/.gitignore b/Prezel/core/network/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/Prezel/core/network/.gitignore @@ -0,0 +1 @@ +/build diff --git a/Prezel/core/network/build.gradle.kts b/Prezel/core/network/build.gradle.kts new file mode 100644 index 00000000..25ce3613 --- /dev/null +++ b/Prezel/core/network/build.gradle.kts @@ -0,0 +1,52 @@ +import com.android.build.api.variant.BuildConfigField +import java.io.StringReader +import java.util.Properties + +plugins { + alias(libs.plugins.prezel.android.library) + alias(libs.plugins.prezel.hilt) +} + +android { + buildFeatures { + buildConfig = true + } + + namespace = "com.team.prezel.core.network" + testOptions.unitTests.isIncludeAndroidResources = true +} + +dependencies { + implementation(libs.ktor.client.core) + implementation(libs.ktor.client.okhttp) + implementation(libs.ktor.client.content.negotiation) + implementation(libs.ktor.serialization.kotlinx.json) + implementation(libs.ktor.client.logging) + + // Ktorfit + implementation(libs.ktorfit.lib) + ksp(libs.ktorfit.ksp) + + testImplementation(libs.kotlinx.coroutines.test) +} + +val backendUrl = providers + .fileContents( + isolated.rootProject.projectDirectory.file("local.properties"), + ).asText + .map { text: String -> + val properties = Properties() + properties.load(StringReader(text)) + properties.getProperty("BACKEND_URL") + }.orElse("http://example.com") + +androidComponents { + onVariants { + it.buildConfigFields!!.put( + "BACKEND_URL", + backendUrl.map { value -> + BuildConfigField(type = "String", value = """"$value"""", comment = null) + }, + ) + } +} diff --git a/Prezel/core/network/consumer-rules.pro b/Prezel/core/network/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/Prezel/core/network/proguard-rules.pro b/Prezel/core/network/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/Prezel/core/network/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/Prezel/core/network/src/androidTest/java/com/team/prezel/core/network/.gitkeep b/Prezel/core/network/src/androidTest/java/com/team/prezel/core/network/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/Prezel/core/network/src/main/AndroidManifest.xml b/Prezel/core/network/src/main/AndroidManifest.xml new file mode 100644 index 00000000..8bdb7e14 --- /dev/null +++ b/Prezel/core/network/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/Prezel/core/network/src/test/java/com/team/prezel/core/network/.gitkeep b/Prezel/core/network/src/test/java/com/team/prezel/core/network/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/Prezel/gradle/libs.versions.toml b/Prezel/gradle/libs.versions.toml index 3fceabbc..7de51abf 100644 --- a/Prezel/gradle/libs.versions.toml +++ b/Prezel/gradle/libs.versions.toml @@ -1,6 +1,7 @@ [versions] agp = "8.13.2" kotlin = "2.3.0" +kotlinxCoroutines = "1.10.2" coreKtx = "1.17.0" junit = "4.13.2" junitVersion = "1.3.0" @@ -14,6 +15,9 @@ ksp = "2.3.4" activityKtx = "1.12.2" hilt = "2.57.2" desugarJdk = "2.0.3" +ktor = "3.3.3" +ktorfit = "2.7.2" +kotlinx-serialization = "1.7.3" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -38,6 +42,16 @@ hilt-android-testing = { group = "com.google.dagger", name = "hilt-android-testi hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" } hilt-core = { group = "com.google.dagger", name = "hilt-core", version.ref = "hilt" } kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.ref = "kotlin" } +kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" } +kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" } +kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" } +ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } +ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } +ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } +ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } +ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } +ktorfit-lib = { module = "de.jensklingenberg.ktorfit:ktorfit-lib", version.ref = "ktorfit" } +ktorfit-ksp = { module = "de.jensklingenberg.ktorfit:ktorfit-ksp", version.ref = "ktorfit" } # Dependencies of the included build-logic android-gradleApiPlugin = { group = "com.android.tools.build", name = "gradle-api", version.ref = "agp" } @@ -56,6 +70,7 @@ detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } # Plugins defined by this project prezel-android-application = { id = "prezel.android.application" } diff --git a/Prezel/settings.gradle.kts b/Prezel/settings.gradle.kts index be3b2335..97683e5d 100644 --- a/Prezel/settings.gradle.kts +++ b/Prezel/settings.gradle.kts @@ -32,3 +32,4 @@ rootProject.name = "Prezel" enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") include(":app") include(":core:data") +include(":core:network") From 335d4f3275fdb0ddec3cf312d0e388e488fb842b Mon Sep 17 00:00:00 2001 From: Ham BeomJoon Date: Mon, 19 Jan 2026 14:28:14 +0900 Subject: [PATCH 03/19] =?UTF-8?q?feat:=20=EB=84=A4=ED=8A=B8=EC=9B=8C?= =?UTF-8?q?=ED=81=AC=20=EB=AA=A8=EB=93=88=20=EA=B8=B0=EB=B3=B8=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20=EC=9C=A0=ED=8B=B8=EB=A6=AC=ED=8B=B0=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 네트워크 통신을 위한 기본적인 설정과 응답 처리를 위한 유틸리티를 추가했습니다. * `AndroidManifest.xml` 내 인터넷 권한 추가 * Ktor, Ktorfit 기반의 `NetworkModule` (Dagger Hilt) 구현 * API 응답 상태 처리를 위한 `ApiResponse` sealed interface 정의 * 예외 처리를 포함한 `safeApiCall` 유틸리티 함수 추가 --- .../core/network/src/main/AndroidManifest.xml | 2 + .../team/prezel/core/network/SafeApiCall.kt | 20 +++++++ .../prezel/core/network/di/NetworkModule.kt | 59 +++++++++++++++++++ .../prezel/core/network/model/ApiResponse.kt | 14 +++++ 4 files changed, 95 insertions(+) create mode 100644 Prezel/core/network/src/main/java/com/team/prezel/core/network/SafeApiCall.kt create mode 100644 Prezel/core/network/src/main/java/com/team/prezel/core/network/di/NetworkModule.kt create mode 100644 Prezel/core/network/src/main/java/com/team/prezel/core/network/model/ApiResponse.kt diff --git a/Prezel/core/network/src/main/AndroidManifest.xml b/Prezel/core/network/src/main/AndroidManifest.xml index 8bdb7e14..5e7684f5 100644 --- a/Prezel/core/network/src/main/AndroidManifest.xml +++ b/Prezel/core/network/src/main/AndroidManifest.xml @@ -1,4 +1,6 @@ + + diff --git a/Prezel/core/network/src/main/java/com/team/prezel/core/network/SafeApiCall.kt b/Prezel/core/network/src/main/java/com/team/prezel/core/network/SafeApiCall.kt new file mode 100644 index 00000000..db61f3c8 --- /dev/null +++ b/Prezel/core/network/src/main/java/com/team/prezel/core/network/SafeApiCall.kt @@ -0,0 +1,20 @@ +package com.team.prezel.core.network + +import com.team.prezel.core.network.model.ApiResponse +import io.ktor.client.call.body +import io.ktor.client.plugins.ClientRequestException +import io.ktor.client.plugins.ServerResponseException +import io.ktor.client.statement.HttpResponse +import java.io.IOException + +suspend inline fun safeApiCall(apiCall: () -> HttpResponse): ApiResponse = + try { + val response = apiCall() + ApiResponse.Success(response.body()) + } catch (e: ClientRequestException) { + ApiResponse.Error(e.response.status.value, e.message) + } catch (e: ServerResponseException) { + ApiResponse.Error(e.response.status.value, e.message) + } catch (e: IOException) { + ApiResponse.NetworkError + } diff --git a/Prezel/core/network/src/main/java/com/team/prezel/core/network/di/NetworkModule.kt b/Prezel/core/network/src/main/java/com/team/prezel/core/network/di/NetworkModule.kt new file mode 100644 index 00000000..e8d2e2ce --- /dev/null +++ b/Prezel/core/network/src/main/java/com/team/prezel/core/network/di/NetworkModule.kt @@ -0,0 +1,59 @@ +package com.team.prezel.core.network.di + +import com.team.prezel.core.network.BuildConfig +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import de.jensklingenberg.ktorfit.Ktorfit +import io.ktor.client.HttpClient +import io.ktor.client.engine.okhttp.OkHttp +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.defaultRequest +import io.ktor.client.plugins.logging.LogLevel +import io.ktor.client.plugins.logging.Logging +import io.ktor.http.ContentType +import io.ktor.http.contentType +import io.ktor.serialization.kotlinx.json.json +import kotlinx.serialization.json.Json +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object NetworkModule { + @Provides + @Singleton + fun provideJson(): Json = + Json { + ignoreUnknownKeys = true + coerceInputValues = true + encodeDefaults = true + prettyPrint = BuildConfig.DEBUG + } + + @Provides + @Singleton + fun provideHttpClient(json: Json): HttpClient = + HttpClient(OkHttp) { + install(ContentNegotiation) { + json(json) + } + + install(Logging) { + level = if (BuildConfig.DEBUG) LogLevel.BODY else LogLevel.NONE + } + + defaultRequest { + url(BuildConfig.BACKEND_URL) + contentType(ContentType.Application.Json) + } + } + + @Provides + @Singleton + fun provideKtorfit(httpClient: HttpClient): Ktorfit = + Ktorfit + .Builder() + .httpClient(httpClient) + .build() +} diff --git a/Prezel/core/network/src/main/java/com/team/prezel/core/network/model/ApiResponse.kt b/Prezel/core/network/src/main/java/com/team/prezel/core/network/model/ApiResponse.kt new file mode 100644 index 00000000..99bfe30c --- /dev/null +++ b/Prezel/core/network/src/main/java/com/team/prezel/core/network/model/ApiResponse.kt @@ -0,0 +1,14 @@ +package com.team.prezel.core.network.model + +sealed interface ApiResponse { + data class Success( + val data: T, + ) : ApiResponse + + data class Error( + val code: Int, + val message: String, + ) : ApiResponse + + data object NetworkError : ApiResponse +} From e6b6ec9a5cf63be3fd63c8afa8efb1bb63036aa0 Mon Sep 17 00:00:00 2001 From: Ham BeomJoon Date: Mon, 19 Jan 2026 15:56:26 +0900 Subject: [PATCH 04/19] =?UTF-8?q?feat:=20=EB=84=A4=ED=8A=B8=EC=9B=8C?= =?UTF-8?q?=ED=81=AC=20=EB=AA=A8=EB=93=88=20=EA=B5=AC=EC=A1=B0=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=20=EB=B0=8F=20Ktorfit=20=EC=BB=A8=EB=B2=84=ED=84=B0?= =?UTF-8?q?=20=EB=8F=84=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * `safeApiCall`을 제거하고 Ktorfit 전용 `ApiResponseConverterFactory`를 추가하여 API 응답 처리를 자동화했습니다. * Timber 로깅 라이브러리를 도입하고 Ktor 클라이언트 및 애플리케이션에 식재(plant)했습니다. * `BACKEND_URL` 설정 명칭을 `BASE_URL`로 변경했습니다. * `core:network` 모듈에 Kotlinx Serialization 플러그인을 추가하고 관련 의존성을 업데이트했습니다. * `PrezelApplication`을 추가하여 Hilt 및 Timber 초기화 로직을 반영했습니다. * `core:data` 모듈에 기초적인 Hilt 모듈(`RepositoryModule`)을 추가하고 의존성을 구성했습니다. --- Prezel/app/build.gradle.kts | 11 +++- Prezel/app/src/main/AndroidManifest.xml | 2 +- .../java/com/team/prezel/PrezelApplication.kt | 15 +++++ Prezel/core/data/build.gradle.kts | 4 +- Prezel/core/data/src/main/AndroidManifest.xml | 2 +- .../team/prezel/core/data/RepositoryModule.kt | 9 +++ Prezel/core/network/build.gradle.kts | 7 ++- .../network/ApiResponseConverterFactory.kt | 56 +++++++++++++++++++ .../team/prezel/core/network/SafeApiCall.kt | 20 ------- .../prezel/core/network/di/NetworkModule.kt | 11 +++- Prezel/gradle/libs.versions.toml | 7 ++- 11 files changed, 116 insertions(+), 28 deletions(-) create mode 100644 Prezel/app/src/main/java/com/team/prezel/PrezelApplication.kt create mode 100644 Prezel/core/data/src/main/java/com/team/prezel/core/data/RepositoryModule.kt create mode 100644 Prezel/core/network/src/main/java/com/team/prezel/core/network/ApiResponseConverterFactory.kt delete mode 100644 Prezel/core/network/src/main/java/com/team/prezel/core/network/SafeApiCall.kt diff --git a/Prezel/app/build.gradle.kts b/Prezel/app/build.gradle.kts index 8b216b84..c761d987 100644 --- a/Prezel/app/build.gradle.kts +++ b/Prezel/app/build.gradle.kts @@ -1,14 +1,23 @@ plugins { - alias(libs.plugins.prezel.android.application) alias(libs.plugins.prezel.android.application.compose) + alias(libs.plugins.prezel.hilt) } android { namespace = "com.team.prezel" + + buildFeatures { + buildConfig = true + } } dependencies { + implementation(projects.core.data) + implementation(libs.androidx.activity.ktx) implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.timber) + + ksp(libs.kotlin.metadata.jvm) } diff --git a/Prezel/app/src/main/AndroidManifest.xml b/Prezel/app/src/main/AndroidManifest.xml index 7162ca24..639a9130 100644 --- a/Prezel/app/src/main/AndroidManifest.xml +++ b/Prezel/app/src/main/AndroidManifest.xml @@ -2,6 +2,7 @@ diff --git a/Prezel/app/src/main/java/com/team/prezel/PrezelApplication.kt b/Prezel/app/src/main/java/com/team/prezel/PrezelApplication.kt new file mode 100644 index 00000000..f05bcfd2 --- /dev/null +++ b/Prezel/app/src/main/java/com/team/prezel/PrezelApplication.kt @@ -0,0 +1,15 @@ +package com.team.prezel + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp +import timber.log.Timber + +@HiltAndroidApp +class PrezelApplication : Application() { + override fun onCreate() { + super.onCreate() + if (BuildConfig.DEBUG) { + Timber.plant(Timber.DebugTree()) + } + } +} diff --git a/Prezel/core/data/build.gradle.kts b/Prezel/core/data/build.gradle.kts index 8f8a25e2..7e02018b 100644 --- a/Prezel/core/data/build.gradle.kts +++ b/Prezel/core/data/build.gradle.kts @@ -9,5 +9,7 @@ android { } dependencies { - // api(projects.core.network) + implementation(projects.core.network) + + implementation(libs.kotlinx.coroutines.core) } diff --git a/Prezel/core/data/src/main/AndroidManifest.xml b/Prezel/core/data/src/main/AndroidManifest.xml index 8bdb7e14..e1000761 100644 --- a/Prezel/core/data/src/main/AndroidManifest.xml +++ b/Prezel/core/data/src/main/AndroidManifest.xml @@ -1,4 +1,4 @@ - + diff --git a/Prezel/core/data/src/main/java/com/team/prezel/core/data/RepositoryModule.kt b/Prezel/core/data/src/main/java/com/team/prezel/core/data/RepositoryModule.kt new file mode 100644 index 00000000..bbf6a3f3 --- /dev/null +++ b/Prezel/core/data/src/main/java/com/team/prezel/core/data/RepositoryModule.kt @@ -0,0 +1,9 @@ +package com.team.prezel.core.data + +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +abstract class RepositoryModule diff --git a/Prezel/core/network/build.gradle.kts b/Prezel/core/network/build.gradle.kts index 25ce3613..008b5ed0 100644 --- a/Prezel/core/network/build.gradle.kts +++ b/Prezel/core/network/build.gradle.kts @@ -5,6 +5,7 @@ import java.util.Properties plugins { alias(libs.plugins.prezel.android.library) alias(libs.plugins.prezel.hilt) + alias(libs.plugins.kotlinx.serialization) } android { @@ -22,6 +23,8 @@ dependencies { implementation(libs.ktor.client.content.negotiation) implementation(libs.ktor.serialization.kotlinx.json) implementation(libs.ktor.client.logging) + implementation(libs.kotlinx.serialization.json) + implementation(libs.timber) // Ktorfit implementation(libs.ktorfit.lib) @@ -37,13 +40,13 @@ val backendUrl = providers .map { text: String -> val properties = Properties() properties.load(StringReader(text)) - properties.getProperty("BACKEND_URL") + properties.getProperty("BASE_URL") }.orElse("http://example.com") androidComponents { onVariants { it.buildConfigFields!!.put( - "BACKEND_URL", + "BASE_URL", backendUrl.map { value -> BuildConfigField(type = "String", value = """"$value"""", comment = null) }, diff --git a/Prezel/core/network/src/main/java/com/team/prezel/core/network/ApiResponseConverterFactory.kt b/Prezel/core/network/src/main/java/com/team/prezel/core/network/ApiResponseConverterFactory.kt new file mode 100644 index 00000000..d8cbdd29 --- /dev/null +++ b/Prezel/core/network/src/main/java/com/team/prezel/core/network/ApiResponseConverterFactory.kt @@ -0,0 +1,56 @@ +package com.team.prezel.core.network + +import com.team.prezel.core.network.model.ApiResponse +import de.jensklingenberg.ktorfit.Ktorfit +import de.jensklingenberg.ktorfit.converter.Converter +import de.jensklingenberg.ktorfit.converter.KtorfitResult +import de.jensklingenberg.ktorfit.converter.TypeData +import io.ktor.client.call.body +import io.ktor.client.statement.HttpResponse +import io.ktor.http.isSuccess +import java.io.IOException + +class ApiResponseConverterFactory : Converter.Factory { + override fun suspendResponseConverter( + typeData: TypeData, + ktorfit: Ktorfit, + ): Converter.SuspendResponseConverter? { + if (typeData.typeInfo.type != ApiResponse::class) return null + + return object : Converter.SuspendResponseConverter> { + override suspend fun convert(result: KtorfitResult): ApiResponse = + when (result) { + is KtorfitResult.Success -> { + val response = result.response + try { + if (response.status.isSuccess()) { + val body = response.body(typeData.typeArgs.first().typeInfo) + ApiResponse.Success(body) + } else { + ApiResponse.Error( + code = response.status.value, + message = response.status.description, + ) + } + } catch (e: Exception) { + ApiResponse.Error( + code = response.status.value, + message = e.message ?: "Unknown error", + ) + } + } + + is KtorfitResult.Failure -> { + when (result.throwable) { + is IOException -> ApiResponse.NetworkError + + else -> ApiResponse.Error( + code = -1, + message = result.throwable.message ?: "Unknown error", + ) + } + } + } + } + } +} diff --git a/Prezel/core/network/src/main/java/com/team/prezel/core/network/SafeApiCall.kt b/Prezel/core/network/src/main/java/com/team/prezel/core/network/SafeApiCall.kt deleted file mode 100644 index db61f3c8..00000000 --- a/Prezel/core/network/src/main/java/com/team/prezel/core/network/SafeApiCall.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.team.prezel.core.network - -import com.team.prezel.core.network.model.ApiResponse -import io.ktor.client.call.body -import io.ktor.client.plugins.ClientRequestException -import io.ktor.client.plugins.ServerResponseException -import io.ktor.client.statement.HttpResponse -import java.io.IOException - -suspend inline fun safeApiCall(apiCall: () -> HttpResponse): ApiResponse = - try { - val response = apiCall() - ApiResponse.Success(response.body()) - } catch (e: ClientRequestException) { - ApiResponse.Error(e.response.status.value, e.message) - } catch (e: ServerResponseException) { - ApiResponse.Error(e.response.status.value, e.message) - } catch (e: IOException) { - ApiResponse.NetworkError - } diff --git a/Prezel/core/network/src/main/java/com/team/prezel/core/network/di/NetworkModule.kt b/Prezel/core/network/src/main/java/com/team/prezel/core/network/di/NetworkModule.kt index e8d2e2ce..112ee5b6 100644 --- a/Prezel/core/network/src/main/java/com/team/prezel/core/network/di/NetworkModule.kt +++ b/Prezel/core/network/src/main/java/com/team/prezel/core/network/di/NetworkModule.kt @@ -1,5 +1,6 @@ package com.team.prezel.core.network.di +import com.team.prezel.core.network.ApiResponseConverterFactory import com.team.prezel.core.network.BuildConfig import dagger.Module import dagger.Provides @@ -11,11 +12,13 @@ import io.ktor.client.engine.okhttp.OkHttp import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.defaultRequest import io.ktor.client.plugins.logging.LogLevel +import io.ktor.client.plugins.logging.Logger import io.ktor.client.plugins.logging.Logging import io.ktor.http.ContentType import io.ktor.http.contentType import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.json.Json +import timber.log.Timber import javax.inject.Singleton @Module @@ -40,11 +43,15 @@ object NetworkModule { } install(Logging) { + logger = object : Logger { + override fun log(message: String) { + Timber.tag("KtorClient").d(message) + } + } level = if (BuildConfig.DEBUG) LogLevel.BODY else LogLevel.NONE } defaultRequest { - url(BuildConfig.BACKEND_URL) contentType(ContentType.Application.Json) } } @@ -54,6 +61,8 @@ object NetworkModule { fun provideKtorfit(httpClient: HttpClient): Ktorfit = Ktorfit .Builder() + .baseUrl(BuildConfig.BASE_URL) .httpClient(httpClient) + .converterFactories(ApiResponseConverterFactory()) .build() } diff --git a/Prezel/gradle/libs.versions.toml b/Prezel/gradle/libs.versions.toml index 7de51abf..d779789f 100644 --- a/Prezel/gradle/libs.versions.toml +++ b/Prezel/gradle/libs.versions.toml @@ -1,6 +1,7 @@ [versions] agp = "8.13.2" kotlin = "2.3.0" +kotlinMetadataJvm = "2.3.0" kotlinxCoroutines = "1.10.2" coreKtx = "1.17.0" junit = "4.13.2" @@ -17,7 +18,8 @@ hilt = "2.57.2" desugarJdk = "2.0.3" ktor = "3.3.3" ktorfit = "2.7.2" -kotlinx-serialization = "1.7.3" +kotlinx-serialization = "1.9.0" +timber = "5.0.1" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -41,6 +43,7 @@ hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref hilt-android-testing = { group = "com.google.dagger", name = "hilt-android-testing", version.ref = "hilt" } hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" } hilt-core = { group = "com.google.dagger", name = "hilt-core", version.ref = "hilt" } +kotlin-metadata-jvm = { module = "org.jetbrains.kotlin:kotlin-metadata-jvm", version.ref = "kotlinMetadataJvm" } kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.ref = "kotlin" } kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" } kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" } @@ -52,6 +55,8 @@ ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } ktorfit-lib = { module = "de.jensklingenberg.ktorfit:ktorfit-lib", version.ref = "ktorfit" } ktorfit-ksp = { module = "de.jensklingenberg.ktorfit:ktorfit-ksp", version.ref = "ktorfit" } +kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serialization" } +timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" } # Dependencies of the included build-logic android-gradleApiPlugin = { group = "com.android.tools.build", name = "gradle-api", version.ref = "agp" } From a1a1ba315c9857d768c2245ef6f42421f39c9c00 Mon Sep 17 00:00:00 2001 From: Ham BeomJoon Date: Mon, 19 Jan 2026 16:36:13 +0900 Subject: [PATCH 05/19] =?UTF-8?q?refactor:=20ApiResponse=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=20=EC=B2=98=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `HttpClient` 설정에 `expectSuccess = true`를 추가하여 HTTP 에러 발생 시 예외를 던지도록 수정했습니다. - `ApiResponse`의 에러 타입을 `Failure` sealed interface로 통합하고, `HttpError`와 `NetworkError`로 세분화했습니다. - `ApiResponseConverterFactory`에서 `ResponseException` 처리를 추가하고, `CancellationException` 발생 시 예외를 다시 던지도록 로직을 개선했습니다. --- .../network/ApiResponseConverterFactory.kt | 35 +++++++------------ .../prezel/core/network/di/NetworkModule.kt | 2 ++ .../prezel/core/network/model/ApiResponse.kt | 11 +++--- 3 files changed, 21 insertions(+), 27 deletions(-) diff --git a/Prezel/core/network/src/main/java/com/team/prezel/core/network/ApiResponseConverterFactory.kt b/Prezel/core/network/src/main/java/com/team/prezel/core/network/ApiResponseConverterFactory.kt index d8cbdd29..9d87b3bd 100644 --- a/Prezel/core/network/src/main/java/com/team/prezel/core/network/ApiResponseConverterFactory.kt +++ b/Prezel/core/network/src/main/java/com/team/prezel/core/network/ApiResponseConverterFactory.kt @@ -6,9 +6,10 @@ import de.jensklingenberg.ktorfit.converter.Converter import de.jensklingenberg.ktorfit.converter.KtorfitResult import de.jensklingenberg.ktorfit.converter.TypeData import io.ktor.client.call.body +import io.ktor.client.plugins.ResponseException import io.ktor.client.statement.HttpResponse -import io.ktor.http.isSuccess import java.io.IOException +import kotlin.coroutines.cancellation.CancellationException class ApiResponseConverterFactory : Converter.Factory { override fun suspendResponseConverter( @@ -21,33 +22,23 @@ class ApiResponseConverterFactory : Converter.Factory { override suspend fun convert(result: KtorfitResult): ApiResponse = when (result) { is KtorfitResult.Success -> { - val response = result.response try { - if (response.status.isSuccess()) { - val body = response.body(typeData.typeArgs.first().typeInfo) - ApiResponse.Success(body) - } else { - ApiResponse.Error( - code = response.status.value, - message = response.status.description, - ) - } + val body = + result.response.body(typeData.typeArgs.first().typeInfo) + ApiResponse.Success(body) + } catch (e: CancellationException) { + throw e } catch (e: Exception) { - ApiResponse.Error( - code = response.status.value, - message = e.message ?: "Unknown error", - ) + ApiResponse.Failure.NetworkError } } is KtorfitResult.Failure -> { - when (result.throwable) { - is IOException -> ApiResponse.NetworkError - - else -> ApiResponse.Error( - code = -1, - message = result.throwable.message ?: "Unknown error", - ) + when (val t = result.throwable) { + is CancellationException -> throw t + is IOException -> ApiResponse.Failure.NetworkError + is ResponseException -> ApiResponse.Failure.HttpError(t) + else -> ApiResponse.Failure.NetworkError } } } diff --git a/Prezel/core/network/src/main/java/com/team/prezel/core/network/di/NetworkModule.kt b/Prezel/core/network/src/main/java/com/team/prezel/core/network/di/NetworkModule.kt index 112ee5b6..09eae8c7 100644 --- a/Prezel/core/network/src/main/java/com/team/prezel/core/network/di/NetworkModule.kt +++ b/Prezel/core/network/src/main/java/com/team/prezel/core/network/di/NetworkModule.kt @@ -38,6 +38,8 @@ object NetworkModule { @Singleton fun provideHttpClient(json: Json): HttpClient = HttpClient(OkHttp) { + expectSuccess = true + install(ContentNegotiation) { json(json) } diff --git a/Prezel/core/network/src/main/java/com/team/prezel/core/network/model/ApiResponse.kt b/Prezel/core/network/src/main/java/com/team/prezel/core/network/model/ApiResponse.kt index 99bfe30c..765b2963 100644 --- a/Prezel/core/network/src/main/java/com/team/prezel/core/network/model/ApiResponse.kt +++ b/Prezel/core/network/src/main/java/com/team/prezel/core/network/model/ApiResponse.kt @@ -5,10 +5,11 @@ sealed interface ApiResponse { val data: T, ) : ApiResponse - data class Error( - val code: Int, - val message: String, - ) : ApiResponse + sealed interface Failure : ApiResponse { + data class HttpError( + val throwable: Throwable, + ) : Failure - data object NetworkError : ApiResponse + data object NetworkError : Failure + } } From 8c7694da9bf8edb35cf0beb7990813511611346a Mon Sep 17 00:00:00 2001 From: Ham BeomJoon Date: Mon, 19 Jan 2026 16:41:45 +0900 Subject: [PATCH 06/19] =?UTF-8?q?feat:=20ApiResponseConverterFactory=20?= =?UTF-8?q?=EB=82=B4=20=EC=97=90=EB=9F=AC=20=EB=A1=9C=EA=B9=85=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 네트워크 요청 처리 중 발생하는 예외 상황을 파악하기 위해 Timber를 사용한 에러 로깅을 추가했습니다. * 응답 파싱 실패 시 로그 출력 추가 * KtorfitResult 실패 시 에러 유형별(IOException, ResponseException, 기타) 상세 로그 출력 추가 --- .../network/ApiResponseConverterFactory.kt | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/Prezel/core/network/src/main/java/com/team/prezel/core/network/ApiResponseConverterFactory.kt b/Prezel/core/network/src/main/java/com/team/prezel/core/network/ApiResponseConverterFactory.kt index 9d87b3bd..abb64726 100644 --- a/Prezel/core/network/src/main/java/com/team/prezel/core/network/ApiResponseConverterFactory.kt +++ b/Prezel/core/network/src/main/java/com/team/prezel/core/network/ApiResponseConverterFactory.kt @@ -8,6 +8,7 @@ import de.jensklingenberg.ktorfit.converter.TypeData import io.ktor.client.call.body import io.ktor.client.plugins.ResponseException import io.ktor.client.statement.HttpResponse +import timber.log.Timber import java.io.IOException import kotlin.coroutines.cancellation.CancellationException @@ -29,16 +30,31 @@ class ApiResponseConverterFactory : Converter.Factory { } catch (e: CancellationException) { throw e } catch (e: Exception) { + Timber.e(e, "Response parsing failed: ${e.message}") ApiResponse.Failure.NetworkError } } is KtorfitResult.Failure -> { when (val t = result.throwable) { - is CancellationException -> throw t - is IOException -> ApiResponse.Failure.NetworkError - is ResponseException -> ApiResponse.Failure.HttpError(t) - else -> ApiResponse.Failure.NetworkError + is CancellationException -> { + throw t + } + + is IOException -> { + Timber.e(t, "Network error: ${t.message}") + ApiResponse.Failure.NetworkError + } + + is ResponseException -> { + Timber.e(t, "HTTP error ${t.response.status.value}: ${t.message}") + ApiResponse.Failure.HttpError(t) + } + + else -> { + Timber.e(t, "Unknown error: ${t.message}") + ApiResponse.Failure.NetworkError + } } } } From 1c55e8b06490e1f5e1a547cde9c6dfbfc21f8916 Mon Sep 17 00:00:00 2001 From: Ham BeomJoon Date: Tue, 20 Jan 2026 01:51:18 +0900 Subject: [PATCH 07/19] =?UTF-8?q?refactor:=20ApiResponse.Failure.NetworkEr?= =?UTF-8?q?ror=20=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EC=A0=84=EB=8B=AC=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ApiResponse.Failure.NetworkError`를 `data object`에서 `data class`로 변경하여 발생한 예외(Throwable)를 포함하도록 수정했습니다. * `ApiResponse.Failure.NetworkError`에 `throwable` 필드 추가 * `ApiResponseConverterFactory`에서 네트워크 에러 및 파싱 실패 시 발생한 예외를 `NetworkError`에 담아서 반환하도록 수정 * `ApiResponseConverterFactory` 내 불필요한 타입 인자 접근 로직 최적화 --- .../prezel/core/network/ApiResponseConverterFactory.kt | 9 +++++---- .../com/team/prezel/core/network/model/ApiResponse.kt | 4 +++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Prezel/core/network/src/main/java/com/team/prezel/core/network/ApiResponseConverterFactory.kt b/Prezel/core/network/src/main/java/com/team/prezel/core/network/ApiResponseConverterFactory.kt index abb64726..f205fd33 100644 --- a/Prezel/core/network/src/main/java/com/team/prezel/core/network/ApiResponseConverterFactory.kt +++ b/Prezel/core/network/src/main/java/com/team/prezel/core/network/ApiResponseConverterFactory.kt @@ -18,6 +18,7 @@ class ApiResponseConverterFactory : Converter.Factory { ktorfit: Ktorfit, ): Converter.SuspendResponseConverter? { if (typeData.typeInfo.type != ApiResponse::class) return null + val bodyTypeInfo = typeData.typeArgs.firstOrNull()?.typeInfo ?: return null return object : Converter.SuspendResponseConverter> { override suspend fun convert(result: KtorfitResult): ApiResponse = @@ -25,13 +26,13 @@ class ApiResponseConverterFactory : Converter.Factory { is KtorfitResult.Success -> { try { val body = - result.response.body(typeData.typeArgs.first().typeInfo) + result.response.body(bodyTypeInfo) ApiResponse.Success(body) } catch (e: CancellationException) { throw e } catch (e: Exception) { Timber.e(e, "Response parsing failed: ${e.message}") - ApiResponse.Failure.NetworkError + ApiResponse.Failure.NetworkError(e) } } @@ -43,7 +44,7 @@ class ApiResponseConverterFactory : Converter.Factory { is IOException -> { Timber.e(t, "Network error: ${t.message}") - ApiResponse.Failure.NetworkError + ApiResponse.Failure.NetworkError(t) } is ResponseException -> { @@ -53,7 +54,7 @@ class ApiResponseConverterFactory : Converter.Factory { else -> { Timber.e(t, "Unknown error: ${t.message}") - ApiResponse.Failure.NetworkError + ApiResponse.Failure.NetworkError(t) } } } diff --git a/Prezel/core/network/src/main/java/com/team/prezel/core/network/model/ApiResponse.kt b/Prezel/core/network/src/main/java/com/team/prezel/core/network/model/ApiResponse.kt index 765b2963..83d44a66 100644 --- a/Prezel/core/network/src/main/java/com/team/prezel/core/network/model/ApiResponse.kt +++ b/Prezel/core/network/src/main/java/com/team/prezel/core/network/model/ApiResponse.kt @@ -10,6 +10,8 @@ sealed interface ApiResponse { val throwable: Throwable, ) : Failure - data object NetworkError : Failure + data class NetworkError( + val throwable: Throwable, + ) : Failure } } From 47a362356b3f96712c0a6c6c332951642dc74fb8 Mon Sep 17 00:00:00 2001 From: Ham BeomJoon Date: Tue, 20 Jan 2026 01:51:27 +0900 Subject: [PATCH 08/19] =?UTF-8?q?chore:=20NetworkModule=20JSON=20prettyPri?= =?UTF-8?q?nt=20=EB=B9=84=ED=99=9C=EC=84=B1=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit JSON 직렬화 설정에서 `prettyPrint` 옵션을 항상 `false`로 설정하도록 수정했습니다. --- .../main/java/com/team/prezel/core/network/di/NetworkModule.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Prezel/core/network/src/main/java/com/team/prezel/core/network/di/NetworkModule.kt b/Prezel/core/network/src/main/java/com/team/prezel/core/network/di/NetworkModule.kt index 09eae8c7..a77a1357 100644 --- a/Prezel/core/network/src/main/java/com/team/prezel/core/network/di/NetworkModule.kt +++ b/Prezel/core/network/src/main/java/com/team/prezel/core/network/di/NetworkModule.kt @@ -31,7 +31,7 @@ object NetworkModule { ignoreUnknownKeys = true coerceInputValues = true encodeDefaults = true - prettyPrint = BuildConfig.DEBUG + prettyPrint = false } @Provides From 1f3d7c3ea3ecadd41c301707636b4062c1635c7b Mon Sep 17 00:00:00 2001 From: Ham BeomJoon Date: Tue, 20 Jan 2026 01:51:34 +0900 Subject: [PATCH 09/19] =?UTF-8?q?feat:=20=EB=B9=8C=EB=93=9C=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=EB=B3=84=20BASE=5FURL=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20localProperty=20=ED=99=95=EC=9E=A5=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * `local.properties`에서 키 값을 기반으로 프로퍼티를 읽어오는 `localProperty` 확장 함수를 추가했습니다. * 빌드 타입(`DEBUG`, `RELEASE`)에 따라 서로 다른 `BASE_URL`을 사용하도록 `core:network` 모듈 설정을 수정했습니다. * 디버그 모드일 경우 기본 URL을 `http://10.0.2.2`로 설정했습니다. --- .../convention/LocalPropertiesExt.kt | 26 +++++++++++++++ Prezel/core/network/build.gradle.kts | 32 +++++++++---------- 2 files changed, 42 insertions(+), 16 deletions(-) create mode 100644 Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/LocalPropertiesExt.kt diff --git a/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/LocalPropertiesExt.kt b/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/LocalPropertiesExt.kt new file mode 100644 index 00000000..f0864a2f --- /dev/null +++ b/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/LocalPropertiesExt.kt @@ -0,0 +1,26 @@ +package com.team.prezel.buildlogic.convention + +import org.gradle.api.file.Directory +import org.gradle.api.provider.Provider +import org.gradle.api.provider.ProviderFactory +import java.io.StringReader +import java.util.Properties + +fun ProviderFactory.localProperty( + projectDirectory: Directory, + key: String, + default: String? = null, +): Provider = + fileContents(projectDirectory.file("local.properties")) + .asText + .map { text -> + val properties = Properties() + properties.load(StringReader(text)) + properties.getProperty(key) + }.let { provider -> + if (default != null) { + provider.orElse(default) + } else { + provider + } + } diff --git a/Prezel/core/network/build.gradle.kts b/Prezel/core/network/build.gradle.kts index 008b5ed0..9a26248d 100644 --- a/Prezel/core/network/build.gradle.kts +++ b/Prezel/core/network/build.gradle.kts @@ -1,6 +1,5 @@ import com.android.build.api.variant.BuildConfigField -import java.io.StringReader -import java.util.Properties +import com.team.prezel.buildlogic.convention.localProperty plugins { alias(libs.plugins.prezel.android.library) @@ -33,22 +32,23 @@ dependencies { testImplementation(libs.kotlinx.coroutines.test) } -val backendUrl = providers - .fileContents( - isolated.rootProject.projectDirectory.file("local.properties"), - ).asText - .map { text: String -> - val properties = Properties() - properties.load(StringReader(text)) - properties.getProperty("BASE_URL") - }.orElse("http://example.com") - androidComponents { - onVariants { - it.buildConfigFields!!.put( + onVariants { variant -> + val isRelease = variant.buildType == "release" + // DEBUG_RELEASE_BASE_URL 또는 RELEASE_BASE_URL + val key = "${variant.buildType!!.uppercase()}_BASE_URL" + + val urlProvider = providers + .localProperty( + projectDirectory = isolated.rootProject.projectDirectory, + key = key, + default = if (isRelease) null else "http://10.0.2.2", + ).map { it } + + variant.buildConfigFields!!.put( "BASE_URL", - backendUrl.map { value -> - BuildConfigField(type = "String", value = """"$value"""", comment = null) + urlProvider.map { value -> + BuildConfigField("String", """"$value"""", null) }, ) } From b740bdf04eb99d1024554c782eb67c69bca4ec56 Mon Sep 17 00:00:00 2001 From: Ham BeomJoon Date: Tue, 20 Jan 2026 02:05:17 +0900 Subject: [PATCH 10/19] =?UTF-8?q?build:=20Hilt=20=EC=BB=A8=EB=B2=A4?= =?UTF-8?q?=EC=85=98=20=ED=94=8C=EB=9F=AC=EA=B7=B8=EC=9D=B8=20=EB=82=B4=20?= =?UTF-8?q?kotlin-metadata-jvm=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `HiltConventionPlugin`에서 `kotlin-metadata-jvm` 라이브러리를 직접 관리하도록 변경하고, `app` 모듈의 중복 선언을 제거했습니다. --- Prezel/app/build.gradle.kts | 2 -- .../prezel/buildlogic/convention/plugin/HiltConventionPlugin.kt | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Prezel/app/build.gradle.kts b/Prezel/app/build.gradle.kts index c761d987..ef75ab70 100644 --- a/Prezel/app/build.gradle.kts +++ b/Prezel/app/build.gradle.kts @@ -18,6 +18,4 @@ dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.timber) - - ksp(libs.kotlin.metadata.jvm) } diff --git a/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/plugin/HiltConventionPlugin.kt b/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/plugin/HiltConventionPlugin.kt index 890d6092..b3474ef0 100644 --- a/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/plugin/HiltConventionPlugin.kt +++ b/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/plugin/HiltConventionPlugin.kt @@ -19,6 +19,7 @@ class HiltConventionPlugin : Plugin { dependencies { "ksp"(libs.findLibrary("hilt.compiler").get()) + "ksp"(libs.findLibrary("kotlin.metadata.jvm").get()) } pluginManager.withPlugin("org.jetbrains.kotlin.jvm") { From b7332d185c4327273f140e4aca35d4cb1b8edd13 Mon Sep 17 00:00:00 2001 From: Ham BeomJoon Date: Tue, 20 Jan 2026 02:22:50 +0900 Subject: [PATCH 11/19] =?UTF-8?q?refactor:=20local.properties=20=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F?= =?UTF-8?q?=20=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `local.properties` 파일을 읽어오는 로직을 개선하고 파일이나 키가 없을 경우에 대한 처리를 추가했습니다. * 파일이 존재하지 않거나 키를 찾을 수 없는 경우 경고 로그를 출력하도록 수정 * `fileContents().asText.map` 방식에서 `provider` 블록을 사용하는 방식으로 변경하여 가독성 개선 * 값이 없을 경우 기본값(`default`)을 반환하도록 로직 보완 --- .../convention/LocalPropertiesExt.kt | 38 ++++++++++++------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/LocalPropertiesExt.kt b/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/LocalPropertiesExt.kt index f0864a2f..da6acec7 100644 --- a/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/LocalPropertiesExt.kt +++ b/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/LocalPropertiesExt.kt @@ -1,26 +1,38 @@ package com.team.prezel.buildlogic.convention import org.gradle.api.file.Directory +import org.gradle.api.logging.Logger +import org.gradle.api.logging.Logging import org.gradle.api.provider.Provider import org.gradle.api.provider.ProviderFactory import java.io.StringReader import java.util.Properties +private val logger: Logger = Logging.getLogger("LocalProperties") + fun ProviderFactory.localProperty( projectDirectory: Directory, key: String, default: String? = null, -): Provider = - fileContents(projectDirectory.file("local.properties")) - .asText - .map { text -> - val properties = Properties() - properties.load(StringReader(text)) - properties.getProperty(key) - }.let { provider -> - if (default != null) { - provider.orElse(default) - } else { - provider - } +): Provider { + val localPropertiesFile = projectDirectory.file("local.properties") + + return provider { + val file = localPropertiesFile.asFile + if (!file.exists()) { + logger.warn("local.properties not found") + return@provider default + } + + val properties = Properties() + properties.load(StringReader(file.readText())) + val value = properties.getProperty(key) + + if (value == null) { + logger.warn("Key '$key' not found in local.properties") + return@provider default } + + value + } +} From edaabcf8c1f943846e974e7ad7fdc00bdcca3d66 Mon Sep 17 00:00:00 2001 From: Ham BeomJoon Date: Tue, 20 Jan 2026 02:36:44 +0900 Subject: [PATCH 12/19] =?UTF-8?q?refactor:=20local.properties=20=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20=EB=B0=A9=EC=8B=9D=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F?= =?UTF-8?q?=20BASE=5FURL=20=EC=84=A4=EC=A0=95=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `localProperty` 확장 함수를 `Project` 기반으로 간소화하고, 빌드 타입에 따른 `BASE_URL` 설정 로직을 강화했습니다. * `localProperty` 확장 함수 위치 및 구현 수정 (`ProviderFactory` -> `Project`) * 릴리즈 빌드 시 `BASE_URL` 누락에 대한 예외 처리 추가 * 디버그 빌드 시 `BASE_URL` 기본값(`http://10.0.2.2`) 적용 방식 수정 --- .../buildlogic/convention/LocalPropertiesExt.kt | 16 +++++----------- Prezel/core/network/build.gradle.kts | 16 +++++++++------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/LocalPropertiesExt.kt b/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/LocalPropertiesExt.kt index da6acec7..2146987f 100644 --- a/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/LocalPropertiesExt.kt +++ b/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/LocalPropertiesExt.kt @@ -1,27 +1,22 @@ package com.team.prezel.buildlogic.convention -import org.gradle.api.file.Directory +import org.gradle.api.Project import org.gradle.api.logging.Logger import org.gradle.api.logging.Logging import org.gradle.api.provider.Provider -import org.gradle.api.provider.ProviderFactory import java.io.StringReader import java.util.Properties private val logger: Logger = Logging.getLogger("LocalProperties") -fun ProviderFactory.localProperty( - projectDirectory: Directory, - key: String, - default: String? = null, -): Provider { - val localPropertiesFile = projectDirectory.file("local.properties") +fun Project.localProperty(key: String): Provider { + val localPropertiesFile = isolated.rootProject.projectDirectory.file("local.properties") - return provider { + return providers.provider { val file = localPropertiesFile.asFile if (!file.exists()) { logger.warn("local.properties not found") - return@provider default + return@provider null } val properties = Properties() @@ -30,7 +25,6 @@ fun ProviderFactory.localProperty( if (value == null) { logger.warn("Key '$key' not found in local.properties") - return@provider default } value diff --git a/Prezel/core/network/build.gradle.kts b/Prezel/core/network/build.gradle.kts index 9a26248d..f4ef423a 100644 --- a/Prezel/core/network/build.gradle.kts +++ b/Prezel/core/network/build.gradle.kts @@ -34,18 +34,20 @@ dependencies { androidComponents { onVariants { variant -> + val buildConfigFields = variant.buildConfigFields ?: return@onVariants val isRelease = variant.buildType == "release" // DEBUG_RELEASE_BASE_URL 또는 RELEASE_BASE_URL val key = "${variant.buildType!!.uppercase()}_BASE_URL" - val urlProvider = providers - .localProperty( - projectDirectory = isolated.rootProject.projectDirectory, - key = key, - default = if (isRelease) null else "http://10.0.2.2", - ).map { it } + val urlProvider = if (isRelease) { + localProperty(key).map { + it.ifEmpty { throw GradleException("$key is required for release builds") } + } + } else { + localProperty(key).orElse("http://10.0.2.2") + } - variant.buildConfigFields!!.put( + buildConfigFields.put( "BASE_URL", urlProvider.map { value -> BuildConfigField("String", """"$value"""", null) From 66fc4d564fc65720f68788660bf47f4c16fc735a Mon Sep 17 00:00:00 2001 From: Ham BeomJoon Date: Tue, 20 Jan 2026 02:37:32 +0900 Subject: [PATCH 13/19] =?UTF-8?q?refactor:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=A1=9C=EA=B1=B0=20=EB=B0=8F?= =?UTF-8?q?=20=EC=9E=84=ED=8F=AC=ED=8A=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `LocalPropertiesExt.kt`에서 사용되지 않는 로거(logger) 선언과 관련 임포트 문을 제거했습니다. --- .../team/prezel/buildlogic/convention/LocalPropertiesExt.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/LocalPropertiesExt.kt b/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/LocalPropertiesExt.kt index 2146987f..2d9dd0d5 100644 --- a/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/LocalPropertiesExt.kt +++ b/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/LocalPropertiesExt.kt @@ -1,14 +1,10 @@ package com.team.prezel.buildlogic.convention import org.gradle.api.Project -import org.gradle.api.logging.Logger -import org.gradle.api.logging.Logging import org.gradle.api.provider.Provider import java.io.StringReader import java.util.Properties -private val logger: Logger = Logging.getLogger("LocalProperties") - fun Project.localProperty(key: String): Provider { val localPropertiesFile = isolated.rootProject.projectDirectory.file("local.properties") From f19c3b3783d2984bd663f29f124fa44aae78aecd Mon Sep 17 00:00:00 2001 From: moondev03 Date: Tue, 20 Jan 2026 03:51:40 +0900 Subject: [PATCH 14/19] =?UTF-8?q?refactor:=20build-logic=20=EB=82=B4?= =?UTF-8?q?=EB=B6=80=20=EA=B5=AC=ED=98=84=EC=9D=84=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=20=EA=B5=AC=EC=A1=B0=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(build-logic): 내부 구현을 위한 `internal` 패키지 추가 및 관련 파일 이동 * refactor(build-logic): `localProperty` 유틸리티를 `external` 패키지로 이동 * refactor(network): `BASE_URL` 설정 로직 단순화 * chore(data): 불필요한 의존성 제거 --- .../{ => external}/LocalPropertiesExt.kt | 2 +- .../{ => internal}/AndroidCompose.kt | 2 +- .../{ => internal}/KotlinAndroid.kt | 2 +- .../{ => internal}/ProjectExtensions.kt | 4 ++-- ...droidApplicationComposeConventionPlugin.kt | 2 +- .../AndroidApplicationConventionPlugin.kt | 2 +- .../AndroidFeatureImplConventionPlugin.kt | 2 +- .../AndroidLibraryComposeConventionPlugin.kt | 2 +- .../plugin/AndroidLibraryConventionPlugin.kt | 4 ++-- .../convention/plugin/HiltConventionPlugin.kt | 2 +- .../plugin/JvmLibraryConventionPlugin.kt | 4 ++-- Prezel/core/data/build.gradle.kts | 1 - Prezel/core/network/build.gradle.kts | 20 ++++--------------- 13 files changed, 18 insertions(+), 31 deletions(-) rename Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/{ => external}/LocalPropertiesExt.kt (93%) rename Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/{ => internal}/AndroidCompose.kt (93%) rename Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/{ => internal}/KotlinAndroid.kt (97%) rename Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/{ => internal}/ProjectExtensions.kt (76%) diff --git a/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/LocalPropertiesExt.kt b/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/external/LocalPropertiesExt.kt similarity index 93% rename from Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/LocalPropertiesExt.kt rename to Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/external/LocalPropertiesExt.kt index 2d9dd0d5..440e8866 100644 --- a/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/LocalPropertiesExt.kt +++ b/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/external/LocalPropertiesExt.kt @@ -1,4 +1,4 @@ -package com.team.prezel.buildlogic.convention +package com.team.prezel.buildlogic.convention.external import org.gradle.api.Project import org.gradle.api.provider.Provider diff --git a/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/AndroidCompose.kt b/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/internal/AndroidCompose.kt similarity index 93% rename from Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/AndroidCompose.kt rename to Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/internal/AndroidCompose.kt index 0a57e87e..4f30fd7a 100644 --- a/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/AndroidCompose.kt +++ b/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/internal/AndroidCompose.kt @@ -1,4 +1,4 @@ -package com.team.prezel.buildlogic.convention +package com.team.prezel.buildlogic.convention.internal import com.android.build.api.dsl.CommonExtension import org.gradle.api.Project diff --git a/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/KotlinAndroid.kt b/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/internal/KotlinAndroid.kt similarity index 97% rename from Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/KotlinAndroid.kt rename to Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/internal/KotlinAndroid.kt index 661b4caa..7add3d06 100644 --- a/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/KotlinAndroid.kt +++ b/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/internal/KotlinAndroid.kt @@ -1,4 +1,4 @@ -package com.team.prezel.buildlogic.convention +package com.team.prezel.buildlogic.convention.internal import com.android.build.api.dsl.CommonExtension import org.gradle.api.JavaVersion diff --git a/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/ProjectExtensions.kt b/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/internal/ProjectExtensions.kt similarity index 76% rename from Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/ProjectExtensions.kt rename to Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/internal/ProjectExtensions.kt index e2343036..a36e5ce0 100644 --- a/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/ProjectExtensions.kt +++ b/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/internal/ProjectExtensions.kt @@ -1,9 +1,9 @@ -package com.team.prezel.buildlogic.convention +package com.team.prezel.buildlogic.convention.internal import org.gradle.api.Project import org.gradle.api.artifacts.VersionCatalog import org.gradle.api.artifacts.VersionCatalogsExtension import org.gradle.kotlin.dsl.getByType -val Project.libs +internal val Project.libs get(): VersionCatalog = extensions.getByType().named("libs") diff --git a/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/plugin/AndroidApplicationComposeConventionPlugin.kt b/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/plugin/AndroidApplicationComposeConventionPlugin.kt index 4c79a9b2..77fbb94f 100644 --- a/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/plugin/AndroidApplicationComposeConventionPlugin.kt +++ b/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/plugin/AndroidApplicationComposeConventionPlugin.kt @@ -1,7 +1,7 @@ package com.team.prezel.buildlogic.convention.plugin import com.android.build.api.dsl.ApplicationExtension -import com.team.prezel.buildlogic.convention.configureAndroidCompose +import com.team.prezel.buildlogic.convention.internal.configureAndroidCompose import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.apply diff --git a/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/plugin/AndroidApplicationConventionPlugin.kt b/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/plugin/AndroidApplicationConventionPlugin.kt index 2e796f0f..da33c831 100644 --- a/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/plugin/AndroidApplicationConventionPlugin.kt +++ b/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/plugin/AndroidApplicationConventionPlugin.kt @@ -1,7 +1,7 @@ package com.team.prezel.buildlogic.convention.plugin import com.android.build.api.dsl.ApplicationExtension -import com.team.prezel.buildlogic.convention.configureKotlinAndroid +import com.team.prezel.buildlogic.convention.internal.configureKotlinAndroid import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.apply diff --git a/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/plugin/AndroidFeatureImplConventionPlugin.kt b/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/plugin/AndroidFeatureImplConventionPlugin.kt index 1ee1a460..0f981deb 100644 --- a/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/plugin/AndroidFeatureImplConventionPlugin.kt +++ b/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/plugin/AndroidFeatureImplConventionPlugin.kt @@ -1,7 +1,7 @@ package com.team.prezel.buildlogic.convention.plugin import com.android.build.api.dsl.LibraryExtension -import com.team.prezel.buildlogic.convention.libs +import com.team.prezel.buildlogic.convention.internal.libs import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.apply diff --git a/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/plugin/AndroidLibraryComposeConventionPlugin.kt b/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/plugin/AndroidLibraryComposeConventionPlugin.kt index 6d10f810..9ebf8ce0 100644 --- a/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/plugin/AndroidLibraryComposeConventionPlugin.kt +++ b/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/plugin/AndroidLibraryComposeConventionPlugin.kt @@ -1,7 +1,7 @@ package com.team.prezel.buildlogic.convention.plugin import com.android.build.api.dsl.LibraryExtension -import com.team.prezel.buildlogic.convention.configureAndroidCompose +import com.team.prezel.buildlogic.convention.internal.configureAndroidCompose import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.apply diff --git a/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/plugin/AndroidLibraryConventionPlugin.kt b/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/plugin/AndroidLibraryConventionPlugin.kt index 963e6b69..d3d3a5cc 100644 --- a/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/plugin/AndroidLibraryConventionPlugin.kt +++ b/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/plugin/AndroidLibraryConventionPlugin.kt @@ -1,8 +1,8 @@ package com.team.prezel.buildlogic.convention.plugin import com.android.build.api.dsl.LibraryExtension -import com.team.prezel.buildlogic.convention.configureKotlinAndroid -import com.team.prezel.buildlogic.convention.libs +import com.team.prezel.buildlogic.convention.internal.configureKotlinAndroid +import com.team.prezel.buildlogic.convention.internal.libs import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.apply diff --git a/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/plugin/HiltConventionPlugin.kt b/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/plugin/HiltConventionPlugin.kt index b3474ef0..bcf7de93 100644 --- a/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/plugin/HiltConventionPlugin.kt +++ b/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/plugin/HiltConventionPlugin.kt @@ -1,6 +1,6 @@ package com.team.prezel.buildlogic.convention.plugin -import com.team.prezel.buildlogic.convention.libs +import com.team.prezel.buildlogic.convention.internal.libs import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.apply diff --git a/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/plugin/JvmLibraryConventionPlugin.kt b/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/plugin/JvmLibraryConventionPlugin.kt index c31144c6..242d2532 100644 --- a/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/plugin/JvmLibraryConventionPlugin.kt +++ b/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/plugin/JvmLibraryConventionPlugin.kt @@ -1,7 +1,7 @@ package com.team.prezel.buildlogic.convention.plugin -import com.team.prezel.buildlogic.convention.configureKotlinJvm -import com.team.prezel.buildlogic.convention.libs +import com.team.prezel.buildlogic.convention.internal.configureKotlinJvm +import com.team.prezel.buildlogic.convention.internal.libs import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.apply diff --git a/Prezel/core/data/build.gradle.kts b/Prezel/core/data/build.gradle.kts index 7e02018b..366eb6c7 100644 --- a/Prezel/core/data/build.gradle.kts +++ b/Prezel/core/data/build.gradle.kts @@ -10,6 +10,5 @@ android { dependencies { implementation(projects.core.network) - implementation(libs.kotlinx.coroutines.core) } diff --git a/Prezel/core/network/build.gradle.kts b/Prezel/core/network/build.gradle.kts index f4ef423a..55a12325 100644 --- a/Prezel/core/network/build.gradle.kts +++ b/Prezel/core/network/build.gradle.kts @@ -1,5 +1,5 @@ import com.android.build.api.variant.BuildConfigField -import com.team.prezel.buildlogic.convention.localProperty +import com.team.prezel.buildlogic.convention.external.localProperty plugins { alias(libs.plugins.prezel.android.library) @@ -25,7 +25,6 @@ dependencies { implementation(libs.kotlinx.serialization.json) implementation(libs.timber) - // Ktorfit implementation(libs.ktorfit.lib) ksp(libs.ktorfit.ksp) @@ -35,22 +34,11 @@ dependencies { androidComponents { onVariants { variant -> val buildConfigFields = variant.buildConfigFields ?: return@onVariants - val isRelease = variant.buildType == "release" - // DEBUG_RELEASE_BASE_URL 또는 RELEASE_BASE_URL - val key = "${variant.buildType!!.uppercase()}_BASE_URL" - - val urlProvider = if (isRelease) { - localProperty(key).map { - it.ifEmpty { throw GradleException("$key is required for release builds") } - } - } else { - localProperty(key).orElse("http://10.0.2.2") - } - + val key = "${variant.buildType?.uppercase()}_BASE_URL" buildConfigFields.put( "BASE_URL", - urlProvider.map { value -> - BuildConfigField("String", """"$value"""", null) + localProperty(key).map { value -> + BuildConfigField("String", "\"$value\"", null) }, ) } From 1bef85c8d1a51846a7c3791f90d44013e07393df Mon Sep 17 00:00:00 2001 From: moondev03 Date: Tue, 20 Jan 2026 04:54:41 +0900 Subject: [PATCH 15/19] =?UTF-8?q?refactor(network):=20ApiResponseConverter?= =?UTF-8?q?Factory=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ApiResponseConverterFactory의 `convert` 함수 내부에 있던 로직을 `parseSuccess`와 `mapFailure`라는 별도의 private 함수로 분리하여 가독성과 구조를 개선했습니다. 또한, `CancellationException` 처리를 위한 `rethrowIfCancellation` 확장 함수를 추가하여 중복 코드를 제거하고 예외 처리 로직을 명확하게 만들었습니다. --- .../network/ApiResponseConverterFactory.kt | 71 ++++++++++--------- 1 file changed, 39 insertions(+), 32 deletions(-) diff --git a/Prezel/core/network/src/main/java/com/team/prezel/core/network/ApiResponseConverterFactory.kt b/Prezel/core/network/src/main/java/com/team/prezel/core/network/ApiResponseConverterFactory.kt index f205fd33..8be77d8e 100644 --- a/Prezel/core/network/src/main/java/com/team/prezel/core/network/ApiResponseConverterFactory.kt +++ b/Prezel/core/network/src/main/java/com/team/prezel/core/network/ApiResponseConverterFactory.kt @@ -8,6 +8,7 @@ import de.jensklingenberg.ktorfit.converter.TypeData import io.ktor.client.call.body import io.ktor.client.plugins.ResponseException import io.ktor.client.statement.HttpResponse +import io.ktor.util.reflect.TypeInfo import timber.log.Timber import java.io.IOException import kotlin.coroutines.cancellation.CancellationException @@ -23,42 +24,48 @@ class ApiResponseConverterFactory : Converter.Factory { return object : Converter.SuspendResponseConverter> { override suspend fun convert(result: KtorfitResult): ApiResponse = when (result) { - is KtorfitResult.Success -> { - try { - val body = - result.response.body(bodyTypeInfo) - ApiResponse.Success(body) - } catch (e: CancellationException) { - throw e - } catch (e: Exception) { - Timber.e(e, "Response parsing failed: ${e.message}") - ApiResponse.Failure.NetworkError(e) - } - } + is KtorfitResult.Success -> parseSuccess(result.response, bodyTypeInfo) + is KtorfitResult.Failure -> mapFailure(result.throwable) + } + } + } - is KtorfitResult.Failure -> { - when (val t = result.throwable) { - is CancellationException -> { - throw t - } + private suspend fun parseSuccess( + response: HttpResponse, + bodyTypeInfo: TypeInfo, + ): ApiResponse { + return try { + val body = response.body(bodyTypeInfo) + ApiResponse.Success(body) + } catch (t: Throwable) { + t.rethrowIfCancellation() + Timber.e(t, "Response parsing failed") + ApiResponse.Failure.NetworkError(t) + } + } - is IOException -> { - Timber.e(t, "Network error: ${t.message}") - ApiResponse.Failure.NetworkError(t) - } + private fun mapFailure(t: Throwable): ApiResponse { + t.rethrowIfCancellation() - is ResponseException -> { - Timber.e(t, "HTTP error ${t.response.status.value}: ${t.message}") - ApiResponse.Failure.HttpError(t) - } + return when (t) { + is IOException -> { + Timber.e(t, "Network error") + ApiResponse.Failure.NetworkError(t) + } - else -> { - Timber.e(t, "Unknown error: ${t.message}") - ApiResponse.Failure.NetworkError(t) - } - } - } - } + is ResponseException -> { + Timber.e(t, "HTTP error ${t.response.status.value}") + ApiResponse.Failure.HttpError(t) + } + + else -> { + Timber.e(t, "Unknown error") + ApiResponse.Failure.NetworkError(t) + } } } + + private fun Throwable.rethrowIfCancellation() { + if (this is CancellationException) throw this + } } From 35377fbd6349cfc360cdc7ccd7f83b6f37fb5f40 Mon Sep 17 00:00:00 2001 From: moondev03 Date: Tue, 20 Jan 2026 04:55:01 +0900 Subject: [PATCH 16/19] =?UTF-8?q?refactor(network):=20ApiResponseConverter?= =?UTF-8?q?Factory=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ApiResponseConverterFactory의 `convert` 함수 내부에 있던 로직을 `parseSuccess`와 `mapFailure`라는 별도의 private 함수로 분리하여 가독성과 구조를 개선했습니다. 또한, `CancellationException` 처리를 위한 `rethrowIfCancellation` 확장 함수를 추가하여 중복 코드를 제거하고 예외 처리 로직을 명확하게 만들었습니다. --- .../team/prezel/core/network/ApiResponseConverterFactory.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Prezel/core/network/src/main/java/com/team/prezel/core/network/ApiResponseConverterFactory.kt b/Prezel/core/network/src/main/java/com/team/prezel/core/network/ApiResponseConverterFactory.kt index 8be77d8e..5569c137 100644 --- a/Prezel/core/network/src/main/java/com/team/prezel/core/network/ApiResponseConverterFactory.kt +++ b/Prezel/core/network/src/main/java/com/team/prezel/core/network/ApiResponseConverterFactory.kt @@ -33,8 +33,8 @@ class ApiResponseConverterFactory : Converter.Factory { private suspend fun parseSuccess( response: HttpResponse, bodyTypeInfo: TypeInfo, - ): ApiResponse { - return try { + ): ApiResponse = + try { val body = response.body(bodyTypeInfo) ApiResponse.Success(body) } catch (t: Throwable) { @@ -42,7 +42,6 @@ class ApiResponseConverterFactory : Converter.Factory { Timber.e(t, "Response parsing failed") ApiResponse.Failure.NetworkError(t) } - } private fun mapFailure(t: Throwable): ApiResponse { t.rethrowIfCancellation() From 2afbabe1c54b2e63736813df2286128940aa8589 Mon Sep 17 00:00:00 2001 From: moondev03 Date: Tue, 20 Jan 2026 04:57:23 +0900 Subject: [PATCH 17/19] =?UTF-8?q?feat(detekt):=20TooGenericExceptionCaught?= =?UTF-8?q?=20=EB=B9=84=ED=99=9C=EC=84=B1=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit detekt-config.yml에서 TooGenericExceptionCaught 규칙을 비활성화하여 일반적인 예외를 잡을 수 있도록 허용합니다. --- Prezel/detekt-config.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Prezel/detekt-config.yml b/Prezel/detekt-config.yml index ea5bfde0..c631b0fc 100644 --- a/Prezel/detekt-config.yml +++ b/Prezel/detekt-config.yml @@ -51,3 +51,5 @@ potential-bugs: exceptions: active: true + TooGenericExceptionCaught: + active: false From d75301acfc2d0882201c6e92c7acbfb668dde99f Mon Sep 17 00:00:00 2001 From: moondev03 Date: Tue, 20 Jan 2026 05:18:57 +0900 Subject: [PATCH 18/19] =?UTF-8?q?feat:=20BuildConfig=20BASE=5FURL=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EB=B0=A9=EC=8B=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `localProperty` 함수가 `Provider` 대신 `String`을 직접 반환하도록 수정했습니다. 이에 따라 `build.gradle.kts`에서 `BuildConfigField`를 설정하는 코드를 단순화했습니다. --- .../convention/external/LocalPropertiesExt.kt | 18 ++++-------------- Prezel/core/network/build.gradle.kts | 7 +++---- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/external/LocalPropertiesExt.kt b/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/external/LocalPropertiesExt.kt index 440e8866..a5950eab 100644 --- a/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/external/LocalPropertiesExt.kt +++ b/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/external/LocalPropertiesExt.kt @@ -1,28 +1,18 @@ package com.team.prezel.buildlogic.convention.external import org.gradle.api.Project -import org.gradle.api.provider.Provider import java.io.StringReader import java.util.Properties -fun Project.localProperty(key: String): Provider { +fun Project.localProperty(key: String): String { val localPropertiesFile = isolated.rootProject.projectDirectory.file("local.properties") return providers.provider { val file = localPropertiesFile.asFile - if (!file.exists()) { - logger.warn("local.properties not found") - return@provider null - } + if (!file.exists()) return@provider null val properties = Properties() properties.load(StringReader(file.readText())) - val value = properties.getProperty(key) - - if (value == null) { - logger.warn("Key '$key' not found in local.properties") - } - - value - } + properties.getProperty(key) + }.get() } diff --git a/Prezel/core/network/build.gradle.kts b/Prezel/core/network/build.gradle.kts index 55a12325..8bb18b91 100644 --- a/Prezel/core/network/build.gradle.kts +++ b/Prezel/core/network/build.gradle.kts @@ -34,12 +34,11 @@ dependencies { androidComponents { onVariants { variant -> val buildConfigFields = variant.buildConfigFields ?: return@onVariants - val key = "${variant.buildType?.uppercase()}_BASE_URL" + + val baseUrlKey = "${variant.buildType?.uppercase()}_BASE_URL" buildConfigFields.put( "BASE_URL", - localProperty(key).map { value -> - BuildConfigField("String", "\"$value\"", null) - }, + BuildConfigField("String", "\"${localProperty(baseUrlKey)}\"", null), ) } } From 681333878bdadea808faaf98fc0c7e679fa81d23 Mon Sep 17 00:00:00 2001 From: Ham BeomJoon Date: Tue, 20 Jan 2026 13:28:01 +0900 Subject: [PATCH 19/19] =?UTF-8?q?build:=20local.properties=20=EC=A0=91?= =?UTF-8?q?=EA=B7=BC=20=EB=B0=A9=EC=8B=9D=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F?= =?UTF-8?q?=20BASE=5FURL=20=EC=84=A4=EC=A0=95=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * `localProperty` 반환 타입을 `Provider`으로 변경하여 Lazy evaluation이 가능하도록 개선했습니다. * `local.properties` 파일이나 키가 없을 경우에 대한 경고 로그를 추가했습니다. * `core:network` 모듈의 `BASE_URL` BuildConfig 설정 시 `Provider`의 `map`을 사용하도록 수정했습니다. --- .../convention/external/LocalPropertiesExt.kt | 18 ++++++++++++++---- Prezel/core/network/build.gradle.kts | 7 +++++-- Prezel/core/network/proguard-rules.pro | 2 +- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/external/LocalPropertiesExt.kt b/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/external/LocalPropertiesExt.kt index a5950eab..440e8866 100644 --- a/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/external/LocalPropertiesExt.kt +++ b/Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/external/LocalPropertiesExt.kt @@ -1,18 +1,28 @@ package com.team.prezel.buildlogic.convention.external import org.gradle.api.Project +import org.gradle.api.provider.Provider import java.io.StringReader import java.util.Properties -fun Project.localProperty(key: String): String { +fun Project.localProperty(key: String): Provider { val localPropertiesFile = isolated.rootProject.projectDirectory.file("local.properties") return providers.provider { val file = localPropertiesFile.asFile - if (!file.exists()) return@provider null + if (!file.exists()) { + logger.warn("local.properties not found") + return@provider null + } val properties = Properties() properties.load(StringReader(file.readText())) - properties.getProperty(key) - }.get() + val value = properties.getProperty(key) + + if (value == null) { + logger.warn("Key '$key' not found in local.properties") + } + + value + } } diff --git a/Prezel/core/network/build.gradle.kts b/Prezel/core/network/build.gradle.kts index 8bb18b91..d36c3be7 100644 --- a/Prezel/core/network/build.gradle.kts +++ b/Prezel/core/network/build.gradle.kts @@ -33,12 +33,15 @@ dependencies { androidComponents { onVariants { variant -> + val buildType = variant.buildType ?: return@onVariants val buildConfigFields = variant.buildConfigFields ?: return@onVariants + val baseUrlKey = "${buildType.uppercase()}_BASE_URL" - val baseUrlKey = "${variant.buildType?.uppercase()}_BASE_URL" buildConfigFields.put( "BASE_URL", - BuildConfigField("String", "\"${localProperty(baseUrlKey)}\"", null), + localProperty(baseUrlKey).map { value -> + BuildConfigField("String", "\"$value\"", null) + }, ) } } diff --git a/Prezel/core/network/proguard-rules.pro b/Prezel/core/network/proguard-rules.pro index 481bb434..f1b42451 100644 --- a/Prezel/core/network/proguard-rules.pro +++ b/Prezel/core/network/proguard-rules.pro @@ -18,4 +18,4 @@ # If you keep the line number information, uncomment this to # hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file +#-renamesourcefileattribute SourceFile